Components Sortable

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

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.

Discovery call
Product demo
Proposal sent
Closed won

Press Space to lift this item, the arrow keys to move it, Space again to drop it, or Escape to cancel.

heex
<.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.

Ship the release build
Write the changelog
Send the invoices locked

Press Space to lift this item, the arrow keys to move it, Space again to drop it, or Escape to cancel.

heex
<.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.

heex
<.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.

First
Second
Third

Press Space to lift this item, the arrow keys to move it, Space again to drop it, or Escape to cancel.

heex
<.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