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-svelte

Import

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

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.

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

APIDescription
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.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;
  children: Snippet;
  container?: Container;
  locked?: boolean;
  className?: string;
  metadata?: Record<string, unknown>;
};
PropDefaultDescription
configRequiredSnapSort container configuration.
childrenRequiredItem and nested container content.
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: snapsort-mode-euclidean, snapsort-mode-progressive, or snapsort-mode-insertion.

Config

OptionDefaultDescription
mode"euclidean"Drag/drop mode for this tree: "euclidean", "progressive", or "insertion".
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.
callbacksDefault DOM callbacksHooks for DOM, framework state, and custom ghost behavior.

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.
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;
};
PropDefaultDescription
childrenRequiredRendered 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.
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
itemIdStable item ID used by moveItem, removeItem, and framework state callbacks.
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 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.