Blazorise Drag & Drop component

Let users move and reorder items by dragging them between drop zones.

Use drag and drop for task boards, sortable lists, or interfaces where moving items directly is easier than choosing an action from a menu.

Structure

A DropContainer manages the items being dragged and the DropZone components that receive them.

  • <DropContainer>Coordinates the items and drag operations.
    • <DropZone>An area where items can be dropped.

Examples

Basic

To start, first define a DropContainer and assign the Items to it.

Next you define DropZone's and assign them a unique Name. The Name, along with the ItemsFilter parameter is used to determine to which DropZone an item will belong.

The callback ItemDropped is used to update the data item, when a drag operation has finished.

Basket

DragDrop image example Apple
DragDrop image example Bananas
DragDrop image example Broccoli
DragDrop image example Cherry

Fruit

DragDrop image example Lemon
DragDrop image example Strawberry

Vegetable

DragDrop image example Cabbage
<DropContainer TItem="DropItem" Items="@items" ItemsFilter="@((item, dropZone) => item.Group == dropZone)" ItemDropped="@ItemDropped" Flex="Flex.Wrap.Grow.Is1">
    <ChildContent>
        <DropZone TItem="DropItem" Name="Basket" Border="Border.Rounded" Background="Background.Light" Padding="Padding.Is3" Margin="Margin.Is3" Flex="Flex.Grow.Is1">
            <Heading Size="HeadingSize.Is4" Margin="Margin.Is3.FromBottom">Basket</Heading>
        </DropZone>
        <DropZone TItem="DropItem" Name="Fruit" DropAllowed="@((item) => item.Fruit)" Border="Border.Rounded" Background="Background.Light" Padding="Padding.Is3" Margin="Margin.Is3" Flex="Flex.Grow.Is1">
            <Heading Size="HeadingSize.Is4" Margin="Margin.Is3.FromBottom">Fruit</Heading>
        </DropZone>
        <DropZone TItem="DropItem" Name="Vegetable" DropAllowed="@((item) => !item.Fruit)" Border="Border.Rounded" Background="Background.Light" Padding="Padding.Is3" Margin="Margin.Is3" Flex="Flex.Grow.Is1">
            <Heading Size="HeadingSize.Is4" Margin="Margin.Is3.FromBottom">Vegetable</Heading>
        </DropZone>
    </ChildContent>
    <ItemTemplate>
        <Card Shadow="Shadow.Default" Margin="Margin.Is3.OnY">
            <CardBody>
                <Image Source="@context.Image" Text="DragDrop image example" Width="Width.Px( 48 )" Height="Height.Px( 48 )" />
                @context.Name
            </CardBody>
        </Card>
    </ItemTemplate>
</DropContainer>
@code {
    public class DropItem
    {
        public string Name { get; init; }

        public string Group { get; set; }

        public string Image { get; set; }

        public bool Fruit { get; set; }
    }

    private List<DropItem> items = new()
    {
        new DropItem() { Name = "Apple", Group = "Basket", Image = "img/fruit/apple.png", Fruit = true },
        new DropItem() { Name = "Bananas", Group = "Basket", Image = "img/fruit/bananas.png", Fruit = true },
        new DropItem() { Name = "Lemon", Group = "Fruit", Image = "img/fruit/lemon.png", Fruit = true },
        new DropItem() { Name = "Broccoli", Group = "Basket", Image = "img/fruit/broccoli.png" },
        new DropItem() { Name = "Strawberry", Group = "Fruit", Image = "img/fruit/strawberry.png", Fruit = true },
        new DropItem() { Name = "Cherry", Group = "Basket", Image = "img/fruit/cherry.png", Fruit = true },
        new DropItem() { Name = "Cabbage", Group = "Vegetable", Image = "img/fruit/cabbage.png" },
    };

    private Task ItemDropped( DraggableDroppedEventArgs<DropItem> dropItem )
    {
        dropItem.Item.Group = dropItem.DropZoneName;
        return Task.CompletedTask;
    }
}

Reorder items

Items can be reordered inside each dropzone with the AllowReorder bool set to true on the dropzone. The dragged item leaves a slot that moves with the insertion point, with or without animation. The Reordered function triggers each time the order changes or items are added or removed from the dropzone, providing the current order in a dictionary.

Drop Zone 1

Item 1
Item 2
Item 3

Drop Zone 2

Item 4
Item 5

Drop Zone 3

<DropContainer TItem="DropItem" Items="@items" ItemsFilter="@((item, dropZone) => item.Group == dropZone)" ItemDropped="@ItemDropped" Flex="Flex.Wrap.Grow.Is1">
    <ChildContent>
        @for ( int i = 1; i < 4; i++ )
        {
            var dropzone = i.ToString();

            <DropZone TItem="DropItem" Name="@dropzone" AllowReorder Reordered="@Reordered" Padding="Padding.Is3" Margin="Margin.Is3" Flex="Flex.Grow.Is1">
                <Heading Size="HeadingSize.Is4" Margin="Margin.Is3.FromBottom">Drop Zone @dropzone</Heading>
            </DropZone>
        }
    </ChildContent>
    <ItemTemplate>
        <Card Shadow="Shadow.Default" Margin="Margin.Is3.OnY">
            <CardBody>
                @context.Name
            </CardBody>
        </Card>
    </ItemTemplate>
