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