---
title: "SortableList"
description: "A list that reorders by drag or keyboard."
category: Data
status: beta
since: 0.3.4
tags: [sortable, drag-drop, list]
syncStatus: reviewed
verified: true
---

## Usage

### Basic
Every item needs a stable string `id`; the `item` slot's `dragging` flag marks the carried row.
:::TuffDemoWrapper{demo="SortableListSortableListDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const list = ref([
    { id: 'one', title: 'One' },
    { id: 'two', title: 'Two' },
    { id: 'three', title: 'Three' },
  ])
  </script>

  <template>
    <TxSortableList v-model="list">
      <template #item="{ item, dragging }">
        <div :class="['sortable-row', { 'sortable-row--dragging': dragging }]">
          {{ item.title }}
        </div>
      </template>
    </TxSortableList>
  </template>
---
:::

### Drag Modes
The default `pointer` mode carries the row while the others spring aside; use `dragMode="native"` when items must leave the list, as in board columns.

```vue
<template>
  <TxSortableList v-model="column.cards" drag-mode="native" />
</template>
```

### Drag Handle Mode
`handle` requires a drag to start inside `[data-tx-sort-handle="true"]`. Default rows include a grip; a custom slot spreads `handleAttrs` onto its own.

```vue
<template>
  <TxSortableList v-model="steps" handle @reorder="saveOrder">
    <template #item="{ item, handleAttrs }">
      <div>
        <button type="button" v-bind="handleAttrs" aria-label="Drag item">☰</button>
        <span>{{ item.title }}</span>
      </div>
    </template>
  </TxSortableList>
</template>
```

### Persisting Reorder

```vue
<script setup lang="ts">
const tasks = ref([{ id: 'draft' }, { id: 'review' }, { id: 'ship' }])

async function persistOrder({ items }: { items: Array<{ id: string }> }) {
  tasks.value = items
  await api.saveTaskOrder(items.map(item => item.id))
}
</script>

<template>
  <TxSortableList v-model="tasks" @reorder="persistOrder" />
</template>
```

### Best Practices

- Use stable persisted ids, never array indexes.
- Update the local array with `v-model`; persist from `reorder` when server order matters.
- Turn on `handle` for rows with buttons, links, inputs, or selectable text, and for long touch lists so rows don't capture swipes.
- Pass `itemLabel` when ids aren't human-readable, or screen readers announce ids like `plugin-a1b3`.
- Keep lists modest; for very long ones, pair a different drag strategy with virtualization.

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `modelValue` | `SortableListItem[]` | required | Ordered list data; every item needs a stable string `id`. |
| `disabled` | `boolean` | `false` | Disables dragging, keyboard reordering, and reorder emits. |
| `handle` | `boolean` | `false` | Requires a drag to start from `[data-tx-sort-handle="true"]`. |
| `dragMode` | `'pointer' \| 'native'` | `'pointer'` | `pointer` carries the row as others step aside; `native` uses HTML5 drag and drop, letting items leave. |
| `ariaLabel` | `string` | - | Accessible name of the list. |
| `itemLabel` | `(item) => string` | - | Item name in announcements; defaults to the `id`. |
| `labels` | `SortableListLabels` | - | Templates for `grabbed`, `moved`, `dropped`, `cancelled`, and the built-in `handle`, with `{item}`, `{position}`, `{size}`. |

### Events

| Event | Payload | Description |
|------|---------|-------------|
| `update:modelValue` | `SortableListItem[]` | Once on release for pointer drags; on every step for native drags and the keyboard. |
| `reorder` | `{ from: number, to: number, items: SortableListItem[] }` | Once when the interaction ends, with the start and end indexes. |

### Slots

| Slot | Props | Description |
|------|------|-------------|
| `item` | `{ item, dragging, grabbed, index, handleAttrs }` | Custom item renderer, defaulting to the `id`; spread `handleAttrs` onto the element that starts a drag. |

### SortableListItem

| Field | Type | Description |
|------|------|-------------|
| `id` | `string` | Stable identity for keys and drag state; other fields are preserved. |

## Overview

- The root is `role="list"` and each item `role="listitem"`.
- The component owns the preview, so rows move whether or not the host writes `modelValue` back; reorders emit a shallow copy that keeps item objects.
- A pointer drag starts after 4px of travel and swallows the release click; inputs and editable regions never start one; Escape or `pointercancel` cancels without emitting.
- A native drag reorders as `dragover` crosses rows, and a drop outside the list still counts.
- Keyboard: the list is one tab stop. Space or Enter picks a row up, arrows move it, and pressing again drops it; Escape restores the order, and blur drops it. A `role="status"` region announces each step.
- Under reduced motion nothing lifts and springs take no time; the carried row still follows the pointer.

## Technologies

- Travel and lift use the separate `translate` and `scale` properties so each keeps its own timing; `resolveTransition` compiles the springs to CSS `linear()` curves.
- Source: `packages/tuffex/packages/components/src/sortable-list/`.

<TuffDocSourceLink />