</DropContainer>

<Div >
    @reorderStatus
</Div>
@code {
    public class DropItem
    {
        public string Name { get; init; }

        public string Group { get; set; }
    }

    private List<DropItem> items = new()
    {
        new DropItem() { Name = "Item 1", Group = "1" },
        new DropItem() { Name = "Item 2", Group = "1" },
        new DropItem() { Name = "Item 3", Group = "1" },
        new DropItem() { Name = "Item 4", Group = "2" },
        new DropItem() { Name = "Item 5", Group = "2" },
    };

    private Task ItemDropped( DraggableDroppedEventArgs<DropItem> dropItem )
    {
        dropItem.Item.Group = dropItem.DropZoneName;
        return Task.CompletedTask;
    }

    string reorderStatus = "";

    private Task Reordered( DropZoneOrder<DropItem> order )
    {
        reorderStatus = $"Order in dropzone {order.DestinationDropZoneName}: {string.Join( ", ", order.OrderedItems.OrderBy( x => x.Order ).Select( x => x.Item.Name ) )}";
        return Task.CompletedTask;
    }
}

Animated reordering

Animated movement helps users follow where items will land as they reorder a list. Enable Animated together with AllowReorder on a DropZone to animate neighboring items around the insertion placeholder.

Set AnimationDuration to control the movement duration in milliseconds. The default is 200. Animations are disabled by default, and a duration of zero or less disables them. The user's reduced motion preference is respected automatically.

Set ShowPlaceholder="true" to show a dashed outline or your PlaceholderTemplate inside the moving slot. Set it to false to hide the outline and content while retaining the slot's space. The default, null, shows the placeholder for non-animated reordering or when a custom template is supplied; otherwise, it leaves an empty slot. Use the controls below to compare visibility and animation independently.

Task priorities
Review the proposal
Plan the release
Update the documentation
Prepare the demo
<Field Display="Display.Flex" Flex="Flex.Wrap.AlignItems.Center" Gap="Gap.Is3">
    <Switch @bind-Value="animated">Animate reordering</Switch>
    <Switch @bind-Value="showPlaceholder">Show placeholder</Switch>
</Field>
<Field>
    <FieldLabel>Animation duration</FieldLabel>
    <FieldBody>
        <Select TValue="int" @bind-Value="animationDuration">
            <SelectItem TValue="int" Value="0">No animation</SelectItem>
            <SelectItem TValue="int" Value="100">100 ms</SelectItem>
            <SelectItem TValue="int" Value="200">200 ms</SelectItem>
            <SelectItem TValue="int" Value="400">400 ms</SelectItem>
            <SelectItem TValue="int" Value="800">800 ms</SelectItem>
        </Select>
    </FieldBody>
</Field>

<DropContainer TItem="DropItem" Items="@items" ItemsFilter="@(( item, dropZone ) => true)">
    <ChildContent>
        <DropZone TItem="DropItem"
                  Name="Tasks"
                  AllowReorder
                  ShowPlaceholder="@showPlaceholder"
                  Animated="@animated"
                  AnimationDuration="@animationDuration"
                  Border="Border.Is1.Rounded"
                  Padding="Padding.Is3">
            <Heading Size="HeadingSize.Is5">Task priorities</Heading>
        </DropZone>
    </ChildContent>
    <ItemTemplate>
        <Card Margin="Margin.Is2.OnY" Shadow="Shadow.Small">
            <CardBody Padding="Padding.Is3">
                @context.Name
            </CardBody>
        </Card>
    </ItemTemplate>
</DropContainer>
@code {
    private bool animated = true;

    private bool showPlaceholder;

    private int animationDuration = 200;

    private readonly List<DropItem> items = new()
    {
        new() { Name = "Review the proposal" },
        new() { Name = "Plan the release" },
        new() { Name = "Update the documentation" },
        new() { Name = "Prepare the demo" },
    };

    public class DropItem
    {
        public string Name { get; init; }
    }
}

Custom placeholder

Use PlaceholderTemplate to replace the default reorder indicator with custom content. The template receives the item being dragged and can be defined on the DropContainer or overridden by an individual DropZone.

This example places the zones in equal responsive columns so that longer placeholder content and changes in item count do not resize the zones during dragging.

Backlog

Write proposal
Review design

Done

