Ghost renders one temporary SnapSort ghost entry as a div. SnapSort creates the GhostState; the application stores that state and renders the component.

<script lang="ts">
  import { Ghost } from "@snap-engine/snapsort/svelte";
  import type { GhostState } from "@snap-engine/snapsort";

  let { ghost }: { ghost: GhostState } = $props();
</script>

<Ghost {ghost} />

Component Properties

type GhostProps = Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
  ghost: GhostState;
  children?: Snippet;
  className?: string;
  insertionMarker?: InsertionMarkerRectOptions;
};

ghost

Type: GhostState · Required · Read-only input

The immutable state for the entry being rendered. Pass the ghost from a RenderEntry or ghost lifecycle event without constructing or modifying it. The component binds its element to ghost.ghostItem and detaches that binding when the state changes or the component unmounts.

{#if entry.isGhost}
  <Ghost ghost={entry.ghost} />
{/if}

children

Type: Snippet · Default: no content

Custom content rendered inside the ghost div. Use ghost.type or metadata before rendering the component when different roles need different content.

<Ghost ghost={entry.ghost}>
  {entry.ghost.items.length} selected
</Ghost>

insertionMarker

Type: InsertionMarkerRectOptions · Default: stockInsertionMarkerRectOptions

Controls the painted rectangle for an "insertion-marker". It has no effect on other ghost roles and never changes target ranking.

<Ghost
  ghost={entry.ghost}
  insertionMarker={{ thickness: 4, startInset: 8, endInset: 8 }}
/>

All three values must be finite and non-negative. startInset + endInset must not exceed ghost.gap.length.

class, className, style, id, and HTML attributes

Type: standard Svelte HTMLAttributes<HTMLDivElement>

The component renders a div. Native class and the cross-framework className alias are merged with snapsort-ghost. The default id is "spacer". Styles, events, ARIA attributes, and data-* attributes are forwarded.

Consumer styles are appended after SnapSort’s calculated structural styles, so they intentionally override properties such as size or positioning.

<Ghost
  ghost={entry.ghost}
  class="task-ghost"
  style="opacity: 0.75;"
  aria-hidden="true"
/>

GhostState

Type: read-only discriminated union

Narrow ghost.type to access role-specific geometry and locations.

function inspectGhost(ghost: GhostState) {
  if (ghost.type !== "insertion-marker") return;
  console.log(ghost.gap, ghost.isCurrentPlacement);
}

type

Type: "source-spacer" | "target-spacer" | "insertion-marker" | "pointer-preview"

Identifies the role and narrows the union:

  • "source-spacer" preserves a vacated source slot.
  • "target-spacer" reserves a prospective flow-layout destination.
  • "insertion-marker" describes the resolved insertion gap.
  • "pointer-preview" represents the dragged run in an overlay.

session

Type: DragSession · Read-only

The stable session handle that owns this ghost. All ghost roles created during one gesture refer to the same handle.

const isActiveGhost = ghost.session.status === "active";

original, originalItemId, and originalMetadata

Types: Item, ItemId, and ItemMetadata · Read-only

Identify the ordinary Item represented by this ghost anchor. Use originalItemId for application identity and originalMetadata for the application data captured with the Item.

const label = String(ghost.originalMetadata.label ?? ghost.originalItemId);

items and itemIds

Types: readonly Item[] and readonly ItemId[] · Read-only

Parallel arrays describing the complete dragged run in its stable order. A single-item drag has one entry. A spacer may represent one anchor while these properties still describe every participant.

const draggedIds = ghost.itemIds.join(", ");

ghostItem, ghostItemId, and ghostMetadata

Types: Item, ItemId, and ItemMetadata · Read-only

Identify the transient core Item allocated for this rendered ghost. ghostItemId is root-unique and is the correct key for a framework-rendered entry. Do not persist these values as application Items or render ghostItem through the Item component.

{#each tree.entries as entry (entry.itemId)}
  {#if entry.isGhost}<Ghost ghost={entry.ghost} />{/if}
{/each}

location

Type: GhostSlotLocation | GhostOverlayLocation · Read-only

Every location contains container and its read-only containerMetadata. Slot roles also contain a zero-based index; overlay roles do not.

if (ghost.location.type === "slot") {
  console.log(ghost.location.container.name, ghost.location.index);
}

source-spacer, target-spacer, and insertion-marker use GhostSlotLocation. pointer-preview uses GhostOverlayLocation.

rect

Type: GhostRect · Read-only · Spacer and pointer-preview only

A world-space rectangle with x, y, width, and height. The component uses width and height for flow spacers and converts pointer-preview coordinates into the owning Container’s local space.

if (ghost.type !== "insertion-marker") {
  console.log(`${ghost.rect.width} × ${ghost.rect.height}`);
}

gap

Type: InsertionGapSegment · Read-only · Insertion marker only

A zero-thickness world-space segment. orientation is "horizontal" or "vertical"; x and y locate its start and length measures it along the orientation’s main axis.

if (ghost.type === "insertion-marker") {
  const { orientation, length } = ghost.gap;
}

previous and next

Type: InsertionMarkerNeighbor | null · Read-only · Insertion marker only

The ordinary Items immediately adjacent to the insertion boundary. Each neighbor contains item, itemId, itemMetadata, and its frozen world-space border-box rect. Either value is null at an open edge.

if (ghost.type === "insertion-marker") {
  const beforeId = ghost.previous?.itemId ?? null;
  const afterId = ghost.next?.itemId ?? null;
}

isCurrentPlacement

Type: boolean · Read-only · Insertion marker only

true when committing the candidate would preserve the dragged run’s current placement. It can be used to tone down or hide a no-op marker.

<Ghost
  ghost={entry.ghost}
  class:current-placement={
    entry.ghost.type === "insertion-marker" &&
    entry.ghost.isCurrentPlacement
  }
/>

Presentation Helpers

InsertionMarkerRectOptions

interface InsertionMarkerRectOptions {
  readonly thickness: number;
  readonly startInset: number;
  readonly endInset: number;
}

thickness expands equally around the zero-thickness gap. The insets trim the start and end: left/right for a horizontal gap and top/bottom for a vertical gap.

stockInsertionMarkerRectOptions

Type: InsertionMarkerRectOptions

The frozen options used by Ghost when insertionMarker is omitted:

const stockInsertionMarkerRectOptions = {
  thickness: 3,
  startInset: 0,
  endInset: 0,
};

insertionMarkerRect()

function insertionMarkerRect(
  marker: InsertionMarkerState,
  options: InsertionMarkerRectOptions,
): ContainerLocalRect;

Expands a marker’s world-space gap into a rectangle positioned in the destination Container’s local coordinate space. It throws for invalid option values, invalid gap geometry, or an unmounted destination Container.

const rect = insertionMarkerRect(marker, {
  thickness: 4,
  startInset: 6,
  endInset: 6,
});

toContainerLocalRect()

function toContainerLocalRect(
  worldRect: Readonly<GhostRect>,
  container: Container,
): ContainerLocalRect;

Converts a world-space rectangle to coordinates measured from a mounted Container’s padding-box outer edge. Border widths and scroll offsets are included. It throws if the Container has no mounted element.

const localRect = toContainerLocalRect(ghost.rect, ghost.location.container);

ContainerLocalRect

Type: { readonly x: number; readonly y: number; readonly width: number; readonly height: number }

The return value of both presentation helpers, suitable for absolute positioning inside the owning Container.

DOM Contract

Every rendered element has snapsort-ghost, data-snapsort-ghost-entry={ghost.type}, and data-snapsort-ghost-item-count={ghost.items.length}. Pointer previews add data-snapsort-ghost="pointer"; insertion markers add data-snapsort-ghost="insertion".

Appendix

RenderTree Integration

Ghost lifecycle callbacks must synchronously update Svelte state and let Svelte reconcile the DOM. Do not use structural DOM APIs for a framework-rendered ghost.

<script lang="ts">
  import {
    reduceRenderTree,
    type ContainerCallbacks,
    type GhostLifecycleEvent,
  } from "@snap-engine/snapsort";
  import { Ghost, Item } from "@snap-engine/snapsort/svelte";

  function applyGhostEvent(event: GhostLifecycleEvent) {
    tree = reduceRenderTree(tree, event);
  }

  const callbacks = {
    onGhostInsert: applyGhostEvent,
    onGhostMove: applyGhostEvent,
    onGhostRemove: applyGhostEvent,
  } satisfies ContainerCallbacks;
</script>

{#each tree.entries as entry (entry.itemId)}
  {#if entry.isGhost}
    <Ghost ghost={entry.ghost} />
  {:else}
    <Item itemId={entry.itemId}>{entry.value.label}</Item>
  {/if}
{/each}

Ghost Roles and Positioning

Source and target spacers remain in normal flow and inherit the original Item’s measured margins. Pointer previews and insertion markers are absolute, pointer-transparent overlays in their owning Container. Pointer representation and placement feedback are independent, so one session may own both a pointer-preview and an insertion-marker.

onGhostMove reports structural relocation or changed marker presentation; it is not a continuous pointer-move stream. See Drags and Ghosts for the lifecycle and DragSession for dragVisual.