Svelte API
Components, props, and state patterns for @snap-engine/snapsort-svelte.
The Svelte package wraps SnapSort’s core objects in Svelte 5 components. Use this page when Svelte owns the DOM and SnapSort should provide drag, drop, ordering, and animation behavior.
Svelte state is the sole source of truth for persistent item order. The adapter
does not install SnapSort’s Vanilla DOM callbacks. Every destination that can
accept a move must synchronously update $state in onItemMove (or onItemInsert), and swap mode must provide onItemSwap. Never call structural
DOM APIs from these callbacks.
Install
npm install @snap-engine/core @snap-engine/asset-base-svelte @snap-engine/snapsort @snap-engine/snapsort-svelte Import
<script lang="ts">
import { Engine } from "@snap-engine/asset-base-svelte";
import { Container, Ghost, Item, Handle } from "@snap-engine/snapsort-svelte";
</script> Container, Item, Ghost, and Handle are the Svelte primitives. Every container picks its drag behavior with a mode config field.
Import core types when you need typed callbacks, metadata, or access to a bindable container object:
import type {
CanDropEvent,
Container,
ContainerCallbacks,
ContainerConfig,
DragEndEvent,
DragStartEvent,
DropTargetChangeEvent,
GhostCreateEvent,
GhostInsertEvent,
GhostRemoveEvent,
Item,
ItemInsertEvent,
ItemMoveEvent,
ItemRemoveEvent,
ItemSnapshotMetadata,
} from "@snap-engine/snapsort"; Modes
Every container is created from the same Container component; pick its drag behavior with config.mode. mode defaults to "euclidean".
| Mode | Best for |
|---|---|
"euclidean" (default) | Lists, boards, and general reordering. |
"progressive" | Sentence builders, token banks, and wrapped variable-width rows. |
"insertion" | File trees, outlines, and other drop-marker UIs where the placeholder is a line instead of a full item. |
Item never needs a mode — the same Item works under a container of any mode.
In these abbreviated examples, tasks, words, and sections are $state arrays and onItemMove synchronously updates the corresponding array.
<Container
config={{ direction: "column", groupID: "tasks", callbacks: { onItemMove } }}
items={tasks}
>
{#snippet entry(task)}
<Item itemId={task.id}>{task.label}</Item>
{/snippet}
</Container>
<Container
config={{ mode: "progressive", direction: "row", groupID: "sentence", callbacks: { onItemMove } }}
items={words}
>
{#snippet entry(word)}
<Item itemId={word.id}>{word.label}</Item>
{/snippet}
</Container>
<Container
config={{ mode: "insertion", direction: "column", groupID: "outline", callbacks: { onItemMove } }}
items={sections}
>
{#snippet entry(section)}
<Item itemId={section.id}>{section.label}</Item>
{/snippet}
</Container> Mode is a property of the whole drag tree, resolved from the root container when a drag starts — nest containers of the same mode.
Binding Model
Wrap SnapSort components in an Engine. The engine provides input handling, layout reads, writes, and animation scheduling.
Each container component creates a SnapSort container object and provides it through Svelte context. Each item component creates a SnapSort item object and registers it with the nearest container context. Handle reads the nearest item context and registers its DOM node as an input alias, so dragging can start from that handle instead of the whole item.
Pass items, getItemId, and an entry snippet. Each entry must render exactly one <Item> or nested <Container> whose itemId matches getItemId(entry).
<script lang="ts">
import { Engine } from "@snap-engine/asset-base-svelte";
import { Container, Ghost, Handle, Item } from "@snap-engine/snapsort-svelte";
import type { ItemMoveEvent } from "@snap-engine/snapsort";
let tasks = $state([
{ id: "design", label: "Design" },
{ id: "build", label: "Build" },
{ id: "ship", label: "Ship" },
]);
function onItemMove(event: ItemMoveEvent) {
const task = tasks.find((entry) => entry.id === event.itemId);
if (!task) return;
const next = tasks.filter((entry) => entry.id !== event.itemId);
next.splice(Math.max(0, Math.min(event.to.index, next.length)), 0, task);
tasks = next;
}
</script>
<Engine id="tasks">
<Container
config={{ direction: "column", groupID: "tasks", callbacks: { onItemMove } }}
items={tasks}
getItemId={(task) => task.id}
>
{#snippet entry(task)}
<Item itemId={task.id}>
<article>
<Handle className="drag-handle">Drag</Handle>
<span>{task.label}</span>
</article>
</Item>
{/snippet}
</Container>
</Engine> State Backed Lists
Update arrays synchronously from callbacks.onItemMove. The adapter wraps the
callback in Svelte’s flushSync, so the framework commits the DOM before
SnapSort reads final geometry for FLIP and drop animation. Do not provide tick, a promise, a timer, or an animation-frame callback for this work.
onItemMove is preferred over the lower-level onItemInsert whenever a drop is really “move this item from one position to another” — it hands you both the source and destination as one event instead of making you reconstruct the move from an insert alone.
<script lang="ts">
import { Engine } from "@snap-engine/asset-base-svelte";
import { Container, Item } from "@snap-engine/snapsort-svelte";
import type { ItemMoveEvent } from "@snap-engine/snapsort";
type Task = { id: string; label: string };
type Column = { id: string; label: string; tasks: Task[] };
let columns = $state<Column[]>([
{
id: "todo",
label: "To Do",
tasks: [
{ id: "task-1", label: "Sketch layout" },
{ id: "task-2", label: "Wire behavior" },
],
},
{
id: "done",
label: "Done",
tasks: [{ id: "task-3", label: "Create branch" }],
},
]);
function moveTask(taskId: string, columnId: string, index: number) {
let movedTask: Task | null = null;
const withoutTask = columns.map((column) => {
const sourceIndex = column.tasks.findIndex((task) => task.id === taskId);
if (sourceIndex === -1) return column;
const nextTasks = column.tasks.slice();
[movedTask] = nextTasks.splice(sourceIndex, 1);
return { ...column, tasks: nextTasks };
});
if (!movedTask) return;
columns = withoutTask.map((column) => {
if (column.id !== columnId) return column;
const nextTasks = column.tasks.slice();
nextTasks.splice(Math.max(0, Math.min(index, nextTasks.length)), 0, movedTask);
return { ...column, tasks: nextTasks };
});
}
function handleItemMove(event: ItemMoveEvent) {
const itemId = event.itemId;
const columnId = event.to.containerMetadata.columnId;
if (typeof itemId !== "string" || typeof columnId !== "string") return;
moveTask(itemId, columnId, event.to.index);
}
</script>
<Engine id="task-board">
<Container
config={{ direction: "row", name: "board-root", noDrop: true }}
locked={true}
items={columns}
getItemId={(column) => column.id}
>
{#snippet entry(column)}
<Container
itemId={column.id}
metadata={{ columnId: column.id }}
config={{
direction: "column",
groupID: "tasks",
callbacks: {
onItemMove: handleItemMove,
},
}}
locked={true}
items={column.tasks}
getItemId={(task) => task.id}
>
{#snippet before()}<h2>{column.label}</h2>{/snippet}
{#snippet entry(task)}
<Item itemId={task.id}>
<article>{task.label}</article>
</Item>
{/snippet}
</Container>
{/snippet}
</Container>
</Engine> Use callbacks.onItemRemove when container.removeItem(id) or your own controls should also remove data from Svelte state. Drag moves and moveItem() are driven by onItemMove when it’s defined, and fall back to onItemInsert on the destination container otherwise.
Imperative Moves
Use bind:container when buttons, keyboard handlers, or other non-drag controls need to ask SnapSort to move an item. Call moveItem(itemId, targetContainer, index) with the same ID used in itemId.
<script lang="ts">
import { Engine } from "@snap-engine/asset-base-svelte";
import { Container, Item } from "@snap-engine/snapsort-svelte";
import type { Container as SnapSortContainer, ItemMoveEvent } from "@snap-engine/snapsort";
let source: SnapSortContainer | undefined = $state();
let target: SnapSortContainer | undefined = $state();
let sourceCards = $state([{ id: "card-1", label: "Card 1" }]);
let targetCards = $state<{ id: string; label: string }[]>([]);
function onItemMove(event: ItemMoveEvent) {
const card = [...sourceCards, ...targetCards].find((entry) => entry.id === event.itemId);
const targetListId = event.to.containerMetadata.listId;
if (!card || (targetListId !== "source" && targetListId !== "target")) return;
sourceCards = sourceCards.filter((entry) => entry.id !== event.itemId);
targetCards = targetCards.filter((entry) => entry.id !== event.itemId);
const list = targetListId === "source" ? sourceCards : targetCards;
list.splice(Math.max(0, Math.min(event.to.index, list.length)), 0, card);
if (targetListId === "source") sourceCards = list;
else targetCards = list;
}
function moveToTarget(id: string) {
if (!source || !target) return;
source.moveItem(id, target, target.numberOfItems);
}
</script>
<Engine id="click-move">
<Container
bind:container={source}
config={{ direction: "column", groupID: "cards", callbacks: { onItemMove } }}
metadata={{ listId: "source" }}
items={sourceCards}
>
{#snippet entry(card)}
<Item itemId={card.id}>
<button type="button" onclick={() => moveToTarget(card.id)}>Move card</button>
</Item>
{/snippet}
</Container>
<Container
bind:container={target}
config={{ direction: "column", groupID: "cards", callbacks: { onItemMove } }}
metadata={{ listId: "target" }}
items={targetCards}
>
{#snippet entry(card)}<Item itemId={card.id}>{card.label}</Item>{/snippet}
</Container>
</Engine> Use the same synchronous onItemMove (or onItemInsert) state update so
imperative moves and drag moves render through Svelte in exactly the same way.
Bindable Container API
bind:container exposes the created core container object.
| API | Description |
|---|---|
container.moveItem(id, targetContainer, index) | Moves an item by itemId into another container or index. |
container.removeItem(id) | Removes an item by itemId or internal object ID. |
container.numberOfItems | Returns the current number of tracked child items. |
container.groupID | Returns the configured group ID. |
container.direction | Gets or sets "column" or "row". |
container.mainAxisAlign | Gets or sets "start" or "center". |
container.dropArea | Gets or sets explicit drop-area behavior. |
container.mode | Gets or sets "euclidean" | "progressive" | "insertion". |
container.config | Returns the live ContainerConfig object. |
container.callbacks | Returns the configured callback object. |
container.dragSession | Returns the in-progress DragSession for this tree, or null when nothing is being dragged. |
Container
Props
type ContainerProps = {
config: ContainerConfig;
items: unknown[];
getItemId?: (entry: unknown) => string;
entry: Snippet<[unknown]>;
ghost?: Snippet<[GhostInsertEvent]>;
before?: Snippet;
after?: Snippet;
itemId?: string;
container?: Container;
locked?: boolean;
className?: string;
metadata?: Record<string, unknown>;
}; | Prop | Default | Description |
|---|---|---|
config | Required | SnapSort container configuration. |
items | Required | Data entries for this container. |
getItemId | (entry) => entry.id | Stable ID for each entry. |
entry | Required | Renders one <Item> or nested <Container> for each entry. |
ghost | Default spacer | Renders flow-mode target ghosts. Custom snippets should render <Ghost {event}>...</Ghost>. |
before | undefined | Non-sortable content rendered before entries. |
after | undefined | Non-sortable content rendered after entries. |
itemId | undefined | Required when this container is rendered as a sortable entry inside another container. |
container | Bindable | Receives the created core container object. |
locked | true | Keeps the container itself from being dragged when nested. Set to false for draggable nested containers. |
className | "" | Extra class string applied to the rendered element. |
metadata | {} | Metadata assigned to the container object and exposed in callback events. |
Container renders a div with snapsort-container plus a mode-specific class.
Config
| Option | Default | Description |
|---|---|---|
mode | "euclidean" | Drag/drop mode for this tree: "euclidean", "progressive", "insertion", or "swap". |
strategy | Resolved from mode | Advanced: a custom { dropTarget, lifecycle } strategy pair, overriding mode. |
direction | "column" | Main layout direction: "column" or "row". |
groupID | "default-group" | Containers with the same group can exchange items. |
mainAxisAlign | "start" | Main-axis alignment for virtual placement. Supports "start" and "center". |
name | Generated | Human-readable container name. |
animation | Default 100ms animations | Reorder, drop, and click-move animation settings. Set to null to disable configured animations. |
disableFlip | false | Disables FLIP movement animation. |
noDrop | false | Prevents the container from being a drop target. Useful for root layout containers. |
dropArea | false | Treats the container as an explicit drop area for collision filtering. |
callbacks | No persistent mutation default | Required state callbacks plus lifecycle and validation hooks. The Svelte adapter never installs Vanilla DOM mutation callbacks. |
Callbacks
| Callback | When it runs |
|---|---|
onItemMove(event) | SnapSort moves an item to a new container/index. Preferred over onItemInsert for state-backed lists — carries both from and to locations. Falls back to onItemInsert on the destination container when not defined. |
onItemSwap(event) | Commits a pairwise exchange. Required for swap mode in the Svelte adapter. |
onItemInsert(event) | SnapSort inserts an item into a container (the primitive onItemMove falls back to). |
onItemRemove(event) | SnapSort removes an item through removeItem(id). |
onDragStart(event) | A drag is starting, before any ghost/state changes. Return false to veto the drag. |
onDragEnd(event) | A drag has ended and its mutation has been committed. event.destination is null when the item returned to its source or the drag was cancelled. |
onDropTargetChange(event) | The prospective drop container/index changed during a drag. Fires on the root container. |
canDrop(event) | Consulted while resolving candidates for this container; return false to reject drops into it for the current drag. Evaluated once per container per resolution, not once per candidate slot — keep it cheap. |
onGhostInsert(event) | SnapSort inserts or moves a ghost item or insertion marker. |
onGhostRemove(event) | SnapSort removes a ghost item or insertion marker. |
createGhost(event) | Core/Vanilla hook. Unsupported by the Svelte adapter; use the ghost snippet instead. |
flushMutation(mutation) | Runs structural callbacks inside Svelte’s synchronous commit. Supplied automatically by the adapter. |
awaitMutation() | Deprecated compatibility callback. Do not use it for framework state updates. |
Ghost
Ghost renders framework-owned flow spacers, insertion markers, and swap
pointer ghosts used by custom ghost snippets. It binds event.ghostItem.element and applies the geometry for event.kind/event.role.
<Container config={{ mode: "progressive", direction: "row", callbacks: { onItemMove } }} items={tiles}>
{#snippet entry(tile)}
<Item itemId={tile.id}>{tile.text}</Item>
{/snippet}
{#snippet ghost(event)}
<Ghost {event}>
<span class="tile-ghost">{event.originalItemId}</span>
</Ghost>
{/snippet}
</Container> Item
Props
type ItemProps = {
children: Snippet;
itemId: string;
style?: string;
className?: string;
metadata?: ItemSnapshotMetadata;
itemObject?: Item | null;
}; | Prop | Default | Description |
|---|---|---|
children | Required | Rendered item content. |
itemId | Required | Stable item ID used by moves, removes, ghosts, and state callbacks. |
style | "" | Inline style string applied to the rendered element. |
className | "" | Extra class string applied to the rendered element. |
metadata | {} | Extra metadata assigned to the item object and exposed in callback events. |
itemObject | null | Optional existing core item object. When provided, the component does not destroy it on unmount. |
Item renders a div with snapsort-item — the same component and class work under any container mode.
ItemSnapshotMetadata is an open record with built-in fields:
| Field | Description |
|---|---|
insertionMarkerInsetLeft | Optional left inset for insertion marker width. |
insertionMarkerInsetRight | Optional right inset for insertion marker width. |
Handle
Handle registers a child element as an input alias for the nearest item.
<Item itemId="task-1">
<article>
<Handle className="drag-handle">Drag</Handle>
<span>Task 1</span>
</article>
</Item> Props
type HandleProps = {
children: Snippet;
style?: string;
className?: string;
}; Handle renders a div with snapsort-handle and the supplied className.