Sortable list
Reorder custom rows with handles, keyboard controls, and order callbacks.
Compose SortableList.Root, Item, and Grabber. You own each row’s content: labels, badges, checkboxes, and menus can live alongside the handle.
Preview
Research
Review customer feedback
Prototype
Explore the interaction
Release
Prepare the changelog
Order: research, prototype, release
<script lang="ts">
import * as SortableList from '$lib/components/flexi-sortable-list';
let order = $state(['research', 'prototype', 'release']);
const tasks = [
{ id: 'research', title: 'Research', detail: 'Review customer feedback' },
{ id: 'prototype', title: 'Prototype', detail: 'Explore the interaction' },
{ id: 'release', title: 'Release', detail: 'Prepare the changelog' }
];
</script>
<div class="w-full">
<SortableList.Root onreorder={(ids) => (order = ids)}>
{#each tasks as task (task.id)}
<SortableList.Item id={task.id}>
<SortableList.Grabber label={\`Move \$\{task.title}\`} />
<div class="min-w-0 flex-1">
<p class="font-medium">{task.title}</p>
<p class="text-muted-foreground text-sm">{task.detail}</p>
</div>
</SortableList.Item>
{/each}
</SortableList.Root>
<p class="text-muted-foreground mt-4 text-sm" aria-live="polite">Order: {order.join(', ')}</p>
</div>Installation
Start with a Tailwind project configured for shadcn and its theme variables. This copies editable source into your components directory; it does not install a second theme.
npx shadcn-svelte@latest add https://flexiboards.dev/r/svelte/flexi-sortable-list.jsonAPI
| Part | Props and defaults |
|---|---|
Root | All FlexiSortable props, plus onreorder(ids). Vertical by default, with gap-2. |
Item | All FlexiWidget props, plus a required, unique string id. It is stored in metadata.id for order reporting. |
Grabber | Grabber props, including an accessible label. |
Use keyed each blocks alongside the item id. The item’s id wins over a conflicting metadata.id.
Reading the order
The board owns the displayed layout; children declare the items, not a controlled order. The callback reports IDs in layout order after a layout change (including drops, deletion, and programmatic moves), not initial creation or layout import. Store the returned order in your application, or persist the full layout with config.onLayoutChange. Both callbacks run when provided.
A custom target key (key) is respected by the callback. For multiple lists that exchange items, use Board instead of nesting separate roots.
Customization
Use class on Root for spacing and on Item for the row surface. Set direction="horizontal" for a horizontal list. Underlying sizing and behavior options remain available through config, targetConfig, and individual widget props.
A handle keeps dragging away from the rest of the row. Place interactive controls beside the handle, not inside it. Resizing is off by default, as rows normally size to their content.
Existing convenience API
The original default export from sortable-list.svelte is retained.
It still accepts items: { id, label }[] and the reorder callback; new compositions should use the namespace API above.
Motion
Movement uses short CSS transitions by default and respects reduced motion. Compare presets or disable transitions.