Sortable
Midnight Drive
Neon Skyline
Paper Planes
The Cartographers
Low Tide
Harbor Lights
Static Bloom
Velvet Antenna
Glass Orchard
Fern & Field
import { GripVertical } from "lucide-solid";import { createSignal, For } from "solid-js";import { Sortable, SortableItem, SortableItemHandle } from "@/registry/kobalte/blocks/sortable";import { Badge } from "~/components/ui/badge";
type Track = { id: string; title: string; artist: string; duration: string;};
const defaultTracks: Track[] = [ { id: "1", title: "Midnight Drive", artist: "Neon Skyline", duration: "3:42" }, { id: "2", title: "Paper Planes", artist: "The Cartographers", duration: "4:05" }, { id: "3", title: "Low Tide", artist: "Harbor Lights", duration: "2:58" }, { id: "4", title: "Static Bloom", artist: "Velvet Antenna", duration: "3:21" }, { id: "5", title: "Glass Orchard", artist: "Fern & Field", duration: "4:37" },];
export default function SortableDemo() { const [tracks, setTracks] = createSignal<Track[]>(defaultTracks);
return ( <Sortable value={tracks()} onValueChange={setTracks} getItemValue={(track) => track.id} class="w-full max-w-md space-y-2" > <For each={tracks()}> {(track, index) => ( <SortableItem value={track.id}> <div class="flex items-center gap-3 rounded-md border bg-background p-3 transition-colors hover:bg-accent/50"> <SortableItemHandle class="text-muted-foreground hover:text-foreground"> <GripVertical class="size-4" /> </SortableItemHandle> <Badge variant="secondary" class="w-6 justify-center tabular-nums"> {index() + 1} </Badge> <div class="min-w-0 flex-1"> <p class="truncate font-medium text-sm">{track.title}</p> <p class="truncate text-muted-foreground text-xs">{track.artist}</p> </div> <span class="text-muted-foreground text-xs tabular-nums">{track.duration}</span> </div> </SortableItem> )} </For> </Sortable> );}Sortable reorders a list or grid of items with pointer, touch, and keyboard input. It is built on @dnd-kit/solid: the root wraps your items in a DragDropProvider, each SortableItem registers itself with the sortable manager, and you keep full ownership of the item markup and the array state.
Installation
Usage
import { For } from "solid-js";import { Sortable, SortableItem, SortableItemHandle, SortableOverlay,} from "~/components/blocks/sortable";<Sortable value={items()} onValueChange={setItems} getItemValue={(item) => item.id}> <For each={items()}> {(item) => ( <SortableItem value={item.id}> <SortableItemHandle> <GripVertical /> </SortableItemHandle> {item.content} </SortableItem> )} </For></Sortable>The root renders a plain container, so the layout is yours: stack items with space-y-* for a vertical list or lay them out with grid classes. @dnd-kit/solid detects the layout automatically.
Examples
Grid
Arrange items with grid classes on the root; dragging reorders across columns and rows. Here the handle only appears while hovering an item.
Hero Image
imageDemo Video
videoVoice Over
audioSpec Sheet
documentGallery 1
imageGallery 2
imageUser Manual
documentTeaser Clip
videoSoundtrack
audioimport { GripVertical } from "lucide-solid";import { createSignal, For } from "solid-js";import { Sortable, SortableItem, SortableItemHandle } from "@/registry/kobalte/blocks/sortable";import { Badge } from "~/components/ui/badge";
type Asset = { id: string; title: string; kind: "image" | "video" | "audio" | "document";};
const defaultAssets: Asset[] = [ { id: "1", title: "Hero Image", kind: "image" }, { id: "2", title: "Demo Video", kind: "video" }, { id: "3", title: "Voice Over", kind: "audio" }, { id: "4", title: "Spec Sheet", kind: "document" }, { id: "5", title: "Gallery 1", kind: "image" }, { id: "6", title: "Gallery 2", kind: "image" }, { id: "7", title: "User Manual", kind: "document" }, { id: "8", title: "Teaser Clip", kind: "video" }, { id: "9", title: "Soundtrack", kind: "audio" },];
const kindVariant: Record<Asset["kind"], "default" | "secondary" | "outline" | "destructive"> = { image: "default", video: "outline", audio: "destructive", document: "secondary",};
export default function SortableGrid() { const [assets, setAssets] = createSignal<Asset[]>(defaultAssets);
return ( <Sortable value={assets()} onValueChange={setAssets} getItemValue={(asset) => asset.id} class="grid w-full max-w-lg grid-cols-3 gap-3" > <For each={assets()}> {(asset) => ( <SortableItem value={asset.id}> <div class="group relative flex min-h-24 flex-col justify-between rounded-md border bg-background p-3 transition-colors hover:bg-accent/50"> <SortableItemHandle class="absolute inset-e-1.5 top-2.5 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100"> <GripVertical class="size-3.5" /> </SortableItemHandle> <p class="truncate pe-5 font-medium text-sm">{asset.title}</p> <Badge variant={kindVariant[asset.kind]} class="w-fit"> {asset.kind} </Badge> </div> </SortableItem> )} </For> </Sortable> );}Drag Overlay
SortableOverlay renders a floating preview that follows the cursor while dragging. Pass a render function to receive the active item id, and render a SortableItem inside it to reuse your item markup; overlay items skip registration and render presentational markup only.
Lint
Static analysis and formatting checks
Build
Compile and bundle the application
Test
Unit and integration test suites
Deploy
Ship the release to production
import { GripVertical } from "lucide-solid";import { createMemo, createSignal, For, Show } from "solid-js";import { Sortable, SortableItem, SortableItemHandle, SortableOverlay,} from "@/registry/kobalte/blocks/sortable";
type Step = { id: string; name: string; description: string;};
const defaultSteps: Step[] = [ { id: "1", name: "Lint", description: "Static analysis and formatting checks" }, { id: "2", name: "Build", description: "Compile and bundle the application" }, { id: "3", name: "Test", description: "Unit and integration test suites" }, { id: "4", name: "Deploy", description: "Ship the release to production" },];
function StepRow(props: { step: Step }) { return ( <div class="flex items-center gap-3 rounded-md border bg-background p-3"> <SortableItemHandle class="text-muted-foreground hover:text-foreground"> <GripVertical class="size-4" /> </SortableItemHandle> <div class="min-w-0 flex-1"> <p class="truncate font-medium text-sm">{props.step.name}</p> <p class="truncate text-muted-foreground text-xs">{props.step.description}</p> </div> </div> );}
export default function SortableOverlayDemo() { const [steps, setSteps] = createSignal<Step[]>(defaultSteps);
return ( <Sortable value={steps()} onValueChange={setSteps} getItemValue={(step) => step.id} class="w-full max-w-md space-y-2" > <For each={steps()}> {(step) => ( <SortableItem value={step.id}> <StepRow step={step} /> </SortableItem> )} </For> <SortableOverlay> {(params: { value: string }) => { const active = createMemo(() => steps().find((step) => step.id === params.value)); return ( <Show when={active()}> {(step) => ( <SortableItem value={step().id} class="shadow-lg"> <StepRow step={step()} /> </SortableItem> )} </Show> ); }} </SortableOverlay> </Sortable> );}Persisting Order
onValueChange fires once per drop with the reordered array. To persist the order to a backend with rollback, use onValueCommit: it fires after onValueChange with the new order and a previousValue snapshot you can restore when the mutation fails. Every other drop below fails on purpose.
Introduction
Setting Up
Core Concepts
Going Further
import { GripVertical } from "lucide-solid";import { createSignal, For, Show } from "solid-js";import { Sortable, SortableItem, SortableItemHandle } from "@/registry/kobalte/blocks/sortable";import { Badge } from "~/components/ui/badge";
type Lesson = { id: string; title: string;};
const defaultLessons: Lesson[] = [ { id: "1", title: "Introduction" }, { id: "2", title: "Setting Up" }, { id: "3", title: "Core Concepts" }, { id: "4", title: "Going Further" },];
// Simulated backend call that fails on every other attempt.let attempt = 0;function saveOrder(_order: string[]) { attempt += 1; const shouldFail = attempt % 2 === 0; return new Promise<void>((resolve, reject) => { setTimeout(() => (shouldFail ? reject(new Error("Network error")) : resolve()), 600); });}
export default function SortablePersistence() { const [lessons, setLessons] = createSignal<Lesson[]>(defaultLessons); const [status, setStatus] = createSignal<"idle" | "saving" | "saved" | "failed">("idle");
return ( <div class="w-full max-w-md space-y-3"> <Sortable value={lessons()} onValueChange={setLessons} getItemValue={(lesson) => lesson.id} onValueCommit={(value, meta) => { setStatus("saving"); saveOrder(value.map((lesson) => lesson.id)) .then(() => setStatus("saved")) .catch(() => { // Roll back the optimistic update on failure. setLessons(meta.previousValue); setStatus("failed"); }); }} class="space-y-2" > <For each={lessons()}> {(lesson, index) => ( <SortableItem value={lesson.id}> <div class="flex items-center gap-3 rounded-md border bg-background p-3"> <SortableItemHandle class="text-muted-foreground hover:text-foreground"> <GripVertical class="size-4" /> </SortableItemHandle> <span class="text-muted-foreground text-xs tabular-nums"> {String(index() + 1).padStart(2, "0")} </span> <p class="min-w-0 flex-1 truncate font-medium text-sm">{lesson.title}</p> </div> </SortableItem> )} </For> </Sortable> <div class="flex h-6 items-center" aria-live="polite"> <Show when={status() === "saving"}> <Badge variant="secondary">Saving order…</Badge> </Show> <Show when={status() === "saved"}> <Badge>Order saved</Badge> </Show> <Show when={status() === "failed"}> <Badge variant="destructive">Save failed — order restored</Badge> </Show> </div> </div> );}Accessibility
- Items are focusable and keyboard sortable via the
KeyboardSensor: pick an item up with Space or Enter, move it with the arrow keys, drop it with Space or Enter, and cancel with Esc. - The
PointerSensorcovers both mouse and touch input. - When a
SortableItemHandleis present, only the handle initiates a drag, so interactive controls inside the item (switches, buttons, links) stay usable.
Server-Side Rendering
@dnd-kit/solid is browser-only, so Sortable renders purely presentational markup during SSR and hydration, then engages drag and drop one tick after mount. The server HTML is visually identical, but drag handles are not focusable and carry no drag-related ARIA attributes until hydration completes — keep this in mind when asserting on SSR output or querying for tabindex/aria-* attributes in tests that don't run a browser.
API Reference
Sortable
The root component. Manages the sortable state and wraps a list of <SortableItem /> children inside a DragDropProvider from @dnd-kit/solid.
The root exposes data-slot="sortable" and sets data-dragging while a drag is active.
SortableItem
An individual draggable item. Registers itself with the sortable manager via useSortable. When rendered inside <SortableOverlay />, registration is skipped and only presentational markup is rendered.
Items expose data-slot="sortable-item", data-value, and set data-dragging / data-disabled when applicable.
SortableItemHandle
The optional drag handle for an item. When present, only the handle triggers a drag instead of the whole item.
The handle exposes data-slot="sortable-item-handle" and mirrors data-dragging / data-disabled from its item.
SortableOverlay
Renders the floating preview that follows the cursor while dragging. Pass either static JSX (rendered whenever a drag is active) or a render function ({ value }) => JSX to render content based on the active item id. DragOverlay self-portals — no <Portal> wrapper is needed.