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

> Explore Instatic's canvas drag-and-drop implementation powered by @dnd-kit. Learn how PointerSensor and collision detection enable seamless item reordering in the visual editor.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: architecture
- Published: 2026-07-28

---

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

```tsx
// 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`](https://github.com/CoreBunch/Instatic/blob/main/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.

```tsx
// 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.

```tsx
// 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:

```tsx
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:

```tsx
<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`](https://github.com/CoreBunch/Instatic/blob/main/SiteExplorerPanel.module.css) and render subtle background changes or borders at the drop location.

```tsx
<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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/AdminCanvasEditorBody.tsx)** sets up the global `DndContext` with a `PointerSensor` (5 px activation distance) and `pointerWithin` collision detection.
- **Each row** in [`SiteExplorerTreeRows.tsx`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/SiteExplorerPanel.module.css).
- **Nested contexts**—including [`SiteExplorerDndScope.tsx`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/SiteExplorerPanel.module.css)—specifically `dropBefore`, `dropAfter`, and `dropInside`—to render background or border indicators that show exactly where an item will land.