Getting Started
snapgrid is a draggable, resizable, repacking grid layout for React — 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 hooks to your own markup — with a turnkey <GridLayout> for when
you just want a grid that works.
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 useDraggable / useSortable, 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/react @dnd-kit/react @dnd-kit/dom@dnd-kit/react 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.
import { useState } from "react";
import type { Layout } from "@snapgridjs/react";
const [layout, setLayout] = useState<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 },
]);Measure the container width
The grid turns columns into pixels using the container’s width. useContainerWidth measures it with
a ResizeObserver; attach its containerRef to the element whose width should drive the grid.
import { useContainerWidth } from "@snapgridjs/react";
const { width, containerRef } = useContainerWidth();Host the grid inside a dnd-kit provider
Render inside dnd-kit’s <DragDropProvider>, and let useGridContainer host the grid. It returns
containerProps for your surface element and the grid’s group — its id, which ties tiles to it.
import { DragDropProvider } from "@dnd-kit/react";
import { useGridContainer } from "@snapgridjs/react";
function Surface({ layout, width, onLayoutChange }) {
const { containerProps, group } = useGridContainer({ layout, width, onLayoutChange });
return (
<div {...containerProps}>
{layout.map((item) => (
<Tile key={item.i} id={item.i} group={group} />
))}
</div>
);
}useGridContainer 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 useGridItem
Each tile calls useGridItem({ id, group }) for its ref and positioning style — the way a dnd-kit
sortable item declares its list. You bring the element, classes, and content.
import { useGridItem } from "@snapgridjs/react";
function Tile({ id, group }) {
const { ref, style } = useGridItem({ id, group });
return (
<div ref={ref} style={style} className="tile">
{id}
</div>
);
}Mark the landing spot
useGridPlaceholder(group) tells you where the dragged tile will land (or null when idle). Render a
marker with its style inside the surface, alongside the tiles, so the target cell shows as you drag.
import { useGridPlaceholder } from "@snapgridjs/react";
// inside Surface — after useGridContainer:
const placeholder = useGridPlaceholder(group);
// inside the container — after the tiles:
{placeholder && <div className="placeholder" style={placeholder.style} />}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
import { DragDropProvider } from "@dnd-kit/react";
import {
type Layout,
useContainerWidth,
useGridContainer,
useGridItem,
useGridPlaceholder,
} from "@snapgridjs/react";
import { useState } from "react";
export function Board() {
const { width, containerRef } = useContainerWidth();
const [layout, setLayout] = useState<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 },
]);
// DragDropProvider is the outermost element; Surface (which calls
// useGridContainer) lives inside it.
return (
<div ref={containerRef}>
<DragDropProvider>
<Surface layout={layout} width={width} onLayoutChange={setLayout} />
</DragDropProvider>
</div>
);
}
function Surface({ layout, width, onLayoutChange }) {
const { containerProps, group } = useGridContainer({
layout,
width,
onLayoutChange,
gridConfig: { cols: 12, rowHeight: 80, margin: [12, 12] },
});
const placeholder = useGridPlaceholder(group);
return (
<div {...containerProps}>
{layout.map((item) => (
<Tile key={item.i} id={item.i} group={group} />
))}
{placeholder && <div className="placeholder" style={placeholder.style} />}
</div>
);
}
function Tile({ id, group }: { id: string; group: string }) {
const { ref, style } = useGridItem({ id, group });
return (
<div ref={ref} style={style} className="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 keyed children —
done. It’s the gentlest on-ramp; drop to the hooks for any grid that needs custom markup. Same
engine either way.
<GridLayout layout={layout} width={width} onLayoutChange={setLayout}>
{layout.map((item) => (
<div key={item.i} className="tile">
{item.i}
</div>
))}
</GridLayout>Coming from react-grid-layout?
You’ll feel at home — same controlled layout / onLayoutChange, same { i, x, y, w, h } items,
same breakpoints. The <GridLayout> component layer mirrors RGL v2’s model, so migrating is a quick
first step, not a rewrite — though not a literal drop-in (you re-author styling, and any hook usage
moves to headless). See Migrating from RGL.
Where to next
- Core Concepts — the mental model (controlled state, the dnd-kit DNA, the
groupkey) plus a gotchas cheat-sheet. - Headless usage — the hooks 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 hook, component, and type.