Learn how to implement drag-and-drop functionality in Blazor and how Telerik UI for Blazor makes it even simpler for developers.
One of the most important things for modern web applications is user and developer experience. With Blazor, we have a simple yet powerful component model. For many applications, drag-and-drop functionality is key in providing a seamless user experience.
We will learn how to implement drag-and-drop functionality in Blazor and how the Progress Telerik UI for Blazor component library makes it even simpler for developers.
You can access the code used in this example on GitHub.
Modern browsers support HTML5 and its Drag-and-Drop API. Drag and drop works as two parts:
First, we define certain HTML elements as draggable, which allows the user to drag and drop them into target areas called drop zones.
Second, JavaScript implements the interaction logic required to perform the drag-and-drop behind the scenes, such as moving an employee from one team to another.
Consider the following minimalistic Drag-and-Drop example developed with HTML5 and JavaScript:

Now, let’s discuss the code:
<div>
<div draggable="true" id="emp1">Alice</div>
<div draggable="true" id="emp2">Bob</div>
<div draggable="true" id="emp3">Charlie</div>
</div>
<div>
<h2>Team A</h2>
<div class="dropzone" id="teamA"></div>
</div>
<div>
<h2>Team B</h2>
<div class="dropzone" id="teamB"></div>
</div>
We have a div containing different employees represented using another div element. The draggable attribute defines an HTML element that can be moved by the user.
In contrast to the draggable attribute, the drop zones are regular div elements without any special attributes applied. In this case, we use the class dropzone to identify the drop zones in the JavaScript code, but there are other solutions.
We can now use JavaScript to handle the DOM events to perform the drag and drop operation and update the application state:
const state = {
employees: {
emp1: { name: 'Alice', team: null },
emp2: { name: 'Bob', team: null },
emp3: { name: 'Charlie', team: null },
},
};
const employees = document.querySelectorAll('.employee');
employees.forEach(employee => {
employee.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', employee.id);
});
});
For the draggable items, we need to add event listeners for the dragstart event and use the dataTransfer property to cache the employeeId.
const dropzones = document.querySelectorAll('.dropzone');
dropzones.forEach(zone => {
zone.addEventListener('dragover', e => {
e.preventDefault();
});
zone.addEventListener('drop', e => {
e.preventDefault();
const empId = e.dataTransfer.getData('text/plain');
const employee = document.getElementById(empId);
if (employee) {
zone.appendChild(employee);
// Update application state
const teamId = zone.id;
state.employees[empId].team = teamId;
}
});
});
For the drop zones, we need to register an event handler for the dragover event and prevent the default action (otherwise dropping does not work), and the drop event to handle a performed drop operation.
Again, we use the dataTransfer property to access the cached employeeId and add the employee div to the drop zone. Thereafter, we update the application state.
The code shown is just the essential bits and pieces. You can access the working HTML file on GitHub.
In the previous example, we only scratched the surface of the native HTML5 Drag-and-Drop API.
Unfortunately, there are subtle differences between different browsers. For example, Firefox requires extra handling for some file types, and some touch devices (especially iOS) don’t fully support the native API.
Another limitation is that we must use JavaScript hacks for HTML elements that do not support the draggable attribute.
Further, there are user experience challenges, such as limited control over animations and inconsistent event orders for dragenter, dragover and dragleave events.
The Telerik UI for Blazor library provides components, such as the Blazor ListBox component with integrated drag-and-drop support. Those components implement cross-browser consistency and built-in support for touch interactions. You can even customize drag previews and get around the draggable limitation.
In the previous example, you could see that with HTML5 and JavaScript, we need to handle various events on different elements. Now, let’s look at how we can handle drag-and-drop in Blazor.
Hint: Before we begin, it’s important to state that there are multiple ways to implement drag-and-drop for Blazor. The solution shown here is my personal favorite.
First, Blazor does not have direct access to the DOM and browser APIs. Therefore, we need to utilize JavaScript interoperability when implementing drag-and-drop in Blazor.
On the upside: The implementation works for Blazor Server and Blazor WebAssembly interactivity.
First, let’s start with the Teams.razor file. It’s the page that renders the employees and lets the user assign them to a specific team.
@page "/teams"
@implements IDisposable
@inject IJSRuntime JS
<h3>Employees</h3>
<div class="column">
@foreach (var emp in Employees.Where(e => e.Team is null))
{
<EmployeeItem @key="emp.Id" Employee="emp" />
}
</div>
<h3>Team A</h3>
<div class="dropzone" @ref="teamARef">
@foreach (var emp in Employees.Where(e => e.Team == "A"))
{
<EmployeeItem @key="emp.Id" Employee="emp" />
}
</div>
<h3>Team B</h3>
<div class="dropzone" @ref="teamBRef">
@foreach (var emp in Employees.Where(e => e.Team == "B"))
{
<EmployeeItem @key="emp.Id" Employee="emp" />
}
</div>
The template code is straightforward.
We use the @page directive to register this Razor component as a routable page.
Next, we implement the IDisposable interface because we will work with refs that need to be manually disposed.
We also inject an instance of the IJSRuntime interface to be able to call JavaScript interop code from our page component.
We have three divs. The first contains all employees, the second and third are drop zones for Team A and Team B. Notice the @ref attribute on both drop zones.
In the code section, we first declare a few properties and fields:
private List<Employee> Employees = new()
{
new Employee { Id = "emp1", Name = "Alice" },
new Employee { Id = "emp2", Name = "Bob" },
new Employee { Id = "emp3", Name = "Charlie" }
};
private ElementReference teamARef;
private ElementReference teamBRef;
private DotNetObjectReference<Teams>? dotNetRef;
We have a list of three employees. The Employee class is defined in a separate file and looks like this:
public class Employee
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string? Team { get; set; }
}
We also have two ElementReferences teamARef and teamBRef that we bind to their drop zones in the template code.
We also have an instance of the DotNetObjectReference type, which allows us to go full circle with the JavaScript-integration and call back to our .NET code from the JavaScript code.
Next, we implement the OnAfterRenderAsync method, where we register the drop zones using JavaScript interoperability.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
dotNetRef = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("dragDropInterop.registerDropZone",
teamARef, dotNetRef, "A");
await JS.InvokeVoidAsync("dragDropInterop.registerDropZone",
teamBRef, dotNetRef, "B");
}
}
We create a DotNetObjectReference object and call the registerDropZone functions.
[JSInvokable]
public Task OnEmployeeDropped(string employeeId, string team)
{
var emp = Employees.FirstOrDefault(e => e.Id == employeeId);
if (emp is not null)
{
emp.Team = team;
StateHasChanged();
}
return Task.CompletedTask;
}
public void Dispose()
{
dotNetRef?.Dispose();
}
Next, we have an OnEmployeeDropped method that will be called from the JavaScript code when a drag-and-drop operation has been completed by the user. Notice the [JSInvokeable] attribute, which is required for the method to be called from the JavaScript code.
And the Dispose method correctly disposes of the dotNetRef object.
Hint: I don’t show the CSS definitions in this article, but you can access the full code on GitHub.
Before we look at the JavaScript interop code, let’s explore the EmployeeItem referenced in the template of the Teams page component.
<div @ref="elementRef" class="employee">
@Employee.Name
</div>
@code {
[Parameter] public Employee Employee { get; set; } = default!;
[Inject] private IJSRuntime JS { get; set; } = default!;
private ElementReference elementRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync(
"dragDropInterop.addDragStartListener",
elementRef,
Employee.Id);
}
}
}
The template code is straightforward. We use a div and assign an ElementReference object to its ref attribute.
In the OnAfterRenderAsync implementation, we call the addDragStartListener function for the first render of the component and provide the ID of the employee.
Now, let’s finally look at the JavaScript interop code holding everything together and making drag-and-drop work:
We create a new dragDropInterop.js file inside the wwwroot/js folder:
window.dragDropInterop = {
addDragStartListener: function (element, employeeId) {
if (!element) return;
element.setAttribute('draggable', 'true');
element.addEventListener('dragstart', function (event) {
event.dataTransfer.setData('text/plain', employeeId);
});
},
registerDropZone: function (element, dotnetHelper, team) {
if (!element) return;
element.addEventListener('dragover', function (event) {
event.preventDefault();
});
element.addEventListener('drop', function (event) {
event.preventDefault();
const draggedId = event.dataTransfer.getData('text/plain');
if (draggedId) {
dotnetHelper.invokeMethodAsync('OnEmployeeDropped', draggedId, team);
}
});
}
};
We add two functions to the window object.
The addDragStartListener function is called from the OnAfterRenderAsync lifecycle method when an EmployeeItem is first rendered.
This function dynamically adds the draggable attribute, letting the browser know the user can drag this HTML element. We also register an event listener for the dragstart event to cache the employee ID, so that we know which employee is being dragged.
Similar to the pure HTML and JavaScript implementation, we use the dataTransfer property and its setData function.
The registerDropZone function is called from the OnAfterRenderAsync method on the Teams page component when rendering for the first time.
In this function, we register an event handler for the dragover event and call the preventDefault function. Again, this is important to make dragging work.
We also register an event listener for the drop event. Here, besides also calling the preventDefault function, we get the ID of the dragged employee and call the .NET method OnEmployeeDropped on the Teams page component using the DotNetObjectReference we received when the registerDropZone function was called from the .NET code.
Important: Don’t forget to add a reference to the dragDropInterop.js file within the App.razor file to load the JavaScript code when the application runs in the browser:
<!DOCTYPE html>
<html lang="en">
<! -- code omitted -->
<body>
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
<script src="js/dragDropInterop.js"></script>
</body>
</html>
Now, run your Blazor application.

