Core Concepts
Six ideas explain almost everything snapgrid does. Read this once and the guides will click.
1. The layout is controlled
snapgrid never holds your layout. You keep the array in state; snapgrid reads it, previews changes
while you drag, and hands you the next array through onLayoutChange when an interaction commits.
const [layout, setLayout] = useState<Layout>(initial);No hidden internal state to fall out of sync with, undo/redo is trivial (keep your own history), and
you persist or transform the layout however you like. The trade-off: you must apply the change.
If you don’t call setLayout, nothing moves.
Treat the layout as immutable. snapgrid clones internally and never mutates the array or items
you pass; produce a new array in onLayoutChange (which setLayout does for you).
2. The layout model
A Layout is LayoutItem[]. Items are positioned and sized in grid units — columns and rows,
not pixels:
interface LayoutItem {
i: string; // unique id — matches the tile's React key / group lookup
x: number; // column (0-based)
y: number; // row (0-based)
w: number; // width in columns
h: number; // height in rows
minW?: number; maxW?: number; // resize limits (maxW defaults to the column count)
minH?: number; maxH?: number;
static?: boolean; // never moves or resizes; others flow around it
isDraggable?: boolean; // per-item override of the grid default
isResizable?: boolean;
isBounded?: boolean; // per-item override of dragConfig.bounded
resizeHandles?: ResizeHandleAxis[]; // per-item handle set
}Pixels come from the GridConfig — cols, rowHeight (default 150), margin (default
[10, 10]), containerPadding (falls back to margin), maxRows — plus the measured container
width. snapgrid’s geometry turns (x, y, w, h) into left/top/width/height and back.
3. It’s built on dnd-kit
This is the most important idea, and the one that makes snapgrid different. snapgrid does not ship its own drag engine — it composes dnd-kit (the 0.4 framework-agnostic line). The grid is just another participant in your dnd-kit tree:
| dnd-kit primitive | its role in a grid |
|---|---|
<DragDropProvider> | the grid lives inside it — you supply it |
useDraggable | every tile (wrapped by useGridItem) |
useDroppable | the grid surface (created by useGridContainer) |
| collision detection | resolves which grid the pointer is over — the drop target |
Feedback plugin | floats the dragged tile itself in the top layer (default feedback); set to none for keyboard, so the tile steps in place |
<DragOverlay> | optional — a separate floating preview; snapgrid doesn’t need one (the tile floats itself), but it’s re-exported as an escape hatch |
snapgrid’s own job is the grid part on top: the coordinate space, the packing, and the controlled layout. The brains of that (geometry, the collision cascade, compaction) come from react-grid-layout ’s engine; the interaction is all dnd-kit.
Know dnd-kit and you already know 80% of snapgrid. The headless API is dnd-kit’s idiom — you
bring a DragDropProvider, tiles declare a group the way useSortable
declares a list, and a grid composes with sortables and other draggables under one provider (see
dnd-kit interop). If dnd-kit is new to you, a five-minute skim of
its docs is the easiest possible head start.
→ Going deeper — the layered packages, the one-per-manager engine, the observable render bridge, and the seam for driving a grid without React: Architecture & dnd-kit.
4. Headless first, with a turnkey escape hatch
The primary API is a set of hooks you wire to your own markup. A thin <GridLayout> component
wraps those same hooks for the common case.
// Headless — your markup, you own every element.
function Board({ layout, width, onLayoutChange }) {
return (
<DragDropProvider> {/* dnd-kit's provider — you supply it */}
<Surface layout={layout} width={width} onLayoutChange={onLayoutChange} />
</DragDropProvider>
);
}
function Surface({ layout, width, onLayoutChange }) {
// useGridContainer is the grid host: it owns the grid and returns its `group`.
const { containerProps, group } = useGridContainer({ layout, width, onLayoutChange });
return (
<div {...containerProps}>
{layout.map((it) => <Tile key={it.i} id={it.i} group={group} />)}
</div>
);
}
function Tile({ id, group }) {
const { ref, style } = useGridItem({ id, group }); // resolves its grid by `group`
return <div ref={ref} style={style}>{id}</div>;
}// Turnkey — the same thing, wired for you. <GridLayout> bundles the provider,
// the container, the items, and the placeholder.
<GridLayout layout={layout} width={width} onLayoutChange={setLayout}>
{layout.map((item) => <div key={item.i}>{item.i}</div>)}
</GridLayout>Two idioms, on purpose. The headless hooks speak dnd-kit — reach for them to compose with
the dnd-kit ecosystem or to own every element. The turnkey <GridLayout>
speaks react-grid-layout — a controlled, ready-made component for when the headless wiring is more
than you need. They share one engine, so you can start turnkey and drop any single grid down to
the hooks. If the hooks ever feel like too much, that component is your escape hatch.
5. How a tile finds its grid: group
There’s no per-grid React context. A tile resolves its grid by a group key — exactly like a
dnd-kit sortable item declares which list it’s in. useGridContainer returns the grid’s group (its
id); you pass it to each useGridItem({ id, group }), useGridResizeHandle({ id, handle, group }), and
useGridPlaceholder(group). This is what lets several grids share one provider and still keep their
items straight — so ids must be unique across grids that share a provider.
6. The interaction model
While you drag (with a pointer):
- the dragged tile floats itself above everything — dnd-kit lifts it into the browser’s top layer — so it can cross grids unclipped;
- dnd-kit’s collision system resolves which grid the pointer is over (the drop target);
- snapgrid maps the pointer to a cell and re-packs, so a placeholder marks where the tile will land while the other tiles animate to their reflowed positions.
On drop, the previewed layout becomes the committed layout and flows back through onLayoutChange.
The full lifecycle (onDragStart → onDrag → onDragStop, and the resize equivalents) fires
throughout — see Events & lifecycle.
Keyboard drags have no floating preview: the tile stays visible and steps cell-by-cell with the arrow keys (Enter/Space to pick up and drop, Escape to cancel). Accessibility comes free from dnd-kit’s keyboard sensor.
Compaction (packing)
After every move, resize, insert, or remove, the layout is re-packed by a Compactor:
verticalCompactor(default) — items fall upward, like react-grid-layout;horizontalCompactor— items pack to the left;noCompactor— free positioning; items stay put and may overlap;masonry/gravity/shelf/wrap/ fast variants — from@snapgridjs/extras.
It’s just a value you pass and can swap at runtime. See Compaction & packing.
Gotchas cheat-sheet
The handful of things that trip people up. Each is spelled out in its guide; this is the quick scan.
| Gotcha | In short | More |
|---|---|---|
| Provider wraps the host | useGridContainer must run inside <DragDropProvider> (in a child), or you get no grid found for group. | Headless |
| The tile floats itself | No overlay to render — a dragged tile lifts into the top layer on its own. Style its drag look via the tile’s [data-dragging]. | Headless · Styling |
| Headless tiles have no affordances | No resize handles, placeholder, or classes — render them with useGridResizeHandle / useGridPlaceholder and bring your own classes. | Headless · Styling |
| Always controlled | Apply every onLayoutChange or nothing moves; never mutate the layout. | above |
| Feed a clean initial layout | snapgrid renders your layout verbatim and only re-packs during a drag — pre-compact gaps/overlaps. | Migrating |
| Nested grids share one provider | A grid nested in another shares the outer’s <DragDropProvider>; innermost-wins collision resolves the inner grid, so tiles cross levels. Give a sub-grid its own provider to contain it. | Nesting |
| Mind the defaults | rowHeight is 150; width is initialWidth (1280) until measured — gate on mounted to avoid a first-paint reflow under SSR. | SSR |