Sortable
Drag-to-reorder lists and grids over LiveView - optimistic on the client, the server stays the source of truth, and keyboard grab-and-move comes free.
Drag to reorder
Grab any row and drag it. On drop the component pushes one on_reorder event with the item id and its old and new index, and your handler persists the order. Tab to a row and press Space to do the same thing without a mouse. Needs the PetalSortable hook from the bundle.
Press Space to lift this item, the arrow keys to move it, Space again to drop it, or Escape to cancel.
<.sortable id="showcase-stages" on_reorder="reorder_stages">
<:item id="discovery" label="Discovery call">Discovery call</:item>
<:item id="demo" label="Product demo">Product demo</:item>
<:item id="proposal" label="Proposal sent">Proposal sent</:item>
<:item id="closed" label="Closed won">Closed won</:item>
</.sortable>
Handle only
handle puts a grip on each row and only the grip starts a drag, so everything else in the row stays clickable. The grip is also the tab stop, with its own accessible name.
Press Space to lift this item, the arrow keys to move it, Space again to drop it, or Escape to cancel.
<.sortable id="showcase-todos" on_reorder="reorder_todos" handle>
<:item id="deploy" label="Ship the release build">
<span>Ship the release build</span>
</:item>
<:item id="changelog" label="Write the changelog">
<span>Write the changelog</span>
</:item>
<:item id="invoices" label="Send the invoices" disabled>
<span>Send the invoices</span>
<span class="ml-auto text-xs text-gray-500 dark:text-gray-400">locked</span>
</:item>
</.sortable>
Grid
orientation="grid" wraps items into a grid and teaches the arrow keys to move in two dimensions. The column count is read from the live layout, so a responsive grid steps correctly at every breakpoint.
Press Space to lift this item, the arrow keys to move it, Space again to drop it, or Escape to cancel.
<.sortable
id="showcase-gallery"
on_reorder="reorder_photos"
orientation="grid"
class="grid-cols-2 sm:grid-cols-3"
>
<:item
:for={photo <- gallery()}
id={photo.id}
label={photo.title}
class="flex-col items-stretch gap-2 p-2"
>
<div class={["aspect-video w-full rounded-md bg-gradient-to-br", photo.tone]}></div>
<span class="text-sm font-medium">{photo.title}</span>
</:item>
</.sortable>
Disabled
disabled on the container turns the whole list inert: no drag, no keyboard lift, and the items leave the tab order. Use it while an order is saving, or when the viewer has read-only access.
Press Space to lift this item, the arrow keys to move it, Space again to drop it, or Escape to cancel.
<.sortable id="showcase-locked" on_reorder="reorder_locked" handle disabled>
<:item id="one" label="First">First</:item>
<:item id="two" label="Second">Second</:item>
<:item id="three" label="Third">Third</:item>
</.sortable>
Properties
| Attribute | Type | Default | Description |
|---|---|---|---|
class
|
any |
nil
|
extra classes for the list container |
disabled
|
boolean |
false
|
disables all drag and keyboard reorder |
handle
|
boolean |
false
|
when true only the grip handle initiates drag; when false the whole item does |
id*
|
string |
DOM id; the hook mounts here | |
on_reorder*
|
string |
event name pushed on drop with %{id, from, to} (string-key params) | |
orientation
|
string |
"vertical"
|
vertical = single-column list; grid = wrapping grid, arrow keys move in 2D
one of: "vertical", "grid"
|
rest
|
global |
passed to the list container, e.g. phx-update="stream" | |
target
|
any |
nil
|
optional phx-target for the reorder event (a live component's @myself) |
:item
slot
|
slot |
one per sortable entry |