Skip to main content
Vuetify0 v1.0 is here
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

createSortable

Headless ordered-list primitive that owns a registry of value-bearing tickets and exposes move, swap, and reorder mutations. Pure logic — no DnD, no keyboard, no DOM — so consumers can drive it from any input modality.

Edit this page
Report a Bug
Open issues
View on GitHub
Copy Markdown

PreviewIntermediateAug 7, 2026

Usage

createSortable extends createModel with mutation primitives over the canonical order. Drag-and-drop wiring composes with useDragDrop; keyboard reorder composes with useVirtualFocus. Consumers can drive sortable from buttons, gestures, server reconciliation, or undo/redo by calling its mutation methods.

ts
import { createSortable } from '@vuetify/v0'

import type { SortableTicketInput } from '@vuetify/v0'

interface Task {
  id: number
  label: string
}

interface TaskTicket extends SortableTicketInput {
  value: Task
}

const sortable = createSortable<TaskTicket>()

const [a, b, c] = sortable.onboard([
  { value: { id: 1, label: 'Cut alpha' } },
  { value: { id: 2, label: 'Ship docs' } },
  { value: { id: 3, label: 'Tweet' } },
])

sortable.move(a.id, 2)
sortable.swap(a.id, b.id)
sortable.reorder([b.id, a.id, c.id])

Architecture

createSortable extends createModel, which extends createRegistry. All methods inherited from those layers are available unchanged, except on and off — those are extended with typed overloads for the move:ticket event. See createModel and createRegistry for the full inherited surface.

Sortable Hierarchy

Use controls to zoom and pan. Click outside or press Escape to close.

Sortable Hierarchy

The composable adds four things on top of createModel:

AdditionLayerPurpose
move overridesortableWraps registry.move to emit move:ticket with { ticket, from, to }
swap(a, b)sortableTwo batched move calls; emits two move:ticket events
reorder(ids)sortableStrict permutation set; logs a warning and no-ops on length mismatch, unknown id, or duplicate id
Typed on / offsortableOverloads narrow move:ticket callback payload to SortableMovePayload<E>
Tip

The composable always enables events: true on the underlying registry, so move:ticket works out of the box and useProxyRegistry snapshots track moves without extra configuration. Consumer-supplied events: false is overridden — sortable’s move:ticket contract requires events to be on.

Reactivity

createSortable’s surface is mostly imperative — move, swap, and reorder mutate the registry and the reactive updates flow downstream through registry events. The composable bakes in events: true so move:ticket and the standard registry events fire without extra setup.

Property/MethodReactiveNotes
sizeGetter — tracks registry count via useProxyRegistry or reactive: true
disabled (option)MaybeRefOrGetter<boolean> — flipping it re-enables move / swap / reorder
move:ticket eventSubscribe via on(); payload is SortableMovePayload<E> with { ticket, from, to }
move(id, toIndex)-Imperative; returns the moved ticket or undefined when gated
swap(a, b)-Imperative; emits move:ticket twice in a batch
reorder(ids)-Imperative; logs a warning and no-ops on size/unknown/duplicate violations
Tip

Reactive iteration useProxyRegistry(sortable) returns a reactive { keys, values, entries, size } snapshot driven by registry events. Templates that iterate it stay in sync with move, swap, and reorder automatically. See Reactive snapshot for templates.

Examples

Button and keyboard reorder

A playlist queue reordered two ways from the same state: per-row up and down buttons, and arrow-key presses while a row is focused. Both paths funnel into sortable.move(id, index) — the entire mutation surface for this example. The composable owns the order and the move semantics; PlaylistQueue.vue is pure presentation that calls back into it, so the same logic drives clicks and keystrokes without duplication.

The template iterates proxy.values from useProxyRegistry(sortable), a reactive snapshot that re-renders whenever the registry order changes — no manual watch, no second copy of the list. Buttons disable at the edges via :disabled="ticket.index === 0" and :disabled="ticket.index === proxy.size - 1", and moveUp / moveDown repeat the bounds check so arrow keys no-op past the ends. Subscribing to the typed move:ticket event drives the live status line below the list, and because TransitionGroup reuses the keyed DOM nodes, focus rides along with a row as it moves — press the arrow key again to keep nudging it. The .list-move transition on transform animates the slide.

