A reusable drag-and-drop sortable component supporting both grid and list layouts.
Uses SortableJS (auto-loaded from CDN) to enable drag-and-drop reordering of items. The component sends a LiveView event when items are reordered.
Design Philosophy
This component provides minimal opinionated styling - it handles drag-drop behavior while you control the appearance through classes and slot content.
Usage - Grid Layout (default)
Perfect for image galleries, card grids, etc:
<.draggable_list
id="post-images"
items={@images}
on_reorder="reorder_images"
cols={4}
>
<:item :let={img}>
<img src={img.url} class="w-full aspect-square object-cover rounded" />
</:item>
<:add_button>
<button phx-click="add_image" class="btn">Add</button>
</:add_button>
</.draggable_list>Usage - List Layout
Perfect for column selectors, ordered lists, etc:
<.draggable_list
id="table-columns"
items={@columns}
on_reorder="reorder_columns"
layout={:list}
item_class="flex items-center p-3 bg-base-100 border rounded-lg hover:bg-base-200"
>
<:item :let={col}>
<div class="mr-3 text-base-content/40">
<.icon name="hero-bars-3" class="w-5 h-5" />
</div>
<span class="flex-1 font-medium">{col.label}</span>
<button phx-click="remove_column" phx-value-id={col.id} class="btn btn-ghost btn-xs">
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
</:item>
</.draggable_list>Event Handler
The on_reorder event receives %{"ordered_ids" => [id1, id2, ...]} with the new order:
def handle_event("reorder_items", %{"ordered_ids" => ordered_ids}, socket) do
# ordered_ids is a list of item IDs in the new order
{:noreply, socket}
end
Summary
Functions
Attributes
id(:string) (required) - Unique ID for the container.items(:list) (required) - List of items to display.item_id(:any) - Function to extract ID from item, defaults to &(&1.id). Defaults tonil.on_reorder(:string) (required) - Event name to send on reorder.layout(:atom) - Layout mode. Defaults to:grid. Must be one of:grid, or:list.cols(:integer) - Number of grid columns (only for layout={:grid}). Defaults to4.gap(:string) - Gap between items (Tailwind class). Defaults to"gap-2".class(:string) - Additional CSS classes for the container. Defaults to"".item_class(:string) - Additional CSS classes for each item wrapper. Defaults to"".
Slots
item(required) - Slot to render each item, receives the item as let.add_button- Optional slot for add button at end of container.