Canvas Drag-and-Drop Implementation in Instatic Using @dnd-kit: Architecture Explained

Instatic powers its visual-editor canvas with @dnd-kit by wrapping the editor in a top-level DndContext that uses PointerSensor and pointerWithin collision detection, while each Site Explorer row registers as both a draggable source and a droppable target via useDraggable and useDroppable hooks.

The visual editor in the CoreBunch/Instatic repository relies on the @dnd-kit library to handle reordering and organization directly on the canvas. This canvas drag-and-drop implementation in Instatic using @dnd-kit combines a global context for Site Explorer rows with nested contexts for panel-specific interactions. The following sections break down the sensor configuration, hook usage, DOM bridging, and styling that make the system work.

Top-Level DndContext and Sensor Configuration

The drag-and-drop system is anchored by AdminCanvasEditorBody, which renders a single DndContext around the entire editor body. According to the Instatic source code, this context registers a PointerSensor that requires a 5-pixel movement before activation, preventing accidental drags during normal clicks. It also applies the pointerWithin collision strategy so drop targets are resolved by pointer proximity rather than bounding-box overlap.

// src/admin/layouts/AdminCanvasLayout/AdminCanvasEditorBody.tsx
const canvasDndSensors = useSensors(
  useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
);

<DndContext sensors={canvasDndSensors} collisionDetection={pointerWithin}>
  {/* editor UI … */}
</DndContext>

This global configuration lets users initiate drags anywhere inside the canvas and enables cross-section reordering of Site Explorer items.

Draggable Rows in the Site Explorer

Each folder and item row inside the explorer panel registers itself as a draggable element through the useDraggable hook. In src/admin/pages/site/panels/SiteExplorerPanel/SiteExplorerTreeRows.tsx, the draggable id encodes the row type, section identifier, and node id—such as site-explorer-drag-folder:${sectionId}:${folder.id}. The hook also receives a disabled flag tied to an active rename state and a strongly-typed data object describing the row kind.

// src/admin/pages/site/panels/SiteExplorerPanel/SiteExplorerTreeRows.tsx
const draggable = useDraggable({
  id: `site-explorer-drag-folder:${sectionId}:${folder.id}`,
  disabled: renameActive,
  data: { kind: 'siteExplorerFolder', sectionId, folderId: folder.id, label: folder.name, icon: FolderGlyphIcon },
});

The isDragging flag returned by this hook is forwarded to the TreeRow component so the UI can apply translation or opacity styles while the item is in flight.

Droppable Targets and Drop Position Data

Rows in the same component also act as drop targets. The useDroppable hook mirrors the draggable naming convention but uses a site-explorer-drop- prefix. Its data payload carries positional metadata—section, folder, root index, and item count—that the canvas uses later to compute whether a dropped item should land before, after, or inside the target.

// src/admin/pages/site/panels/SiteExplorerPanel/SiteExplorerTreeRows.tsx
const droppable = useDroppable({
  id: `site-explorer-drop-folder:${sectionId}:${folder.id}`,
  data: { kind: 'siteExplorerFolder', sectionId, folderId: folder.id, rootIndex, itemCount } satisfies SiteExplorerDropData,
});

Because the draggable and droppable identifiers are scoped by prefix, the collision detection system can distinguish between drag sources and drop zones even when they reference the same underlying entity.

Bridging Draggable and Droppable DOM Nodes

To avoid duplicate wrappers, Instatic connects both the draggable and droppable behaviors to a single DOM node. A local setRowRef helper accepts an HTML element and assigns it to both hooks:

function setRowRef(node: HTMLDivElement | null) {
  draggable.setNodeRef(node);
  droppable.setNodeRef(node);
}

This pattern means one <div> functions simultaneously as the drag handle and the drop zone, keeping the tree markup flat and performant. The TreeRow component receives this merged ref directly:

<TreeRow ref={setRowRef} dragging={draggable.isDragging} … />

Visual Drop Indicators with CSS

The TreeRow component applies conditional CSS classes to visualize the computed dropPosition. The styles—dropBefore, dropAfter, and dropInside—are defined in SiteExplorerPanel.module.css and render subtle background changes or borders at the drop location.

<TreeRow
  className={cn(
    dropPosition === 'before' && treeDropStyles.dropBefore,
    dropPosition === 'after' && treeDropStyles.dropAfter,
    dropPosition === 'inside' && treeDropStyles.dropInside,
  )}
/>

By coupling the drop-position logic to specific CSS modules, the canvas provides immediate visual feedback without extra wrapper elements.

Nested DndContexts for Panel Isolation

While the canvas-wide DndContext handles Site Explorer reordering, Instatic isolates other drag-and-drop domains with nested contexts. The DomPanel, for example, creates its own DndContext for rearranging DOM nodes. Because @dnd-kit fully supports nested contexts, the two systems remain independent—interactions inside the DOM tree do not leak into the Site Explorer and vice versa.

The explorer panel itself is wrapped in SiteExplorerDndScope.tsx, which scopes its drag-and-drop logic within the broader canvas context. This layered architecture prevents event collisions and keeps each panel responsible for its own sensors and collision rules.

Test Coverage for Canvas DND

The implementation is validated by a dedicated test suite located under src/__tests__/canvas/. These tests import DndContext and verify that dragging, dropping, and collision detection behave as expected across different panels. For example, src/__tests__/canvas/visualComponentRefInlineBody.test.tsx exercises drag interactions within the visual editor, ensuring that the 5-pixel activation distance and drop indicators function correctly in simulated user flows.

Summary

  • AdminCanvasEditorBody.tsx sets up the global DndContext with a PointerSensor (5 px activation distance) and pointerWithin collision detection.
  • Each row in SiteExplorerTreeRows.tsx registers both useDraggable and useDroppable hooks with uniquely prefixed ids and typed data payloads.
  • A setRowRef helper merges the draggable and droppable node refs onto a single DOM element.
  • TreeRow renders conditional drop-indicator classes (dropBefore, dropAfter, dropInside) from SiteExplorerPanel.module.css.
  • Nested contexts—including SiteExplorerDndScope.tsx and DomPanel—keep drag-and-drop domains isolated across the canvas.
  • The suite under src/__tests__/canvas/ provides automated coverage for the entire drag-and-drop surface.

Frequently Asked Questions

What sensor configuration does Instatic use for canvas drag-and-drop?

Instatic configures a PointerSensor with an activationConstraint distance of 5 pixels inside AdminCanvasEditorBody.tsx. This prevents accidental drags on ordinary clicks while still allowing responsive reordering once the user moves the cursor beyond that threshold.

How does Instatic combine draggable and droppable behavior on the same element?

In SiteExplorerTreeRows.tsx, a helper named setRowRef receives an HTML node and passes it to both draggable.setNodeRef and droppable.setNodeRef. This merges the two refs so that a single <div> acts as both the drag source and the drop target.

Does Instatic use a single DndContext or multiple contexts?

Instatic uses multiple nested DndContext instances. The top-level context in AdminCanvasEditorBody.tsx governs Site Explorer row reordering, while panels such as DomPanel host their own nested contexts to handle DOM-node rearrangement in isolation.

How are drop positions visualized in the Site Explorer panel?

The TreeRow component reads a computed dropPosition value and appends conditional class names from SiteExplorerPanel.module.css—specifically dropBefore, dropAfter, and dropInside—to render background or border indicators that show exactly where an item will land.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →