ZaidanHomeDocsComponentsBlocksChartsTypesetCreate

Command Palette

Search for a command to run...

GitHub
New
Sections
  • Introduction
  • Components
  • Blocks
  • Installation
  • Customization
  • Dark Mode
  • Typeset
  • Zaidan Skills
  • FAQ
  • Roadmap
  • Changelog
Installation
  • Astro
  • Manual
  • Solid Start
  • TanStack Router
  • TanStack Start
  • Vite
Blocks
  • Data Grid
  • Event Calendar
  • Filters
  • Gantt
  • Image Crop
  • Kanban
  • Message Scroller
  • Questionnaire
  • Sortable
Components
  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Marker
  • Menubar
  • Message
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip

Sortable

1

Midnight Drive

Neon Skyline

3:42
2

Paper Planes

The Cartographers

4:05
3

Low Tide

Harbor Lights

2:58
4

Static Bloom

Velvet Antenna

3:21
5

Glass Orchard

Fern & Field

4:37
1
import { GripVertical } from "lucide-solid";
2
import { createSignal, For } from "solid-js";
3
import { Sortable, SortableItem, SortableItemHandle } from "@/registry/kobalte/blocks/sortable";
4
import { Badge } from "~/components/ui/badge";
5
6
type Track = {
7
id: string;
8
title: string;
9
artist: string;
10
duration: string;
11
};
12
13
const defaultTracks: Track[] = [
14
{ id: "1", title: "Midnight Drive", artist: "Neon Skyline", duration: "3:42" },
15
{ id: "2", title: "Paper Planes", artist: "The Cartographers", duration: "4:05" },
16
{ id: "3", title: "Low Tide", artist: "Harbor Lights", duration: "2:58" },
17
{ id: "4", title: "Static Bloom", artist: "Velvet Antenna", duration: "3:21" },
18
{ id: "5", title: "Glass Orchard", artist: "Fern & Field", duration: "4:37" },
19
];
20
21
export default function SortableDemo() {
22
const [tracks, setTracks] = createSignal<Track[]>(defaultTracks);
23
24
return (
25
<Sortable
26
value={tracks()}
27
onValueChange={setTracks}
28
getItemValue={(track) => track.id}
29
class="w-full max-w-md space-y-2"
30
>
31
<For each={tracks()}>
32
{(track, index) => (
33
<SortableItem value={track.id}>
34
<div class="flex items-center gap-3 rounded-md border bg-background p-3 transition-colors hover:bg-accent/50">
35
<SortableItemHandle class="text-muted-foreground hover:text-foreground">
36
<GripVertical class="size-4" />
37
</SortableItemHandle>
38
<Badge variant="secondary" class="w-6 justify-center tabular-nums">
39
{index() + 1}
40
</Badge>
41
<div class="min-w-0 flex-1">
42
<p class="truncate font-medium text-sm">{track.title}</p>
43
<p class="truncate text-muted-foreground text-xs">{track.artist}</p>
44
</div>
45
<span class="text-muted-foreground text-xs tabular-nums">{track.duration}</span>
46
</div>
47
</SortableItem>
48
)}
49
</For>
50
</Sortable>
51
);
52
}

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

pnpm dlx shadcn@latest add @zaidan/sortable
npx shadcn@latest add @zaidan/sortable
yarn dlx shadcn@latest add @zaidan/sortable
bunx --bun shadcn@latest add @zaidan/sortable

Usage

1
import { For } from "solid-js";
2
import {
3
Sortable,
4
SortableItem,
5
SortableItemHandle,
6
SortableOverlay,
7
} from "~/components/blocks/sortable";
1
<Sortable value={items()} onValueChange={setItems} getItemValue={(item) => item.id}>
2
<For each={items()}>
3
{(item) => (
4
<SortableItem value={item.id}>
5
<SortableItemHandle>
6
<GripVertical />
7
</SortableItemHandle>
8
{item.content}
9
</SortableItem>
10
)}
11
</For>
12
</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

image

Demo Video

video

Voice Over

audio

Spec Sheet

document

Gallery 1

image

Gallery 2

image

User Manual

document

Teaser Clip

video

Soundtrack

audio
1
import { GripVertical } from "lucide-solid";
2
import { createSignal, For } from "solid-js";
3
import { Sortable, SortableItem, SortableItemHandle } from "@/registry/kobalte/blocks/sortable";
4
import { Badge } from "~/components/ui/badge";
5
6
type Asset = {
7
id: string;
8
title: string;
9
kind: "image" | "video" | "audio" | "document";
10
};
11
12
const defaultAssets: Asset[] = [
13
{ id: "1", title: "Hero Image", kind: "image" },
14
{ id: "2", title: "Demo Video", kind: "video" },
15
{ id: "3", title: "Voice Over", kind: "audio" },
16
{ id: "4", title: "Spec Sheet", kind: "document" },
17
{ id: "5", title: "Gallery 1", kind: "image" },
18
{ id: "6", title: "Gallery 2", kind: "image" },
19
{ id: "7", title: "User Manual", kind: "document" },
20
{ id: "8", title: "Teaser Clip", kind: "video" },
21
{ id: "9", title: "Soundtrack", kind: "audio" },
22
];
23
24
const kindVariant: Record<Asset["kind"], "default" | "secondary" | "outline" | "destructive"> = {
25
image: "default",
26
video: "outline",
27
audio: "destructive",
28
document: "secondary",
29
};
30
31
export default function SortableGrid() {
32
const [assets, setAssets] = createSignal<Asset[]>(defaultAssets);
33
34
return (
35
<Sortable
36
value={assets()}
37
onValueChange={setAssets}
38
getItemValue={(asset) => asset.id}
39
class="grid w-full max-w-lg grid-cols-3 gap-3"
40
>
41
<For each={assets()}>
42
{(asset) => (
43
<SortableItem value={asset.id}>
44
<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">
45
<SortableItemHandle class="absolute inset-e-1.5 top-2.5 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100">
46
<GripVertical class="size-3.5" />
47
</SortableItemHandle>
48
<p class="truncate pe-5 font-medium text-sm">{asset.title}</p>
49
<Badge variant={kindVariant[asset.kind]} class="w-fit">
50
{asset.kind}
51
</Badge>
52
</div>
53
</SortableItem>
54
)}
55
</For>
56
</Sortable>
57
);
58
}

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

1
import { GripVertical } from "lucide-solid";
2
import { createMemo, createSignal, For, Show } from "solid-js";
3
import {
4
Sortable,
5
SortableItem,
6
SortableItemHandle,
7
SortableOverlay,
8
} from "@/registry/kobalte/blocks/sortable";
9
10
type Step = {
11
id: string;
12
name: string;
13
description: string;
14
};
15
16
const defaultSteps: Step[] = [
17
{ id: "1", name: "Lint", description: "Static analysis and formatting checks" },
18
{ id: "2", name: "Build", description: "Compile and bundle the application" },
19
{ id: "3", name: "Test", description: "Unit and integration test suites" },
20
{ id: "4", name: "Deploy", description: "Ship the release to production" },
21
];
22
23
function StepRow(props: { step: Step }) {
24
return (
25
<div class="flex items-center gap-3 rounded-md border bg-background p-3">
26
<SortableItemHandle class="text-muted-foreground hover:text-foreground">
27
<GripVertical class="size-4" />
28
</SortableItemHandle>
29
<div class="min-w-0 flex-1">
30
<p class="truncate font-medium text-sm">{props.step.name}</p>
31
<p class="truncate text-muted-foreground text-xs">{props.step.description}</p>
32
</div>
33
</div>
34
);
35
}
36
37
export default function SortableOverlayDemo() {
38
const [steps, setSteps] = createSignal<Step[]>(defaultSteps);
39
40
return (
41
<Sortable
42
value={steps()}
43
onValueChange={setSteps}
44
getItemValue={(step) => step.id}
45
class="w-full max-w-md space-y-2"
46
>
47
<For each={steps()}>
48
{(step) => (
49
<SortableItem value={step.id}>
50
<StepRow step={step} />
51
</SortableItem>
52
)}
53
</For>
54
<SortableOverlay>
55
{(params: { value: string }) => {
56
const active = createMemo(() => steps().find((step) => step.id === params.value));
57
return (
58
<Show when={active()}>
59
{(step) => (
60
<SortableItem value={step().id} class="shadow-lg">
61
<StepRow step={step()} />
62
</SortableItem>
63
)}
64
</Show>
65
);
66
}}
67
</SortableOverlay>
68
</Sortable>
69
);
70
}

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.

01

Introduction

02

Setting Up

03

Core Concepts

04

Going Further

1
import { GripVertical } from "lucide-solid";
2
import { createSignal, For, Show } from "solid-js";
3
import { Sortable, SortableItem, SortableItemHandle } from "@/registry/kobalte/blocks/sortable";
4
import { Badge } from "~/components/ui/badge";
5
6
type Lesson = {
7
id: string;
8
title: string;
9
};
10
11
const defaultLessons: Lesson[] = [
12
{ id: "1", title: "Introduction" },
13
{ id: "2", title: "Setting Up" },
14
{ id: "3", title: "Core Concepts" },
15
{ id: "4", title: "Going Further" },
16
];
17
18
// Simulated backend call that fails on every other attempt.
19
let attempt = 0;
20
function saveOrder(_order: string[]) {
21
attempt += 1;
22
const shouldFail = attempt % 2 === 0;
23
return new Promise<void>((resolve, reject) => {
24
setTimeout(() => (shouldFail ? reject(new Error("Network error")) : resolve()), 600);
25
});
26
}
27
28
export default function SortablePersistence() {
29
const [lessons, setLessons] = createSignal<Lesson[]>(defaultLessons);
30
const [status, setStatus] = createSignal<"idle" | "saving" | "saved" | "failed">("idle");
31
32
return (
33
<div class="w-full max-w-md space-y-3">
34
<Sortable
35
value={lessons()}
36
onValueChange={setLessons}
37
getItemValue={(lesson) => lesson.id}
38
onValueCommit={(value, meta) => {
39
setStatus("saving");
40
saveOrder(value.map((lesson) => lesson.id))
41
.then(() => setStatus("saved"))
42
.catch(() => {
43
// Roll back the optimistic update on failure.
44
setLessons(meta.previousValue);
45
setStatus("failed");
46
});
47
}}
48
class="space-y-2"
49
>
50
<For each={lessons()}>
51
{(lesson, index) => (
52
<SortableItem value={lesson.id}>
53
<div class="flex items-center gap-3 rounded-md border bg-background p-3">
54
<SortableItemHandle class="text-muted-foreground hover:text-foreground">
55
<GripVertical class="size-4" />
56
</SortableItemHandle>
57
<span class="text-muted-foreground text-xs tabular-nums">
58
{String(index() + 1).padStart(2, "0")}
59
</span>
60
<p class="min-w-0 flex-1 truncate font-medium text-sm">{lesson.title}</p>
61
</div>
62
</SortableItem>
63
)}
64
</For>
65
</Sortable>
66
<div class="flex h-6 items-center" aria-live="polite">
67
<Show when={status() === "saving"}>
68
<Badge variant="secondary">Saving order…</Badge>
69
</Show>
70
<Show when={status() === "saved"}>
71
<Badge>Order saved</Badge>
72
</Show>
73
<Show when={status() === "failed"}>
74
<Badge variant="destructive">Save failed — order restored</Badge>
75
</Show>
76
</div>
77
</div>
78
);
79
}

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 PointerSensor covers both mouse and touch input.
  • When a SortableItemHandle is 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.

PropTypeDefaultDescription
valueT[]-Required. The array of items to sort.
onValueChange(value: T[]) => void-Required. Fired once per drop with the reordered array. Not called when onMove is provided.
getItemValue(item: T) => string-Required. Extracts a unique string id from an item.
onValueCommit(value: T[], meta: { previousValue: T[] }) => void-Fired after onValueChange with the new order and a previousValue snapshot for rollback. Not called when onMove handles the reorder.
onMove(event: { activeIndex: number; overIndex: number; activeId: string; overId: string }) => void-Optional handler for custom reorder logic. When provided, onValueChange is not called automatically.
onDragStart(event: { activeId: string }) => void-Fired when a drag operation begins.
onDragEnd(event: { activeId: string; overId: string | null; canceled: boolean }) => void-Fired when a drag operation ends, including canceled drops.
modifiersunknown[]-Modifiers forwarded to DragDropProvider (and DragOverlay via internal context).
strategy"horizontal" | "vertical" | "grid"-Deprecated no-op: @dnd-kit/solid auto-detects layout. Kept for forward compatibility only.
asValidComponent"div"The element or component to render as.
classstring-Additional CSS classes for the container.

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.

PropTypeDefaultDescription
valuestring-Required. The unique identifier for the item.
disabledbooleanfalseWhether the item is disabled (not draggable).
asValidComponent"div"The element or component to render as.
classstring-Additional CSS classes for the item.

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.

PropTypeDefaultDescription
cursorbooleantrueWhether to apply cursor-grab / cursor-grabbing styles to the handle automatically.
asValidComponent"div"The element or component to render as.
classstring-Additional CSS classes for the handle.

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.

PropTypeDefaultDescription
childrenJSX.Element | ((params: { value: string }) => JSX.Element)-Static JSX or a render function that receives the active item id.
dropAnimation{ duration: number; easing: string } | null{ duration: 250, easing: "cubic-bezier(0.18, 0.67, 0.6, 1.22)" }Drop animation config. Pass null to disable.
classstring-Additional CSS classes for the overlay container.

On This Page

  • Installation
  • Usage
  • Examples
    • Grid
    • Drag Overlay
    • Persisting Order
  • Accessibility
  • Server-Side Rendering
  • API Reference
    • Sortable
    • SortableItem
    • SortableItemHandle
    • SortableOverlay
Built by Kevin Abatan. The source code is available on GitHub.