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

Nested grids

A grid can live inside a tile of another grid. Nested grids share the outer grid’s DragDropProvider (one dnd-kit manager), so a tile can be dragged between the inner and outer grids. snapgrid resolves which grid the pointer is over with an innermost-wins rule: a drag that starts inside the inner grid stays scoped to it, and dragging a tile out hands it to the outer grid.

Nested grids cross levels by default. To keep a sub-grid self-contained — tiles can’t leave it — see Contain a nested grid below.

Nested grids
drag the panel by its header; drag tiles between the inner and outer grids

The one thing to wire: the outer tile that hosts the inner grid must drag only from a handle, so a pointer-down inside the inner grid doesn’t drag the whole panel. Put a handleRef (from useGridItem) on the panel’s header — not the grid-wide dragConfig.handle, which would force every outer tile to drag from that selector and disable the ones without it.

import { useGridContainer, useGridItem } from "@snapgridjs/react"; // One provider at the top (yours, or supplied by <GridLayout> / <SnapGridGroup>). // The inner grid does NOT mount its own — it shares this one. function OuterBoard({ outer, width, setOuter }) { const { containerProps, group } = useGridContainer({ layout: outer, width, onLayoutChange: setOuter }); return ( <div {...containerProps}> {outer.map((it) => ( <Tile key={it.i} id={it.i} group={group} /> ))} </div> ); } function Tile({ id, group }) { const { ref, handleRef, style } = useGridItem({ id, group }); if (id === "panel") { return ( <div ref={ref} style={style} className="panel"> {/* The header is the only drag handle, so grabbing an inner tile never drags the panel — and the other outer tiles stay whole-tile draggable. */} <div ref={handleRef} className="panel-head">Nested board</div> <InnerBoard /> {/* an ordinary host — shares the provider above, no new one */} </div> ); } return ( <div ref={ref} style={style} className="tile"> {id} </div> ); } // InnerBoard is an ordinary useGridContainer host. It just does NOT wrap itself // in a <DragDropProvider>, so it joins the outer grid's manager.

How it works

The grids share one dnd-kit manager, so a single collision pass sees both droppable surfaces. When their rects overlap (the pointer is over the inner grid, which sits inside the outer), snapgrid ranks the innermost grid highest, so the drag resolves to the inner grid. Move the pointer out of the inner grid and the outer grid wins — that’s how a tile crosses levels. The panel’s handleRef keeps a pointer-down on an inner tile from arming the panel’s own drag.

A tile that crosses levels is removed from one grid’s layout and added to the other’s — wire both grids’ onLayoutChange, exactly like cross-grid dragging between siblings.

Contain a nested grid (opt out of cross-layer)

To keep a nested grid’s tiles from leaving it — a self-contained widget — give it its own <DragDropProvider>. That puts it on a separate manager, so the outer grid’s collision never sees it and tiles can’t cross the boundary:

import { DragDropProvider } from "@dnd-kit/react"; <div className="panel"> <div ref={handleRef} className="panel-head">Contained board</div> <DragDropProvider> <InnerBoard /> {/* its own manager → isolated; tiles stay put */} </DragDropProvider> </div>;

Limits

Host the inner grid with a per-tile handleRef, not a grid-wide dragConfig.handle. The latter is applied to every tile in the grid, so any tile without a matching handle element becomes undraggable.

  • Size the inner grid to its cell. It measures its own container with useContainerWidth, so it reflows as the outer tile is resized.
Last updated on