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.
Install
npm install @snap-engine/core @snap-engine/asset-base-svelte @snap-engine/snapsort @snap-engine/snapsort-svelteImport
<script lang="ts">
import { Engine } from "@snap-engine/asset-base-svelte";
import { Container, Item, Handle } from "@snap-engine/snapsort-svelte";
</script>Container and Item are the only components — 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.
<Container config={{ direction: "column", groupID: "tasks" }}>
<Item metadata={{ itemId: "task-1" }}>Task 1</Item>
</Container>
<Container config={{ mode: "progressive", direction: "row", groupID: "sentence" }}>
<Item metadata={{ itemId: "word-1" }}>hello</Item>
</Container>
<Container config={{ mode: "insertion", direction: "column", groupID: "outline" }}>
<Item metadata={{ itemId: "section-1" }}>Section 1</Item>
</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.
Use keyed {#each} blocks and pass a stable metadata.itemId when Svelte owns the rendered list. SnapSort uses that ID for imperative moves and framework-owned state updates.
<script lang="ts">
import { Engine } from "@snap-engine/asset-base-svelte";
import { Container, Handle, Item } from "@snap-engine/snapsort-svelte";
const tasks = [
{ id: "design", label: "Design" },
{ id: "build", label: "Build" },
{ id: "ship", label: "Ship" },
];
</script>
<Engine id="tasks">
<Container config={{ direction: "column", groupID: "tasks" }}>
{#each tasks as task (task.id)}
<Item metadata={{ itemId: task.id }}>
<article>
<Handle className="drag-handle">Drag</Handle>
<span>{task.label}</span>
</article>
</Item>
{/each}
</Container>
</Engine>State Backed Lists
When Svelte owns the list data, update your arrays from callbacks.onItemMove and provide callbacks.awaitMutation: tick. SnapSort calls the mutation callback, waits for awaitMutation, then reads final DOM geometry for FLIP and drop animation.
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 { tick } from "svelte";
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.itemMetadata.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}>
{#each columns as column (column.id)}
<Container
metadata={{ columnId: column.id }}
config={{
direction: "column",
groupID: "tasks",
callbacks: {
onItemMove: handleItemMove,
awaitMutation: tick,
},
}}
locked={true}
>
<h2>{column.label}</h2>
{#each column.tasks as task (task.id)}
<Item metadata={{ itemId: task.id }}>
<article>{task.label}</article>
</Item>
{/each}
</Container>
{/each}
</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 metadata.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 } from "@snap-engine/snapsort";
let source: SnapSortContainer | undefined = $state();
let target: SnapSortContainer | undefined = $state();
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" }}>
<Item metadata={{ itemId: "card-1" }}>
<button type="button" onclick={() => moveToTarget("card-1")}>
Move card
</button>
</Item>
</Container>
<Container bind:container={target} config={{ direction: "column", groupID: "cards" }} />
</Engine>When your list is backed by Svelte state, use the same onItemMove (or onItemInsert) and awaitMutation pattern so imperative moves and drag moves update the rendered arrays 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 metadata.itemId into another container or index. |
container.removeItem(id) | Removes an item by metadata.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;
children: Snippet;
container?: Container;
locked?: boolean;
className?: string;
metadata?: Record<string, unknown>;
};| Prop | Default | Description |
|---|---|---|
config | Required | SnapSort container configuration. |
children | Required | Item and nested container content. |
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: snapsort-mode-euclidean, snapsort-mode-progressive, or snapsort-mode-insertion.
Config
| Option | Default | Description |
|---|---|---|
mode | "euclidean" | Drag/drop mode for this tree: "euclidean", "progressive", or "insertion". |
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 | Default DOM callbacks | Hooks for DOM, framework state, and custom ghost behavior. |
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. |
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) | SnapSort needs a ghost/marker element. event.kind is "flow" (euclidean/progressive) or "marker" (insertion). Return an element, null, or void. |
awaitMutation() | SnapSort waits for framework-owned DOM updates before reading final layout. Use tick for Svelte state updates. |
Item
Props
type ItemProps = {
children: Snippet;
style?: string;
className?: string;
metadata?: ItemSnapshotMetadata;
itemObject?: Item | null;
};| Prop | Default | Description |
|---|---|---|
children | Required | Rendered item content. |
style | "" | Inline style string applied to the rendered element. |
className | "" | Extra class string applied to the rendered element. |
metadata | {} | Metadata assigned to the item object. Use metadata.itemId for stable keyed state. |
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 |
|---|---|
itemId | Stable item ID used by moveItem, removeItem, and framework state callbacks. |
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 metadata={{ 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.