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

A few ideas explain almost everything about how snapgrid behaves.

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); <GridLayout layout={layout} width={width} onLayoutChange={setLayout}> {/* … */} </GridLayout>;

This means there’s no hidden internal state to fall out of sync with, undo/redo is trivial (keep your own history), and you can persist or transform the layout however you like. The trade-off is that 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 in; you should do the same (always produce a new array in onLayoutChange).

The layout model

A Layout is LayoutItem[]. Every item is positioned and sized in grid units, not pixels:

interface LayoutItem { i: string; // unique id — matches the child's React key 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 minH?: number; maxH?: number; static?: boolean; // never moves, never resizes; others flow around it isDraggable?: boolean; // per-item override of the grid default isResizable?: boolean; resizeHandles?: ResizeHandleAxis[]; // per-item handle set }

Pixels come from the GridConfig (cols, rowHeight, margin, containerPadding, maxRows) plus the measured container width. The geometry helpers turn (x, y, w, h) into left/top/width/height and back.

Two layers: drop-in and headless

snapgrid is headless-first with a thin component shell on top. Use whichever fits:

  • Components: <GridLayout>, <GridItem>, <GridPlaceholder>, <GridDragOverlay>, <ResponsiveGridLayout>. Keyed children, default markup and class names. Great for the common case and the fastest way to ship.
  • Headless: SnapGridProvider + useGridContainer / useGridItem / useGridResizeHandle / useGridPlaceholder / useGridDragOverlay. You render every element; snapgrid supplies refs, positioning styles, and state. No imposed DOM or CSS.

<GridLayout> is literally SnapGridProvider + the hooks wired to default markup, so you can start with the component and drop to hooks for any piece without changing engines. See Headless usage.

Compaction (packing)

After every move or resize, the layout is re-packed by a Compactor. The compactor decides where items settle:

  • vertical (default): items fall upward, like react-grid-layout.
  • horizontal: items pack to the left.
  • none: free positioning; items stay where you put them and may overlap.
  • masonry / gravity / shelf: from @snapgridjs/extras, for variable-height packing.

You can swap the compactor at runtime (it’s just a prop) and even write your own. See Compaction & packing.

The interaction model

snapgrid drives positioning itself rather than letting dnd-kit move the element (it drags with feedback: "none"). While you drag:

  • the active tile is hidden in place and a floating overlay (a body portal) follows the pointer, so it can cross between grids without being clipped;
  • a placeholder marks the cell where the tile will land after compaction;
  • other tiles animate to their reflowed positions in real time.

On drop, the previewed layout becomes the committed layout and flows back through onLayoutChange. Lifecycle callbacks (onDragStart, onDrag, onDragStop, and the resize equivalents) fire throughout. See Events & lifecycle.

Last updated on