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

Architecture & dnd-kit

snapgrid is a thin, transparent layer over dnd-kit  — not a black box with its own drag engine bolted on. This page is the deep dive: how the pieces fit, and the exact seam a non-React binding (or a direct dnd-kit integration) would build on.

You don’t need any of this to use snapgrid. <GridLayout> and the headless hooks are the whole API for building grids. This page is for the curious — and for anyone evaluating how extensible the library really is.

The layers

snapgrid’s runtime is three layered packages (the optional @snapgridjs/extras packers sit alongside):

@snapgridjs/core pure layout math — geometry, compaction, move/resize, and the drag-session state machine. No dnd-kit. No DOM. No framework. ▲ builds on @snapgridjs/dnd the engine — wires that math to dnd-kit: one per-manager drag/resize/cross-grid engine, the observable GridController render bridge, collision detection, sensors, the snap modifier. Framework-agnostic (depends on @dnd-kit/dom, not React). ▲ builds on @snapgridjs/react the binding — hooks + components that register dnd-kit entities and render from the controller. (depends on @dnd-kit/react)

The line that matters is the middle one. The engine has no framework dependency, so a Vue, Solid, or Svelte binding reuses core + dnd unchanged and only re-implements the thin top layer — registering entities and rendering. That’s why the drag brain lives in its own package.

A grid is just dnd-kit

The grid is another participant in your dnd-kit tree — there’s no parallel drag system:

dnd-kit primitiveits role in a grid
<DragDropProvider>the grid lives inside it — you supply it
useSortableevery tile (wrapped by useGridItem)
useDroppablethe grid surface (created by useGridContainer)
collision detectionresolves which grid the pointer is over — the drop target
Feedback pluginfloats the dragged tile in the top layer (none for keyboard, so it steps in place)

Each tile is a real useSortable carrying a small payload — { kind, itemId, item, group } — so it speaks the same protocol as any dnd-kit sortable. That’s what lets a grid compose with sortable lists and other draggables under one provider, and what makes cross-grid and external drops fall out of dnd-kit’s own collision system rather than bespoke code.

One engine per manager

The drag brain isn’t in React — it’s a single listener set installed on the dnd-kit manager’s monitor. attachEngine(manager) wires dragstart / dragmove / dragend (plus a window key handler for keyboard dragging) and is ref-counted: one engine per manager, no matter how many grids mount. On each event it:

  1. resolves the source grid from the drag payload’s group, and the destination grid from the collision target;
  2. computes the next layout with the pure session functions from corebeginDragdragTocommitLayout for a move, or beginReceive for a tile arriving from another grid;
  3. writes the result into the relevant grid’s controller.

Because which grid the pointer is over comes from one collision oracle, the same handlers drive in-grid moves, cross-grid hand-off, and nested grids — no special cases. (One engine routing every grid also means a multi-grid page processes each drag event once, not once per grid.)

The render bridge: GridController

Each grid has a GridController — a small observable store, framework-agnostic. The engine writes into it; the binding reads from it. It holds:

  • the committed layout (the source of truth),
  • the live drag session (the preview shown while dragging),
  • the published per-grid config (geometry, compactor, gates, callbacks) the engine reads back.

It exposes subscribe plus value-cached snapshots (renderedSnapshot(), itemSnapshot(id)), so a binding can subscribe per tile and re-render only the tiles whose cell actually changed during a drag. In React that’s useSyncExternalStore; another framework plugs in its own reactivity.

That split is the whole trick: the engine produces layout, the controller carries it, the binding renders it — and only the last step is framework-specific.

Collision: the innermost grid wins

Each grid registers a per-droppable collision detector whose priority is base + depth, where depth counts the grid’s ancestor grid containers in the DOM. The deepest grid under the pointer wins, so a drag that starts inside a nested grid stays scoped to it until you drag a tile out — ground truth from DOM nesting, independent of the React tree.

Going lower: driving a grid with the engine

Advanced — binding-author surface. Everything below is the @snapgridjs/dnd API. It’s semi-stable while the first non-React bindings shape it, and @snapgridjs/react is the reference implementation. You never need this to use snapgrid.

A binding does four things. In sketch (the shape, not a runnable app):

import { attachEngine, GridController, registerController } from "@snapgridjs/dnd"; // 1. One engine per dnd-kit manager (ref-counted; detach on teardown). const detach = attachEngine(manager); // 2. A controller per grid — the observable render bridge. const controller = new GridController(gridId, layout, manager); registerController(manager, gridId, controller); // 3. Publish this grid's geometry, compaction, gates, and callbacks (each update). controller.setConfig({ positionParams, gridConfig, compactor, /* gates, callbacks, … */ }); controller.setCommitted(layout); // 4. Render from the controller: subscribe, read snapshots, position tiles by cell. const unsubscribe = controller.subscribe(rerender); const rendered = controller.renderedSnapshot(); // preview while dragging, else committed // …and each tile is a dnd-kit sortable carrying { snapGrid: { kind, itemId, item, group } }.

That is exactly what useGridContainer and useGridItem do, wrapped in React’s reactivity — a Vue/Solid binding is the same four steps in that framework’s idioms. For the full, working version, read the @snapgridjs/react source. A dedicated build-a-binding guide will land once the surface stabilizes.

Last updated on