Skip to main content
Vuetify0 is now in alpha!
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.

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.

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; throws 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

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.

API Reference

The following API details are for the createSortable composable.

Functions

createSortable

(_options?: SortableOptions) => SortableContext<Z, E>

Create an ordered-list state primitive.

Options

reactive

boolean | undefined

Enable reactive behavior for registry operations

Default: false

disabled

MaybeRefOrGetter<boolean> | undefined

Disabled state for the entire model instance

Default: false

enroll

MaybeRefOrGetter<boolean> | undefined

Auto-select tickets on registration

Default: true

multiple

MaybeRefOrGetter<boolean> | undefined

Allow multiple tickets to be selected simultaneously

Default: false

Properties

collection

ReadonlyMap<ID, E>

The collection of tickets in the registry

size

number

The number of tickets in the registry

selectedIds

Reactive<Set<ID>>

Set of currently selected ticket IDs

selectedItems

ComputedRef<Set<E>>

Computed Set of selected ticket instances

selectedValues

ComputedRef<Set<E["value"] extends Ref<infer U, infer U> ? U : E["value"]>>

Computed Set of selected ticket values

disabled

MaybeRefOrGetter<boolean>

Disabled state for the entire model instance

on

SortableEventListener<E>

Subscribe to the typed `move:ticket` event with a strongly-typed payload.

off

SortableEventListener<E>

Methods

clear

() => void

Clear the entire registry

has

(id: ID) => boolean

Check if a ticket exists by ID

keys

() => readonly ID[]

Get all registered IDs

browse

(value: E["value"]) => readonly ID[] | undefined

Browse for an ID(s) by value

lookup

(index: number) => ID | undefined

Lookup a ticket by index number

get

(id: ID) => E | undefined

Get a ticket by ID

upsert

(id: ID, ticket?: Partial<Z>, event?: string) => E

Update or insert a ticket by ID

values

() => readonly E[]

Get all values of registered tickets

entries

() => readonly [ID, E][]

Get all entries of registered tickets

unregister

(id: ID) => void

Unregister a ticket by ID

reindex

() => void

Reset the index directory and update all tickets

seek

(direction?: "first" | "last", from?: number, predicate?: (ticket) => boolean) => E | undefined

Seek for a ticket based on direction and optional predicate

emit

<K extends Extensible<RegistryEventName>>(event: K, data: EventPayload<E, K>) => void

Emit an event with data

dispose

() => void

Clears the registry and removes all listeners

onboard

(registrations: Partial<Z & RegistryTicket>[]) => E[]

Onboard multiple tickets at once

offboard

(ids: ID[]) => void

Offboard multiple tickets at once

batch

<R>(fn: () => R) => R

Execute operations in a batch, deferring cache invalidation and event emission until complete

reset

() => void

Reset selection state without destroying the registry

select

(id: ID) => void

Select a ticket by ID

unselect

(id: ID) => void

Unselect a ticket by ID

toggle

(id: ID) => void

Toggle a ticket's selection state

selected

(id: ID) => boolean

Check if a ticket is currently selected

apply

(values: unknown[], options?: { multiple?) => void

Apply external values to the model

register

(registration?: Partial<Z>) => E

Register a new ticket

move

(id: ID, toIndex: number) => E | undefined

Move a ticket to a target index. Other tickets shift to fill. Emits `move:ticket` once when the index actually changes — a move whose `toIndex` already equals the ticket's current index is a no-op. No-ops when the root sortable is disabled or when the ticket is disabled.

swap

(a: ID, b: ID) => void

Swap two tickets' positions. Emits `move:ticket` twice — once per ticket. No-ops when the root sortable is disabled or when either ticket is disabled.

reorder

(ids: ID[]) => void

Set the canonical order in one shot.

Was this page helpful?

Ctrl+/