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-svelte
npm install @snap-engine/core @snap-engine/asset-base-react @snap-engine/snapsort @snap-engine/snapsort-react react react-dom
npm install @snap-engine/core @snap-engine/snapsort

SvelteKit / 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";

  const tasks = [
    { id: "design", label: "Design" },
    { id: "build", label: "Build" },
    { id: "ship", label: "Ship" },
  ];
</script>

<Engine id="quick-start">
  <Container config={{ direction: "column", groupID: "tasks" }} items={tasks}>
    {#snippet entry(task)}
      <Item itemId={task.id}>{task.label}</Item>
    {/snippet}
  </Container>
</Engine>
import { Engine, Container, Item } from "@snap-engine/snapsort-react";

const tasks = [
  { id: "design", label: "Design" },
  { id: "build", label: "Build" },
  { id: "ship", label: "Ship" },
];

export function QuickStart() {
  return (
    <Engine id="quick-start">
      <Container config={{ direction: "column", groupID: "tasks" }}>
        {tasks.map((task) => (
          <Item key={task.id} itemId={task.id}>
            {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. The list on screen updates, but your tasks array does not — wiring drops back into framework state is covered in Session Lifecycle.

Production Checklist

Before treating a list as complete:

  1. Give every item a stable, unique itemId; do not use its array index.
  2. Keep the framework array as the source of truth. Update it synchronously in onItemMove (and onItemSwap when using swap mode).
  3. Put containers that exchange items in the same groupID and attach identifying metadata to each container.
  4. Give the Engine and sortable container real dimensions. Engine defaults to height: 100%, position: relative, and overflow: visible; pass style or a class when the parent does not already establish a height.
  5. Render one framework component for each core object. Use Ghost for framework-owned flow placeholders instead of wrapping event.ghostItem in a normal Item.

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.