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

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.

Drag & resize
drag a tile · resize from the corner

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 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

Board.tsx
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> ); }

snapgrid ships no CSS. The tiles and the .placeholder marker are styled by your own classes — see Styling for the hooks it exposes. Want resize handles? Tiles are drag-only until you render them — see Resizing.

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

Last updated on