# Sortable
A single list that allows users to reorder items through drag-and-drop interactions.
## Usage
Use standalone, or inside `SortableGroup` to connect lists.
```svelte
```
## Anatomy
The `class` attribute values represent part names. Other attributes represent their corresponding props.
```svelte
{#each items as value (key(value))}
-
{item(value, variant, handle)}
{/each}
```
A `position: fixed` floating shadow is rendered while dragging.
The `` items use roving `tabindex` and carry `aria-roledescription`; a visually hidden `aria-live="polite"` region announces keyboard drag actions.
## Props
| Name | Type | Default | Description |
|---|---|---|---|
| `items*` | `T[]` | - | Bindable plain array; mutations happen in place through `bind:items` |
| `item*` | `Snippet` | - | Item renderer; receives the value, variant, and drag handle |
| `key` | `(item: T) => PropertyKey` | - | Identity extractor; required for object items |
| `clone` | `(item: T) => T` | - | Clone factory; required for object items in clone mode |
| `group` | `SortableGroupController` | - | Shared controller from `createSortableGroup(...)` for connected lists |
| `id` | `string` | - | List id used by `accept` |
| `ghost` | `Snippet` | - | Custom floating preview rendered while dragging |
| `mode` | `` | `"move"` | `move` moves items, `clone` copies to the target, and `swap` swaps with the target item |
| `accept` | `string[]` | - | Accepted source list ids; omitted accepts any list in the group, `[]` accepts none |
| `sort` | `boolean` | `true` | Enables sorting within the same list |
| `multiple` | `boolean` | `false` | Enables click selection; dragging a selected item carries the other selected items as followers |
| `draggable` | `boolean` | `true` | Allows the whole item to start a drag when no custom handle is attached |
| `appendable` | `boolean` | `false` | Allows items to append when entering the list area, including empty lists |
| `confirm` | `boolean` | `false` | Adds a hover delay before committing a move |
| `dragging` | `boolean` | - | Bindable group drag activity |
| `messages` | `Partial` | - | Live-region announcement formatters for keyboard actions; ignored when a `group` controller supplies its own |
| `ariaLabel` | `string` | - | Accessible label for the list |
| `ariaRoleDescription` | `string` | `"Sortable Item"` | `aria-roledescription` applied to each item |
| `styling` | `SVSClass` | - | Styling override |
| `variant` | `SVSVariant` | `NEUTRAL` | Base variant for items |
`*` required. Other `` attributes are passed via rest props; `class` is merged onto the list.
Group motion defaults to `300ms` / `cubicOut`; reduced motion resolves the duration to `0`.
## Styling
To learn more, see [Styling](/docs/styling).
### Variant Management
The item `variant` becomes `active` while an item is selected or dragged.
### Default Class Name
`svs-sortable`
## Accessibility
Name the list with `ariaLabel`, or pass `aria-labelledby` through rest props.
### Keyboard Interactions
| Key | Description |
| --- | --- |
| `ArrowUp` / `ArrowDown` | Moves focus to the previous or next item; while an item is picked up, moves the picked-up item |
| `Space` / `Enter` | Picks up the focused item, or drops the picked-up item |
| `Escape` | Cancels the pick-up and returns the item to its original position |
| `Ctrl` + `Space` | Toggles selection of the focused item (when `multiple` is true) |
### ARIA Roles and Attributes
The component applies these automatically:
- List (`whole` ``): `aria-label` from `ariaLabel`.
- Item (`main` `- `): roving `tabindex` (`0` for the focused or active item, `-1` otherwise) and `aria-roledescription` from `ariaRoleDescription`.
- A visually hidden `aria-live="polite"` region announces keyboard pick-up, move, drop, cancel, and selection actions. Customize the wording with `messages` (or the `messages` argument of `createSortableGroup` for connected lists).
## Exports
### Types
```ts
interface SortableProps extends Omit, "children" | "aria-label" | "onpointerdown" | "onpointerover" | "onpointerout" | "onpointerenter" | "onpointerup" | "onkeydown"> {
items: T[]; // bindable plain array
item: Snippet<[T, SVSVariant, Attachment]>; // Snippet<[value, variant, dragHandle]>
key?: (item: T) => PropertyKey; // identity extractor; required for object items
clone?: (item: T) => T; // required for object items in clone mode
group?: SortableGroupController; // shared controller from createSortableGroup()
id?: string; // list id used by accept
ghost?: Snippet<[T]>; // custom floating preview
mode?: "move" | "clone" | "swap"; // ("move")
accept?: string[] | ((fromId: string) => boolean); // undefined=any in group, []=none
sort?: boolean; // enable sorting within the same list (true)
multiple?: boolean; // enable multi-select followers (false)
draggable?: boolean; // whole item is draggable when no handle is attached (true)
appendable?: boolean; // append when entering the list area (false)
confirm?: boolean; // hover delay before committing (false)
dragging?: boolean; // bindable group drag activity
messages?: Partial; // live-region announcement formatters
ariaLabel?: string;
ariaRoleDescription?: string; // ("Sortable Item")
styling?: SVSClass;
variant?: SVSVariant; // (VARIANT.NEUTRAL)
// other ul attributes are passed to
via ...rest; `class` is merged onto the list
// ariaLabel is the recommended accessible-name prop; aria-labelledby may be passed via ...rest
}
type SortableReqdProps = "items" | "item";
type SortableBindProps = "items" | "dragging";
type SortableMode = "move" | "clone" | "swap";
interface SortableGroupController extends SVSContext {
readonly identity: symbol;
readonly dragging: boolean;
readonly activeKey: string;
readonly confirmKey: string;
readonly keyboardActive: boolean;
readonly message: string;
readonly messageOwnerId: string;
readonly messages: SortableMessages;
readonly shadow: SortableShadowState;
readonly send: TransitionFn;
readonly receive: TransitionFn;
readonly tp: TransitionParams;
register(member: SortableMember): () => void;
prepare(member: SortableMember, key: string, ev: PointerEvent, el: HTMLElement, followers: string[]): boolean;
setGhost(ghost: boolean, ev?: PointerEvent): void;
move(ev: PointerEvent): void;
commit(): void;
cancel(): void;
grabKeyboard(member: SortableMember, key: string, followers: string[]): boolean;
stepKeyboard(dir: -1 | 1): string;
dropKeyboard(): void;
cancelKeyboard(): void;
announce(message: string, ownerId: string): void;
over(member: SortableMember, key: string): void;
leave(): void;
enterGroup(member: SortableMember): void;
itemVariant(key: string, selected: boolean, base: SVSVariant): SVSVariant;
}
interface SortableMessages {
grabbed(key: string, index: number, total: number): string;
atPosition(index: number, total: number): string;
moved(index: number, total: number): string;
dropped(index: number, total: number): string;
cancelled(index: number, total: number): string;
selected(key: string, count: number): string;
deselected(key: string, count: number): string;
}
```
### Others
```ts
function createSortableGroup(
presentation?: {
get variant(): SVSVariant;
get styling(): SVSClass | undefined;
},
motion?: {
duration?: number;
easing?: EasingFunction;
},
messages?: Partial,
): SortableGroupController;
```
```svelte
{#snippet item(value)}
{value}
{/snippet}
{#snippet item(value)}
{value}
{/snippet}
```
## Examples
The example code uses Tailwind CSS along with `SVSClass`.
### Basic
**Code**
```svelte src=SortableBasic.svelte
{#snippet item(value)}
{value}
{/snippet}
```
### With Interactive Variant
**Code**
```svelte src=SortablePract.svelte
{#snippet item(name)}
{@render inner(name)}
{/snippet}
{#snippet inner(name: string)}
{fruits.get(name)?.img}
{name}
{/snippet}
```
### With Drag Handle
Only the attached handle starts a drag when `draggable={false}`.
**Code**
```svelte src=SortableHandle.svelte
{#snippet item(value, variant, handle)}
::
{value.label}
{/snippet}
```
### Multiple Selection
Clicking items selects them, and dragging one selected item moves the selection together.
**Code**
```svelte src=SortableMultiple.svelte
Click items to select them, then drag one selected item to move the selection together.
{#snippet item(value)}
{value.label}
{/snippet}
```