Getting Started
snapgrid is a draggable, resizable, repacking grid layout for Svelte 5 — a
react-grid-layout v2 alternative built on
dnd-kit . It keeps the controlled { i, x, y, w, h } model RGL users know,
and adds things RGL can’t do: dragging tiles between grids, nested grids, interop with the wider
dnd-kit ecosystem (sortable lists, boards), keyboard accessibility, and pluggable packing — masonry,
gravity, shelf, or your own compactor.
It’s headless-first — you wire runes-based factories to your own markup — with a turnkey
<GridLayout> for when you just want a grid that works. Requires Svelte 5 (runes).
Know dnd-kit? You already know most of this. snapgrid is dnd-kit plus grid math — a grid
lives in dnd-kit’s <DragDropProvider>, tiles are draggables, the surface is a droppable. If
you’ve touched createDraggable / createSortable, the headless API will feel familiar. New to
dnd-kit? A five-minute skim of its overview is the single easiest win
before you read on.
Install
pnpm
pnpm add @snapgridjs/svelte @dnd-kit/svelte @dnd-kit/dom@dnd-kit/svelte and @dnd-kit/dom are peer dependencies — snapgrid uses your copy, so there’s
nothing to dedupe. See Installation for the full breakdown.
Your first grid
Hold the layout in state
snapgrid is controlled: you own the array, it never mutates it. Each item has an id (i), a
position (x, y), and a size (w, h) in grid units — columns and rows, not pixels. Hold it
in a $state rune.
<script lang="ts">
import type { Layout } from "@snapgridjs/svelte";
let layout = $state<Layout>([
{ i: "a", x: 0, y: 0, w: 4, h: 2 },
{ i: "b", x: 4, y: 0, w: 4, h: 2 },
{ i: "c", x: 8, y: 0, w: 4, h: 2 },
]);
</script>Measure the container width
The grid turns columns into pixels using the container’s width. createContainerWidth measures it
with a ResizeObserver; use its attach attachment on the element whose width should drive the grid.
<script lang="ts">
import { createContainerWidth } from "@snapgridjs/svelte";
const width = createContainerWidth();
</script>
<div {@attach width.attach}>…</div>Host the grid inside a dnd-kit provider
Render inside dnd-kit’s <DragDropProvider>, and let createGridContainer host the grid. Pass it a
getter for its options; it returns an attach attachment + style for your surface element and
the grid’s group — its id, which ties tiles to it.
<!-- Surface.svelte -->
<script lang="ts">
import { createGridContainer, type Layout } from "@snapgridjs/svelte";
import Tile from "./Tile.svelte";
let { layout, width, onLayoutChange }: {
layout: Layout; width: number; onLayoutChange: (l: Layout) => void;
} = $props();
const container = createGridContainer(() => ({ layout, width, onLayoutChange }));
</script>
<div {@attach container.attach} style={container.style}>
{#each layout as item (item.i)}
<Tile id={item.i} group={container.group} />
{/each}
</div>createGridContainer must run inside the <DragDropProvider>. It registers the grid on
dnd-kit’s manager, so the host has to be a child of the provider — like Surface above — not the
component that renders the provider. Call it outside and you’ll get no grid found for group.
Render a tile with createGridItem
Each tile calls createGridItem({ id, group }) for its attach attachment and positioning style —
the way a dnd-kit sortable item declares its list. You bring the element, classes, and content.
<!-- Tile.svelte -->
<script lang="ts">
import { createGridItem } from "@snapgridjs/svelte";
let { id, group }: { id: string; group: string } = $props();
const tile = createGridItem({ id, group });
</script>
<div {@attach tile.attach} style={tile.style} class="tile">{id}</div>Mark the landing spot
createGridPlaceholder(group) tells you where the dragged tile will land (current is null when
idle). Render a marker with its style inside the surface, alongside the tiles, so the target cell
shows as you drag.
<script lang="ts">
import { createGridPlaceholder } from "@snapgridjs/svelte";
// inside Surface — after createGridContainer:
const placeholder = createGridPlaceholder(container.group);
</script>
<!-- inside the container — after the tiles: -->
{#if placeholder.current}
<div class="placeholder" style={placeholder.current.style}></div>
{/if}That’s it — drag it
There’s nothing more to wire. While you drag with a pointer, the tile floats itself above
everything — dnd-kit lifts it into the browser’s top layer, so it can cross grids unclipped — and on
drop it settles into its new cell. Keyboard drags step the tile cell-by-cell in place. Tiles drag,
the rest compact upward, and every committed change flows back through onLayoutChange.
Putting it together
<script lang="ts">
import { DragDropProvider } from "@dnd-kit/svelte";
import { createContainerWidth, type Layout } from "@snapgridjs/svelte";
import Surface from "./Surface.svelte";
const width = createContainerWidth();
let layout = $state<Layout>([
{ i: "a", x: 0, y: 0, w: 4, h: 2 },
{ i: "b", x: 4, y: 0, w: 4, h: 2 },
{ i: "c", x: 8, y: 0, w: 4, h: 2 },
]);
</script>
<!-- DragDropProvider is the outermost element; Surface (which calls
createGridContainer) lives inside it. -->
<div {@attach width.attach}>
<DragDropProvider>
<Surface {layout} width={width.width} onLayoutChange={(next) => (layout = next)} />
</DragDropProvider>
</div><script lang="ts">
import { createGridContainer, createGridPlaceholder, type Layout } from "@snapgridjs/svelte";
import Tile from "./Tile.svelte";
let { layout, width, onLayoutChange }: {
layout: Layout; width: number; onLayoutChange: (l: Layout) => void;
} = $props();
const container = createGridContainer(() => ({
layout,
width,
onLayoutChange,
gridConfig: { cols: 12, rowHeight: 80, margin: [12, 12] },
}));
const placeholder = createGridPlaceholder(container.group);
</script>
<div {@attach container.attach} style={container.style}>
{#each layout as item (item.i)}
<Tile id={item.i} group={container.group} />
{/each}
{#if placeholder.current}
<div class="placeholder" style={placeholder.current.style}></div>
{/if}
</div><script lang="ts">
import { createGridItem } from "@snapgridjs/svelte";
let { id, group }: { id: string; group: string } = $props();
const tile = createGridItem({ id, group });
</script>
<div {@attach tile.attach} style={tile.style} class="tile">{id}</div>Don’t want to wire all that?
The turnkey <GridLayout> bundles the provider, the host, the
tiles, and the placeholder. Pass it layout / width / onLayoutChange and an item snippet —
done. It’s the gentlest on-ramp; drop to the factories for any grid that needs custom markup. Same
engine either way.
<GridLayout {layout} {width} onLayoutChange={(next) => (layout = next)}>
{#snippet item(it)}
<div class="tile">{it.i}</div>
{/snippet}
</GridLayout>Where to next
- Core Concepts — the mental model (controlled state, the dnd-kit DNA, the
groupkey) plus a gotchas cheat-sheet. - Headless usage — the factories in depth: resize handles, the placeholder, and how a dragged tile floats itself.
- Component layer — the turnkey
<GridLayout>when you don’t need custom markup. - Dragging · Resizing · Compaction · Cross-grid · dnd-kit interop · Responsive.
- API Reference — every factory, component, and type.