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".

ModeBest 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.

APIDescription
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.numberOfItemsReturns the current number of tracked child items.
container.groupIDReturns the configured group ID.
container.directionGets or sets "column" or "row".
container.mainAxisAlignGets or sets "start" or "center".
container.dropAreaGets or sets explicit drop-area behavior.
container.modeGets or sets "euclidean" | "progressive" | "insertion".
container.configReturns the live ContainerConfig object.
container.callbacksReturns the configured callback object.
container.dragSessionReturns 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>;
};
PropDefaultDescription
configRequiredSnapSort container configuration.
itemsRequiredData entries for this container.
getItemId(entry) => entry.idStable ID for each entry.
entryRequiredRenders one <Item> or nested <Container> for each entry.
ghostDefault spacerRenders flow-mode target ghosts. Custom snippets should render <Ghost {event}>...</Ghost>.
beforeundefinedNon-sortable content rendered before entries.
afterundefinedNon-sortable content rendered after entries.
itemIdundefinedRequired when this container is rendered as a sortable entry inside another container.
containerBindableReceives the created core container object.
lockedtrueKeeps 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

OptionDefaultDescription
mode"euclidean"Drag/drop mode for this tree: "euclidean", "progressive", "insertion", or "swap".
strategyResolved from modeAdvanced: 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".
nameGeneratedHuman-readable container name.
animationDefault 100ms animationsReorder, drop, and click-move animation settings. Set to null to disable configured animations.
disableFlipfalseDisables FLIP movement animation.
noDropfalsePrevents the container from being a drop target. Useful for root layout containers.
dropAreafalseTreats the container as an explicit drop area for collision filtering.
callbacksNo persistent mutation defaultRequired state callbacks plus lifecycle and validation hooks. The Svelte adapter never installs Vanilla DOM mutation callbacks.

Callbacks

CallbackWhen 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;
};
PropDefaultDescription
childrenRequiredRendered item content.
itemIdRequiredStable 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.
itemObjectnullOptional 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:

FieldDescription
insertionMarkerInsetLeftOptional left inset for insertion marker width.
insertionMarkerInsetRightOptional 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.