Reach for this pattern for any user-ordered list where drag is unnecessary: priority queues, column choosers, sidebar section ordering. It builds on createModel for the value store and useProxyRegistry for the reactive view. Upgrade to the drag-and-drop example below — pairing with useDragDrop — when pointer dragging is required.

FileRole
usePlaylist.tsOwns the sortable instance, track data, the proxy snapshot, the move:ticket status line, and bounds-checked moveUp / moveDown
PlaylistQueue.vueRenders the ordered rows with up/down buttons and translates arrow-key presses into move calls
playlist.vueWires the composable to the component and shows the live reorder status
  1. 1Midnight CityM83
  2. 2NightcallKavinsky
  3. 3ResonanceHome
  4. 4Strobedeadmau5
  5. 5FadedAlan Walker

Reorder the queue with the arrows or your keyboard.

Drag-and-drop reorder

The natural use case. Pair createSortable with useDragDrop: each item registers as a draggable, the list registers as a drop zone, and the zone’s onDrop callback maps the drop position to a sortable.move call. createSortable owns the order; useDragDrop owns the input modality.

The split is intentional — it keeps the two primitives independently testable and lets you swap the DnD layer (mouse, touch, keyboard) without touching the order state. DraggableItem.vue calls dnd.draggables.register per item; DnDSortable.vue calls dnd.zones.register on the container with an onDrop handler that calls sortable.move(drag.id, position.index ?? 0). The drop position is provided by useDragDropsortable never touches the pointer event.

FileRole
data.tsInitial item dataset
DraggableItem.vuePer-item draggable registration
DnDSortable.vueContainer with drop zone, indicator, and onDrop → sortable.move wiring
  • Cut alpha
  • Ship the docs
  • File the bug
  • Tweet about it
  • Review the PR
  • Update the changelog
  • Notify the Discord
  • Plan the next milestone

Recipes

Disabling reorder

disabled works at two scopes. Root (createSortable({ disabled })) no-ops move, swap, and reorder for the whole list. Per-ticket (register({ value, disabled: true })) no-ops move and swap for that ticket. reorder bypasses per-ticket disabled — it’s a bulk operation declaring the canonical order; if you want disabled tickets pinned, exclude their ids from the array. Registration is never gated.

ts
const sortable = createSortable<Todo>({
  disabled: toRef(() => isReadOnlyMode.value),
})

sortable.move(id, 0)   // no-op when disabled.value === true

Server-reconciled order

reorder accepts a strict permutation of currently-registered ids. Use it to apply an authoritative order from the backend without diffing positions yourself.

ts
const sortable = createSortable<Todo>()
const order = await fetchOrder()       // ID[] from backend
sortable.reorder(order)

Reactive snapshot for templates

useProxyRegistry returns a reactive { keys, values, entries, size } snapshot driven by registry events. Because createSortable bakes in events: true, you do not pass it explicitly.

ts
const sortable = createSortable<Todo>()
const proxy = useProxyRegistry(sortable)

// proxy.keys, proxy.values, proxy.entries, proxy.size all track moves

Pair with useDragDrop

Wire useDragDrop’s onDrop callback to sortable.move to translate pointer or keyboard drags into reorder mutations. The headless contract keeps the two primitives independent — sortable owns order, drag-drop owns input.

ts
const sortable = createSortable<Todo>()
const dnd = useDragDrop()

dnd.zones.register({
  el: containerEl,
  accept: ['todo'],
  onDrop: (drag, position) => {
    if (drag.type === 'todo') sortable.move(drag.id, position.index ?? 0)
  },
})

A first-class useSortableDnD adapter is on the roadmap; until then, wire useDragDrop’s callbacks to sortable.move directly.

FAQ

Discord
Need help? Join our community for support and discussions ↗

API Reference

The following API details are for the createSortable composable.
Was this page helpful?

© 2016-1970 Vuetify, LLC
Services
Ctrl+/