Navigate to the Teams page (adjust the code in the NavMenu component, or add /teams in the address bar), and you should be able to see the three employees in the Employee list initially.
You can assign them to Team A or B and even change teams using Drag-and-Drop.

Let’s highlight the differences between Blazor Drag-and-Drop and a native HTML5 & JavaScript implementation.
Object binding provides better control and a simpler way to handle state. We use C# properties and bind state instead of imperative HTML5 APIs.
We use less code with binding events to C# methods instead of adding event listeners using the imperative JavaScript API. We can encapsulate the required JavaScript code in an interop.js file and reuse it for multiple drag-and-drop implementations.
Last, debugging is much more powerful using the full C# debugger and type safety than console logs or the browser’s dev tools.
On the flipside, because Blazor does not have direct access to DOM events and the Drag-and-Drop API, we need to use JavaScript interop. However, we could encapsulate that behavior. It’s exactly what component libraries, such as Telerik UI for Blazor, do.
While Drag-and-Drop is a modern and generally user-friendly feature, consider the following tips and tricks:
ondragenter and ondragover event handlers to implement such behavior.invalid class to the HTML element.Drag-and-drop is an integral part of modern web applications. In Blazor web applications, we use JavaScript interop to orchestrate it.
With data binding, we get a simple way to keep track and render the application state when a drag-and-drop operation is performed.
Make sure to follow best practices and properly validate drop targets and highlight available drop zones in your applications.
You can access the code used in this example on GitHub.
If you want to learn more about Blazor development, you can watch my free Blazor Crash Course on YouTube. And stay tuned to the Telerik blog for more Blazor Basics.
Try out the Telerik UI for Blazor drag-and-drop features for yourself. The library comes with a free 30-day trial.
Claudio Bernasconi is a passionate software engineer and content creator writing articles and running a .NET developer YouTube channel. He has more than 10 years of experience as a .NET developer and loves sharing his knowledge about Blazor and other .NET topics with the community.