Swappable
Drag-and-drop list reordering with a tiny contract: provide an items array and a render snippet, then opt into hold-to-drag or instant dragging. No built-in styles are imposed, so you control the handle, focus states, and motion.
Alpha
Bravo
Charlie
Press and hold the handle to start dragging. Drag up or down to reorder;
Usage
<script lang="ts">
import { Swappable } from '@obelus/trioxide';
type Item = { id: string; title: string };
let items: Item[] = $state([{ title: 'Alpha' }, { title: 'Bravo' }, { title: 'Charlie' }]);
</script>
<Swappable
bind:items
drag-on-hold={120}
threshold={18}
onSwap={(from, to) => console.log(from, to)}
>
{#snippet Item(item, bindings)}
{@const { element, handle, isDragging } = bindings}
<article {...element} class={`${isDragging ? 'z-10' : ''}`}>
<button
{...handle}
aria-label={`Reorder ${item.title}`}
class={isDragging ? 'cursor-grabbing' : 'cursor-grab'}
>
<i class="ri-draggable" />
</button>
<span class="ml-2">{item.title}</span>
</article>
{/snippet}
</Swappable> items is mutated in place as the user swaps rows, so keep it in a $state store if you want to persist order changes.
Props
items: array being reordered. Items are keyed by identity.Item: snippet with signature(props, bindings). Render your row and spread the provided bindings.onSwap(from, to?): optional callback fired after two items exchange places (indices are zero-based).threshold(default20): vertical pixels the pointer must move before a swap occurs.drag-on-hold(default0): delay in ms before a drag starts after pressing the handle; set >0 to avoid accidental drags on click.
Item bindings
bindings contains the affordances needed for drag interactions:
element: spread onto the row container; applies inlinetranslatewhile dragging and registers cleanup.handle: spread onto the drag handle (or the entire row) to start the gesture on mouse/touch down.isDragging: boolean you can use to change cursor, elevation, or opacity.dy: current pixel offset (useful if you want to animate shadows or scale).
Behavior
- Drag vertically to swap neighboring items; the swap happens once you cross
thresholdpixels. - Works with mouse and touch; listeners are attached to
document.bodyso the drag continues outside the item bounds. - The component is unopinionated about layout—flex rows, grid items, or cards all work as long as you attach
elementandhandle. - Mutates the original
itemsarray; callonSwapto persist changes or trigger side effects (e.g., saving to a server).