Setup
Install
SnapSort consists of SnapSort package itself, the SnapEngine core it’s built on, and the frontend framework bindings (if applicable).
npm install @snap-engine/core @snap-engine/asset-base-svelte @snap-engine/snapsort @snap-engine/snapsort-sveltenpm install @snap-engine/core @snap-engine/asset-base-react @snap-engine/snapsort @snap-engine/snapsort-react react react-domnpm install @snap-engine/core @snap-engine/snapsortSvelteKit / SSR
SnapEngine asset packages publish TypeScript source. SvelteKit must bundle the
framework-agnostic asset packages during SSR instead of externalizing them to
Node. Add them to ssr.noExternal in vite.config.js:
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [sveltekit()],
ssr: {
noExternal: ["@snap-engine/asset-base", "@snap-engine/snapsort"],
},
});Your First Sortable List
Wrap SnapSort components in an Engine, give a Container a group,
and render one Item per entry. This creates a working drag-and-drop list:
<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";
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="quick-start">
<Container
config={{
direction: "column",
groupID: "tasks",
callbacks: { onItemMove },
}}
items={tasks}
>
{#snippet entry(task)}
<Item itemId={task.id}>{task.label}</Item>
{/snippet}
</Container>
</Engine>import { useState } from "react";
import { Engine, Container, Ghost, Item } from "@snap-engine/snapsort-react";
import type {
GhostInsertEvent,
GhostRemoveEvent,
ItemMoveEvent,
} from "@snap-engine/snapsort";
type Task = { id: string; label: string };
type RenderEntry =
| { kind: "task"; task: Task }
| { kind: "ghost"; event: GhostInsertEvent };
const initialTasks: Task[] = [
{ id: "design", label: "Design" },
{ id: "build", label: "Build" },
{ id: "ship", label: "Ship" },
];
export function QuickStart() {
const [tasks, setTasks] = useState(initialTasks);
const [ghost, setGhost] = useState<GhostInsertEvent | null>(null);
function onItemMove(event: ItemMoveEvent) {
setTasks((current) => {
const task = current.find((entry) => entry.id === event.itemId);
if (!task) return current;
const next = current.filter((entry) => entry.id !== event.itemId);
next.splice(Math.max(0, Math.min(event.to.index, next.length)), 0, task);
return next;
});
}
function onGhostRemove(event: GhostRemoveEvent) {
setGhost((current) =>
current?.ghostItem === event.ghostItem ? null : current,
);
}
const entries: RenderEntry[] = tasks.map((task) => ({ kind: "task", task }));
if (ghost) {
const originalIndex = tasks.findIndex((task) => task.id === ghost.originalItemId);
const coreIndex = Math.max(0, Math.min(ghost.index, entries.length));
entries.splice(originalIndex !== -1 && originalIndex <= coreIndex ? coreIndex + 1 : coreIndex, 0, {
kind: "ghost",
event: ghost,
});
}
return (
<Engine id="quick-start">
<Container
config={{
direction: "column",
groupID: "tasks",
callbacks: {
onItemMove,
onGhostInsert: setGhost,
onGhostRemove,
},
}}
>
{entries.map((entry) =>
entry.kind === "ghost" ? (
<Ghost key={`ghost-${entry.event.ghostItem.id}`} event={entry.event} />
) : (
<Item key={entry.task.id} itemId={entry.task.id}>
{entry.task.label}
</Item>
),
)}
</Container>
</Engine>
);
}import { Engine } from "@snap-engine/core";
import { CollisionEngine } from "@snap-engine/core/collision";
import { Container, Item } from "@snap-engine/snapsort";
const root = document.querySelector("#quick-start");
const list = root.querySelector("[data-snapsort-container]");
const engine = new Engine();
engine.setCollisionEngine(new CollisionEngine());
engine.assignDom(root);
const container = new Container(engine, null, {
direction: "column",
groupID: "tasks",
});
container.element = list;
for (const element of list.querySelectorAll("[data-item-id]")) {
const item = new Item(engine, container);
item.itemId = element.dataset.itemId;
item.element = element;
}Items can now be dragged to reorder, with FLIP animations included by default. In Svelte and React, the callback updates framework state and the framework renders the new order. SnapSort never reparents framework-owned item nodes. The Vanilla integration uses SnapSort’s default DOM callbacks. Cross-container state updates are covered in Session Lifecycle.
Production Checklist
Before treating a list as complete:
- Give every item a stable, unique
itemId; do not use its array index. - Keep the framework array as the source of truth. Update it synchronously in
onItemMove(andonItemSwapwhen using swap mode). - Put containers that exchange items in the same
groupIDand attach identifyingmetadatato each container. - Give the
Engineand sortable container real dimensions.Enginedefaults toheight: 100%,position: relative, andoverflow: visible; passstyleor a class when the parent does not already establish a height. - Render one framework component for each core object. Svelte
Containermanages its ghost entries; React code storesonGhostInsert/onGhostRemoveevents and renders keyedGhostcomponents. Never wrapevent.ghostItemin a normalItem.
All wrapper components forward normal div attributes such as class, className, style, id, data-*, ARIA attributes, and event handlers. Consumer styles are merged after the structural defaults, so an explicit style can override them.