Primitives Layout and display
Sortable
A list whose items are reordered by dragging a handle, by Alt with the arrow keys or by move buttons. The list stays a native list: its document order is the value, and hidden inputs in the items submit it with a form.
Markdown version of this page, for assistants (Markdown for AI)
On this page
Code
<script lang="ts">
// With @svelte-lean/vite the register import is injected for the static data-slean="sortable".
import '@svelte-lean/styles/sortable.css';
import '@svelte-lean/styles/button.css';
// The first render only: the behavior moves the item elements, hidden inputs included.
const COLUMNS = [
{ value: 'name', label: 'Name' },
{ value: 'email', label: 'Email' },
{ value: 'role', label: 'Role' },
{ value: 'team', label: 'Team' },
{ value: 'created', label: 'Created' }
];
let submitted = $state('');
function submit(event: SubmitEvent) {
event.preventDefault();
const data = new FormData(event.currentTarget as HTMLFormElement);
submitted = data.getAll('columns').join(', ');
}
</script>
<form class="columns" onsubmit={submit}>
<p id="sortable-columns-instructions" hidden>
Press Alt and an arrow key to move an item. Drag a handle with a pointer.
</p>
<ul data-slean="sortable" aria-label="Columns">
{#each COLUMNS as column (column.value)}
<li data-slean-value={column.value}>
<button
type="button"
data-slean-part="handle"
aria-label="Move {column.label}"
aria-describedby="sortable-columns-instructions"
aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown Alt+Home Alt+End"
></button>
{column.label}
<input type="hidden" name="columns" value={column.value} />
</li>
{/each}
</ul>
<div class="actions">
<button type="submit" data-slean="button">Save order</button>
<output>{submitted ? `Submitted: ${submitted}` : 'Not submitted yet'}</output>
</div>
</form>
<style>
.columns {
display: grid;
gap: var(--slean-space-3);
max-inline-size: 22rem;
}
.actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--slean-space-3);
}
output {
font-size: var(--slean-text-sm);
color: var(--slean-fg-muted);
}
</style>Examples
Move buttons
Press Alt and an arrow key, or use the arrow buttons, to move a step. Drag a handle with a pointer.
- Draft
- Legal review
- Approval
- Publish
Code
<script lang="ts">
// With @svelte-lean/vite the register import is injected for the static data-slean="sortable".
import '@svelte-lean/styles/sortable.css';
import { sortableParts } from '@svelte-lean/primitives/sortable';
const steps = sortableParts('sortable-steps');
const STEPS = [
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'Legal review' },
{ value: 'approve', label: 'Approval' },
{ value: 'publish', label: 'Publish' }
];
</script>
<p {...steps.instructions}>
Press Alt and an arrow key, or use the arrow buttons, to move a step. Drag a handle with a
pointer.
</p>
<ol class="steps" data-slean="sortable" aria-label="Approval steps">
{#each STEPS as step (step.value)}
<li {...steps.item(step.value)}>
<button {...steps.handle(step.label)}></button>
<span class="label">{step.label}</span>
<button {...steps.previous(step.label)}></button>
<button {...steps.next(step.label)}></button>
</li>
{/each}
</ol>
<style>
.steps {
max-inline-size: 22rem;
}
.label {
flex: 1;
}
</style>Horizontal
Press Alt and an arrow key to move a tab. Drag a handle with a pointer.
- General
- Security
- Billing
- Members
Code
<script lang="ts">
// With @svelte-lean/vite the register import is injected for the static data-slean="sortable".
import '@svelte-lean/styles/sortable.css';
import { sortableParts } from '@svelte-lean/primitives/sortable';
const tabs = sortableParts('sortable-tabs', { orientation: 'horizontal' });
const TABS = ['General', 'Security', 'Billing', 'Members'];
</script>
<p {...tabs.instructions}>
Press Alt and an arrow key to move a tab. Drag a handle with a pointer.
</p>
<!-- A row does not wrap: on a narrow screen it scrolls. -->
<div class="row">
<ul data-slean="sortable" data-slean-orientation="horizontal" aria-label="Tab order">
{#each TABS as tab (tab)}
<li {...tabs.item(tab.toLowerCase())}>
<button {...tabs.handle(tab)}></button>
{tab}
</li>
{/each}
</ul>
</div>
<style>
.row {
overflow-x: auto;
padding-block: var(--slean-space-2);
}
</style>Application-rendered order
Press Alt and an arrow key to move a section. Drag a handle with a pointer.
- General
- Security
- Billing
- Members
- Limits
State: ["general","security","billing","members","limits"]
Code
<script lang="ts">
// With @svelte-lean/vite the register import is injected for the static data-slean="sortable".
import '@svelte-lean/styles/sortable.css';
import { moveItem, sortableParts } from '@svelte-lean/primitives/sortable';
import '@svelte-lean/styles/button.css';
const sections = sortableParts('sortable-sections');
const SECTIONS = [
{ value: 'general', label: 'General' },
{ value: 'security', label: 'Security' },
{ value: 'billing', label: 'Billing' },
{ value: 'members', label: 'Members' },
{ value: 'limits', label: 'Limits' }
];
const label = (value: string) =>
SECTIONS.find((section) => section.value === value)?.label ?? value;
// The application owns the order; the behavior only asks for a move.
let order = $state(SECTIONS.map((section) => section.value));
let list: HTMLElement | undefined = $state();
</script>
<p {...sections.instructions}>
Press Alt and an arrow key to move a section. Drag a handle with a pointer.
</p>
<ul
class="sections"
bind:this={list}
data-slean="sortable"
data-slean-render="app"
data-slean-announce={'{label} is now {position} of {count}'}
aria-label="Sections"
onslean:change={(event) => {
if (event.detail.behavior === 'sortable') order = event.detail.value;
}}
>
{#each order as value (value)}
<li {...sections.item(value)}>
<button {...sections.handle(label(value))}></button>
{label(value)}
</li>
{/each}
</ul>
<div class="state">
<button
type="button"
data-slean="button"
data-variant="outlined"
onclick={() => list && moveItem(list, 'limits', 0)}>Move Limits first</button
>
<p>State: <code>{JSON.stringify(order)}</code></p>
</div>
<style>
.sections {
max-inline-size: 22rem;
}
.state {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--slean-space-3);
margin-block-start: var(--slean-space-3);
}
.state p {
margin: 0;
font-size: var(--slean-text-sm);
}
</style>Helper or plain markup
Two ways to write the same markup: with sortableParts() or by hand. Both render the same
HTML, which a test of this site compares; use either.
<script lang="ts">
import { sortableParts } from '@svelte-lean/primitives/sortable';
const s = sortableParts('columns');
const columns = [
{ value: 'name', label: 'Name' },
{ value: 'email', label: 'Email' }
];
</script>
<p {...s.instructions}>
Press Alt and an arrow key to move an item. Drag a handle with
a pointer.
</p>
<ul data-slean="sortable" aria-label="Columns">
{#each columns as column (column.value)}
<li {...s.item(column.value)}>
<button {...s.handle(column.label)}></button>
{column.label}
<button {...s.previous(column.label)}></button>
<button {...s.next(column.label)}></button>
</li>
{/each}
</ul><p id="columns-instructions" hidden>
Press Alt and an arrow key to move an item. Drag a handle with
a pointer.
</p>
<ul data-slean="sortable" aria-label="Columns">
<li data-slean-value="name">
<button
type="button"
data-slean-part="handle"
aria-label="Move Name"
aria-describedby="columns-instructions"
aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown Alt+Home Alt+End"
></button>
Name
<button
type="button"
data-slean-part="previous"
aria-label="Move Name up"
></button>
<button
type="button"
data-slean-part="next"
aria-label="Move Name down"
></button>
</li>
<li data-slean-value="email">
<button
type="button"
data-slean-part="handle"
aria-label="Move Email"
aria-describedby="columns-instructions"
aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown Alt+Home Alt+End"
></button>
Email
<button
type="button"
data-slean-part="previous"
aria-label="Move Email up"
></button>
<button
type="button"
data-slean-part="next"
aria-label="Move Email down"
></button>
</li>
</ul>What the helper does
- One id in, one reference out:
sortableParts('columns')writescolumns-instructionson the instructions and points every handle’saria-describedbyat it. - Strings only: no handler, no state, no instance. The server and the browser render the same attributes, so hydration has nothing to patch.
- Each button gets
type="button", its part and a name from the item’s label (Move Name,Move Name up);orientation: 'horizontal'switches the arrow names to left and right and the handle’saria-keyshortcutsto the left and right arrows;messagestranslates the words.
Points to watch
- Render
s.instructionsonce, beside the list and not inside it: a<ul>holds only<li>s. - It registers nothing and imports no stylesheet: the root's
data-slean="sortable"stay in your markup, where@svelte-lean/vitefinds it. - The id is not the list’s id: the helper derives only the instructions’ id from it, and the root needs none.
- Ids must be unique on the page. Hand-written markup works the same; the ids and the references between them are then yours to keep in step.
Usage
Install @svelte-lean/primitives and @svelte-lean/styles. With @svelte-lean/vite the behavior registers itself for every data-slean="sortable"; without it, import the register module once.
npm install @svelte-lean/primitivespnpm add @svelte-lean/primitivesyarn add @svelte-lean/primitivesbun add @svelte-lean/primitivesimport '@svelte-lean/primitives/sortable/register';import '@svelte-lean/styles/sortable.css';Render a <ul> or <ol> with data-slean="sortable" and a name. Give every item a unique data-slean-value and a handle button named after the item, described by one hidden instructions paragraph beside the list. Add previous and next buttons for a way that needs neither a pointer nor a key combination.
Put a hidden input in each item to submit the order with a form. Listen to slean:change to store the order or to cancel a move. When the list is rendered from application state, set data-slean-render="app" and write detail.value into that state.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <ul data-slean="sortable" aria-label> | – | yes | Or <ol>. Options: data-slean-orientation (vertical, horizontal), data-slean-render="app", data-slean-announce. |
| item | <li data-slean-value> | – | yes | Every element child of the root with a unique data-slean-value. Optional data-slean-label names it in announcements. |
| handle | <button type="button" aria-label aria-describedby aria-keyshortcuts data-slean-part="handle"> | handle | yes | One per item, named after it (Move Name). Pointer drags and the Alt keys start here. |
| previous | <button type="button" aria-label data-slean-part="previous"> | previous | no | Moves the item one place back (up, or left in a left-to-right row). |
| next | <button type="button" aria-label data-slean-part="next"> | next | no | Moves the item one place on. |
| instructions | <p id hidden> | – | yes | Beside the list, not inside it. Says how to move an item; every handle points at it with aria-describedby. |
| hidden input | <input type="hidden" name value> | – | no | Inside an item: it moves with the item, so a form submits the order. |
Keyboard
| Key | When | Result |
|---|---|---|
| Alt+ArrowUp/Alt+ArrowDown | focus on a handle | Moves the item one place back or on, in either orientation |
| Alt+ArrowLeft/Alt+ArrowRight | focus on a handle, horizontal list | Moves the item one place left or right; swapped under dir="rtl" |
| Alt+Home/Alt+End | focus on a handle | Moves the item to the first or the last place |
| Enter/Space | focus on a previous or next button | Moves the item one place back or on |
| Escape | during a pointer drag | Cancels the drag: every item slides back and nothing is reported |
API
Options
Attributes on the root, read by the behavior on every event.
| Name | Type | Default | Description |
|---|---|---|---|
# data-slean-orientation | 'vertical' | 'horizontal' | vertical | A column of items, or a row: left and right arrows and a drag on the x axis. A wrapping row is not supported. |
# data-slean-render | 'app' | – | The application reorders its own keyed list: the behavior dispatches slean:change and moves nothing, then animates the drop and restores focus. |
# data-slean-announce | string | – | What the live region says after a move, with {label}, {position} and {count} filled in; without it, “{label}, position {position} of {count}”. Translate it here. |
# data-slean-label | string | – | On an item: its name in announcements, where the item’s text is longer than its name. Without it the text is used. |
# aria-disabled | 'true' | – | On the root: every press and key is ignored. On a handle, previous or next button (or disabled): that control is ignored. |
States
Attributes the behavior writes during a pointer drag. Positions during the drag are inline transforms written through the CSSOM.
| Name | Type | Default | Description |
|---|---|---|---|
# data-slean-dragging | boolean attribute | – | On the root, from lift until the drop has settled; the stylesheet raises the lifted item. |
# data-slean-settling | boolean attribute | – | On the root for one frame, while settled positions are written without transitions. |
# data-slean-lifted | boolean attribute | – | On the item under the pointer, and on the item gliding into its place after the drop. |
Events
| Name | Type | Default | Description |
|---|---|---|---|
# slean:change | CustomEvent<SortableChangeDetail> | – | Dispatched from the root before an item moves (with data-slean-render="app", instead of moving it). Cancelable: preventDefault() moves nothing. |
# detail.behavior | 'sortable' | – | Narrows a handler shared by several primitives. |
# detail.value | string[] | – | The items’ data-slean-value in the order the move asks for. |
# detail.previous | string[] | – | The order before the move. |
# detail.from | number | – | Index of the moved item in previous. |
# detail.to | number | – | Index of the moved item in value. |
Functions
From @svelte-lean/primitives/sortable; importing the module registers nothing.
| Name | Type | Default | Description |
|---|---|---|---|
# sortableParts(id, options?) | SortableParts | – | The attributes of the instructions (<id>-instructions), item, handle, previous and next. options: orientation, messages. |
# sortableValue(root) | string[] | – | The items’ data-slean-value in document order: the current value. |
# sortableItems(root) | HTMLElement[] | – | The item elements in document order. |
# moveItem(root, value, to) | boolean | – | Asks for the same move a key press makes: slean:change first, then the element unless data-slean-render="app". Returns whether the move went ahead. Nothing is announced. |
Limits
One list at a time, moved on one axis. Not part of the primitive:
- dragging an item from one list to another (a kanban board);
- a two-dimensional grid or a wrapping row: an item is placed by its position on one axis;
- dragging a whole item without a handle, and dragging several selected items at once;
- persistence: listen to
slean:changeand storedetail.value; - a drop indicator other than the items moving out of the way.
Styling
sortable.css lays the items out as bordered rows in a column, or in a row with data-slean-orientation="horizontal", and draws the handle, previous and next buttons as icon buttons from the --slean-icon-* masks. During a drag the lifted item is raised with a shadow and the other items slide with a transition; coarse pointers get larger buttons. data-color on the root or an ancestor colors the focused handle and the lifted item’s border (primary by default). The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-space-1 | calc(var(--slean-spacing) * 1) | gap, padding-block, padding-inline |
--slean-fg | var(--slean-neutral-12) | color |
--slean-space-2 | calc(var(--slean-spacing) * 2) | gap, padding-inline |
--slean-control-height-sm | 2rem | min-block-size, inline-size, block-size |
--slean-border | var(--slean-neutral-6) | border |
--slean-radius-md | var(--slean-radius) | border-radius |
--slean-surface | oklch(100% 0 0) | background |
--slean-control-height-xs | 1.75rem | inline-size, block-size |
--slean-radius-sm | calc(var(--slean-radius) * 0.6) | border-radius |
--slean-fg-muted | var(--slean-neutral-11) | color |
--slean-duration-fast | 100ms | transition |
--slean-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-icon-grip | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4h.01M10 4h.01M6 8h.01M10 8h.01M6 12h.01M10 12h.01' stroke='black' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E") | mask |
--slean-icon-chevron-up | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 10l4-4 4 4' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") | mask-image |
--slean-icon-chevron-down | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") | mask-image |
--slean-icon-chevron-left | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M10 4L6 8l4 4' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") | mask-image |
--slean-icon-chevron-right | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4l4 4-4 4' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") | mask-image |
--slean-muted-hover | var(--slean-neutral-4) | background |
--slean-focus-ring | var(--slean-focus-ring-width) solid var(--slean-focus-ring-color) | outline |
--slean-focus-ring-offset | 2px | outline-offset |
--slean-color-main | var(--slean-primary-main) | color, border-color |
--slean-duration-normal | 160ms | transition |
--slean-elevation-3 | 0 4px 12px oklch(0% 0 0 / 0.1), 0 1px 3px oklch(0% 0 0 / 0.08) | box-shadow |
State selectors the stylesheet targets, all from the platform or ARIA: :dir(rtl), :disabled, :focus-visible, :hover, [aria-disabled="true"].
Variant attributes: data-slean-orientation (horizontal); data-slean-value; data-slean-dragging; data-slean-settling; data-slean-lifted.
Controlled integration
Every move asks first: slean:change is cancelable, and cancelling it moves nothing (a dropped item slides back). A list the behavior moves itself needs no state; store detail.value to keep the order. With data-slean-render="app" write it into the state the keyed {#each} reads, as in the example above, and leave animate:flip off the items: the behavior already animates the drop.
<ul
data-slean="sortable"
aria-label="Columns"
onslean:change={(event) => {
if (event.detail.behavior !== 'sortable') return;
// Keep the name column first: a cancelled move moves nothing, a dropped item slides back.
if (event.detail.value[0] !== 'name') return event.preventDefault();
localStorage.setItem('columns', JSON.stringify(event.detail.value));
}}
>
…
</ul>Under the hood
Runtime and size
- Tier
- 2 · Lazy scoped controller
- Behavior JS
- 3833 B brotli · 4279 B gzip ·
packages/· methodprimitives/ artifacts/ size.json - Platform features
<ul>/<ol>,<button>,setPointerCapture(),touch-action,moveBefore(),role="status"- Shared listeners
- pointerdown, keydown, click
- Per-instance listeners
- scoped to one interaction session
- Lazy state
- WeakMap entry created on first interaction
- Native base
<ul>/<ol> + <button> handles; document order is the value
The sortable list registers one pointerdown, one keydown and one click handler with the shared router. During a pointer drag, and only then, the reorder engine holds pointermove, pointerup, pointercancel, a capturing keydown for Escape and the scroll of the scrolling container under one AbortController, with an auto-scroll timer; all are released when the press ends. One live region, created on the first announcement, speaks for every list of the page. A thousand lists keep one listener per type (tests/sortable.test.ts).
Why this implementation
The platform has no reorderable list. HTML drag and drop (draggable) has no keyboard path, no touch support in every engine, no preview of the drop position and no scroll near an edge, and the dragged item cannot be styled. The list stays a native <ul> or <ol> whose document order is the value; each item gets a native <button> handle, and a form receives the order through hidden inputs inside the items, without script.
Keys and buttons need no state: the behavior reads the item from the event target and moves the element, with moveBefore() where the browser has it, so focus and the state inside the item travel with it. A pointer drag is the one moment that needs memory: the shared reorder engine (@svelte-lean/core/reorder, also behind the DataTable row drag) reads the items’ boxes once at lift, holds its listeners for the press and releases them on drop. That scoped lifetime is what Tier 2 means here.
The browser owns
- the list and its document order, which is the value
- the handles, previous and next buttons: focus, the tab order, Enter and Space
- pointer capture: the handle keeps receiving the drag outside its own box
- the hidden inputs that submit the order with the form
- moveBefore(), which moves an item without losing its focus or state, where the browser has it
Svelte Lean owns
- Alt with the arrow keys, Home and End on a handle; mirrored in a right-to-left row
- a pointer drag that slides the other items out of the way before the drop, with auto-scroll near an edge and Escape to cancel, whose listeners live as long as the press
- the cancelable slean:change, and the move of the element unless data-slean-render="app"
- focus kept on the control that moved the item, and the glide of a dropped item into the place the application gave it
- one live region for the page that announces the new position
- sortable.css: the item rows, the grip and arrow icons, focus, the lifted item and the drag states
Accessibility
- The root is a list named with
aria-labeloraria-labelledby. Each handle is a button named after its item (Move Name), described by the instructions witharia-describedby, with the keys inaria-keyshortcuts. - Alt with ArrowUp or ArrowDown moves the focused handle’s item one place; Alt with Home or End to the ends. Keys without Alt, or with Ctrl, Meta or Shift as well, are left to the page. Alt+ArrowUp and Alt+ArrowDown work in a horizontal list too: no browser binds them, while Alt+ArrowLeft is the history back key of Chromium and Firefox on Windows and Linux (the behavior cancels it only on a handle).
previousandnextare the move buttons of the WAI-ARIA rearrangeable listbox: a way to reorder that needs neither a pointer nor a key combination to be discovered.- After every move one visually hidden
role="status"region saysdata-slean-announcewith the item’s label, position and count. Inside an open<dialog>the region is created in the dialog, since outside a modal dialog it would be inert. - A moved item keeps focus on the control that moved it, also after an application re-render. Under
dir="rtl"a horizontal row starts on the right: ArrowLeft moves an item on.
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
Pointer events, pointer capture | Widely available | Not applicable |
touch-action: none on the handle | Widely available | Not applicable |
Element.moveBefore() | Not Baseline | insertBefore(), and the behavior puts focus back on the control that had it |
role="status" live region | Every engine and screen reader | Not applicable |
Without JavaScript
The list renders in its server order with its handles, which are buttons that do nothing, and a form submits that order through the hidden inputs. Previous and next buttons can work without script when they carry name and value and the server handles the submission.
Server rendering and hydration
Render the items in their current order with their values and handles. Nothing is measured on the server and no id is generated: the instructions' id comes from the author id given to sortableParts().
Before the behavior loads, the handles and buttons are focusable but move nothing. The first event after hydration works on the server's markup.
Compatibility
Pointer events, pointer capture and touch-action are widely available. Element.moveBefore() is not Baseline: where it is missing the behavior uses insertBefore() and puts focus back on the control that had it. Cross-list drags, grids and wrapping rows are not part of the primitive (see Limits).
Tests and source
packages/primitives/tests/sortable.test.tsVitest: keys, RTL, buttons, cancelled and disabled moves, the live region, moveBefore() and insertBefore(), the form order, data-slean-render="app", a pointer drag, Escape, 1000 rootspackages/core/tests/reorder.test.tsVitest: the shared reorder engine the drag runs on (also used by the DataTable row drag)apps/playground/tests/primitives/sortable.spec.tsPlaywright: keyboard moves, a real pointer drag, Escape, the application-rendered list, auto-scroll, routed listeners with 1 and 1000 rootsapps/playground/tests/primitives/a11y.spec.tsaxe on every playground page and on open dialog, popover and menu statesfixtures/sortable-onlyconsumer build asserting which modules ship with a static data-slean="sortable"packages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
packages/primitives/src/sortable/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/sortable/contract.tstyped constants: name, tier, base, parts, options, eventspackages/primitives/src/sortable/behavior.tsthe behavior definition, sortableValue(), sortableItems() and moveItem()packages/primitives/src/sortable/parts.tssortableParts(), the attribute helperpackages/primitives/src/sortable/register.tsthe registration modulepackages/core/src/reorder.tsthe shared reorder engine (@svelte-lean/core/reorder)packages/styles/css/sortable.cssthe optional stylesheet