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

dnd-kit interop

A snapgrid grid is dnd-kit. Every tile is a real useSortable, so a grid composes with the rest of the dnd-kit ecosystem under one DragDropProvider: drag a card from a useSortable tray into the grid (it lands at a real cell, with compaction), drag a tile back out, or reorder the tray — all in the same drag.

Sortable ↔ grid
drag a widget into the grid · drag a tile out to the tray · reorder the tray

Managed vs. onDragOver

For pure snapgrid grids you don’t write any drag handlers — the grid’s managed engine drives in-grid moves, cross-grid hand-off, and resizing, and reports results through onLayoutChange. That still applies here: an in-grid move falls through to the engine untouched.

Interop with a foreign dnd-kit container (a sortable list, a kanban column) is different. The two sides have separate state shapes — a grid is a Layout, a list is a string[] — so you own the seam between them, in the provider’s onDragOver. snapgrid gives you the reducers; you wire them to the source/target types you care about.

Reduce the cross-parent move live in onDragOver, not on drop. dnd-kit reparents the dragged DOM node mid-drag; if you only update state on drop, React’s next render fights that reparent and throws removeChild. Reducing live keeps React in sync with dnd-kit the whole drag.

One provider

The grid and the foreign sortable must share a single dnd-kit manager, so they live under one DragDropProvider (or snapgrid’s SnapGridGroup, a thin wrapper). Import the dnd-kit primitives from @snapgridjs/react so everything resolves the same dnd-kit instance — a second copy of @dnd-kit/react is a separate provider context and its drags never reach the grid.

import { DragDropProvider } from "@dnd-kit/react"; import { useSortable } from "@dnd-kit/react/sortable"; import { move } from "@dnd-kit/helpers"; import { defaultGridConfig, removeItemWithCompactor, snapMove, toPositionParams, useGridContainer, useGridItem, verticalCompactor, } from "@snapgridjs/react";

The grid accepts foreign sortables

By default a grid only accepts grid tiles and external-drop draggables. Opt a foreign sortable in with the accept option on useGridContainer — it widens which sources resolve the grid as a drop target (the built-in checks and the nested-grid ancestry guard still apply). You drive the actual receive yourself with snapMove (below).

const { containerProps, group } = useGridContainer({ layout, width, onLayoutChange: setGrid, // Accept the tray's cards (type "tray-card") as drop targets. accept: (source) => source.type === "tray-card", });

The tray card is a plain useSortable. Give it a type you’ll branch on, and have it accept grid tiles so a tile can be dropped onto it:

function TrayCard({ id, index }: { id: string; index: number }) { const { ref } = useSortable({ id, index, group: "tray", type: "tray-card", accept: ["tray-card", "grid-item"], }); return <div ref={ref}>{id}</div>; }

snapgrid tiles use type: "grid-item" and the grid container uses type: "grid". Branch on those in onDragOver alongside your own types. Item ids must be unique across the whole provider — a card in the tray and a tile in the grid can’t share an id.

The onDragOver reducer

Each seam is one branch keyed on source.typetarget.type. snapMove brings an item into the grid; removeItemWithCompactor takes one out; dnd-kit’s move reorders the list. In-grid moves fall through to the managed engine.

const positionParams = toPositionParams({ ...defaultGridConfig, ...GRID }, width); <DragDropProvider onDragOver={(event) => { const { source, target } = event.operation; if (!source || !target) return; const id = String(source.id); // Tray card → grid: drop it out of the tray, into the layout at the hovered cell. if (source.type === "tray-card" && (target.type === "grid" || target.type === "grid-item")) { setTray((t) => t.filter((x) => x !== id)); setGrid((g) => snapMove(g, event, { positionParams, compactor: verticalCompactor, defaultItem: { w: 2, h: 2 }, }), ); } // Grid tile → tray: remove it AND re-pack the hole, then insert into the tray. else if (source.type === "grid-item" && target.type === "tray-card") { setGrid((g) => removeItemWithCompactor(g, id, { compactor: verticalCompactor, cols: GRID.cols }), ); setTray((t) => (t.includes(id) ? t : insertBefore(t, id, String(target.id)))); } // Reorder within the tray. else if (source.type === "tray-card" && target.type === "tray-card") { setTray((t) => move(t, event)); } // In-grid moves fall through — the grid's engine drives them. }} > {/* grid host + tray, rendered inside the provider */} </DragDropProvider>

Use removeItemWithCompactor, not layout.filter(...), to take a tile out. A plain filter leaves a hole where the tile was; removeItemWithCompactor removes it and re-packs the remainder so the grid stays tidy — the same compaction the managed engine applies on a cross-grid move.

snapMove

snapMove is the 2-D grid analog of dnd-kit’s move(items, event) — call it from onDragOver (or onDragEnd) to place the dragged item into a Layout at the cell under the pointer, with compaction, and get the new layout back.

function snapMove(layout: Layout, event: SnapMoveEvent, ctx: SnapMoveContext): Layout; interface SnapMoveContext { positionParams: PositionParams; // the grid's geometry — build with toPositionParams compactor: Compactor; // e.g. verticalCompactor defaultItem?: { w: number; h: number }; // size for a foreign source (default 1×1) gridRect?: { left: number; top: number }; // override the grid's client rect (else read from the target) }

The item’s size comes from a dragged grid tile’s payload when present (so a tile keeps its size as it crosses), else from defaultItem for a foreign card. Build positionParams from the same width and config the grid renders at, so the dropped cell lines up with what the reader sees.

The drag preview

There’s no overlay to wire up — each draggable floats itself, and the preview stays under the pointer across the hand-off. Grid tiles are positioned with left/top (not a transform) precisely so dnd-kit’s self-float reads each tile’s true rect; when a tile becomes a tray card mid-drag (or vice versa), the floating preview tracks the pointer cleanly instead of jumping. This is the same mechanism dnd-kit’s own flow-positioned sortables use to hand off between lists.

Beyond a tray

The same three reducers cover any grid ⇄ sortable seam — a kanban board whose columns are sortable lists and whose cards drop into a dashboard grid, a sidebar of blocks, a list view that toggles to a grid view. Branch on whatever types your containers carry; snapMove / removeItemWithCompactor handle the grid side, dnd-kit’s move handles the list side.

Last updated on