Skip to Content
snapgrid is a react-grid-layout v2 alternative built on dnd-kit. Drag, resize, repack, and drag between grids.
DocumentationCore Concepts

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 GridConfigcols, 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 primitiveits role in a grid
<DragDropProvider>the grid lives inside it — you supply it
useDraggableevery tile (wrapped by useGridItem)
useDroppablethe grid surface (created by useGridContainer)
collision detectionresolves which grid the pointer is over — the drop target
Feedback pluginfloats 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 (onDragStartonDragonDragStop, 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.

GotchaIn shortMore
Provider wraps the hostuseGridContainer must run inside <DragDropProvider> (in a child), or you get no grid found for group.Headless
The tile floats itselfNo 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 affordancesNo resize handles, placeholder, or classes — render them with useGridResizeHandle / useGridPlaceholder and bring your own classes.Headless · Styling
Always controlledApply every onLayoutChange or nothing moves; never mutate the layout.above
Feed a clean initial layoutsnapgrid renders your layout verbatim and only re-packs during a drag — pre-compact gaps/overlaps.Migrating
Nested grids share one providerA 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 defaultsrowHeight is 150; width is initialWidth (1280) until measured — gate on mounted to avoid a first-paint reflow under SSR.SSR
Last updated on