Create prototype
<DropContainer TItem="DropItem" Items="@items" ItemsFilter="@(( item, dropZone ) => item.Group == dropZone)" ItemDropped="@ItemDropped">
    <ChildContent>
        <Row>
            @foreach ( var dropZoneName in DropZoneNames )
            {
                <Column ColumnSize="ColumnSize.Is12.OnMobile.Is6.OnDesktop" Margin="Margin.Is3.OnY">
                    <DropZone TItem="DropItem" Name="@dropZoneName" AllowReorder Border="Border.Rounded" Background="Background.Light" Padding="Padding.Is3" Height="Height.Is100">
                        <Heading Size="HeadingSize.Is4" Margin="Margin.Is3.FromBottom">@dropZoneName</Heading>
                    </DropZone>
                </Column>
            }
        </Row>
    </ChildContent>
    <ItemTemplate>
        <Card Shadow="Shadow.Default" Margin="Margin.Is2.OnY">
            <CardBody Padding="Padding.Is3">
                @context.Name
            </CardBody>
        </Card>
    </ItemTemplate>
    <PlaceholderTemplate>
        <Div Background="Background.Primary.Subtle"
             TextColor="TextColor.Primary.Emphasis"
             Border="Border.Is1.Rounded"
             Padding="Padding.Is3"
             Margin="Margin.Is2.OnY"
             Flex="Flex.JustifyContent.Center.AlignItems.Center"
             Gap="Gap.Is2">
            <Icon Name="IconName.ArrowDown" />
            Place @context.Name here
        </Div>
    </PlaceholderTemplate>
</DropContainer>
@code {
    public class DropItem
    {
        public string Name { get; init; }

        public string Group { get; set; }
    }

    private static readonly string[] DropZoneNames = new[] { "Backlog", "Done" };

    private List<DropItem> items = new()
    {
        new DropItem() { Name = "Write proposal", Group = "Backlog" },
        new DropItem() { Name = "Review design", Group = "Backlog" },
        new DropItem() { Name = "Create prototype", Group = "Done" },
    };

    private Task ItemDropped( DraggableDroppedEventArgs<DropItem> dropItem )
    {
        dropItem.Item.Group = dropItem.DropZoneName;

        return Task.CompletedTask;
    }
}

Best Practices

Drag Alternative

Every operation available through dragging should also have a keyboard-accessible method, such as move buttons. Make draggable items and valid drop zones visually clear, and show immediate feedback for both allowed and rejected drops.

To do

Review pull request
Update release notes

Done

Publish package
<DropContainer @ref="dropContainer" TItem="WorkItem" Items="@items" ItemsFilter="@(( item, dropZone ) => item.Group == dropZone)" ItemDropped="@OnItemDropped" Flex="Flex.Wrap.Grow.Is1">
    <ChildContent>
        <DropZone TItem="WorkItem" Name="@TodoZone" Border="Border.Rounded" Background="Background.Light" Padding="Padding.Is3" Margin="Margin.Is3" Flex="Flex.Grow.Is1">
            <Heading Size="HeadingSize.Is4" Margin="Margin.Is3.FromBottom">@TodoZone</Heading>
        </DropZone>
        <DropZone TItem="WorkItem" Name="@DoneZone" Border="Border.Rounded" Background="Background.Light" Padding="Padding.Is3" Margin="Margin.Is3" Flex="Flex.Grow.Is1">
            <Heading Size="HeadingSize.Is4" Margin="Margin.Is3.FromBottom">@DoneZone</Heading>
        </DropZone>
    </ChildContent>
    <ItemTemplate>
        <Card @key="@context.Id" Shadow="Shadow.Default" Margin="Margin.Is3.OnY">
            <CardBody>
                <Div Display="Display.Flex" Flex="Flex.AlignItems.Center.JustifyContent.Between" Gap="Gap.Is3">
                    <Text>@context.Name</Text>
                    <Button Color="Color.Secondary" Size="Size.Small" Clicked="@(() => MoveItem( context, context.Group == TodoZone ? DoneZone : TodoZone ))">
                        Move to @(context.Group == TodoZone ? DoneZone : TodoZone)
                    </Button>
                </Div>
            </CardBody>
        </Card>
    </ItemTemplate>
</DropContainer>
@code {
    private const string TodoZone = "To do";
    private const string DoneZone = "Done";

    private DropContainer<WorkItem> dropContainer;

    private List<WorkItem> items =
    [
        new( 1, "Review pull request", TodoZone ),
        new( 2, "Update release notes", TodoZone ),
        new( 3, "Publish package", DoneZone ),
    ];

    private Task OnItemDropped( DraggableDroppedEventArgs<WorkItem> eventArgs )
        => MoveItem( eventArgs.Item, eventArgs.DropZoneName );

    private Task MoveItem( WorkItem item, string destination )
    {
        items = items
            .Select( current => current.Id == item.Id ? current with { Group = destination } : current )
            .ToList();

        dropContainer?.Refresh();

        return Task.CompletedTask;
    }

    public sealed record WorkItem( int Id, string Name, string Group );
}

Validate Every Move

Use stable item identifiers and update the source collection through one state owner after a successful drop. When ordering, permissions, or destination rules affect persisted data, validate the move again in application code before saving it.

API

See the API reference for the parameters, events, methods, and related types available to the components covered on this page.