Core Concepts
Drag and Drop
Intuitive drag-and-drop interactions for layer management, element positioning, and file imports. Built with accessibility in mind.
Layer Reordering
Drag layers in the layers panel to change z-order.
KeyboardArrow keys to move selected layer up/down
Element Positioning
Drag elements on the canvas to reposition them.
KeyboardArrow keys for precise movement (+ Shift for 10px steps)
File Import
Drop images, SVGs, or design files onto the canvas.
KeyboardCmd/Ctrl + V to paste from clipboard
Template Insertion
Drag components from the integrations panel to canvas.
KeyboardDouble-click to insert at center
Technical Implementation
Built with @dnd-kit for accessibility-first drag-and-drop
// Dependencies // @dnd-kit/core - Core DnD functionality // @dnd-kit/sortable - Sortable lists (layers panel) // @dnd-kit/utilities - Helper functions // Why @dnd-kit? // - Accessibility first (keyboard, screen reader support) // - Touch device support // - Customizable drag overlays // - Multiple drag handles // - Collision detection algorithms // - Small bundle size
Layer Reordering
Drag layers to change stacking order
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
function LayersPanel({ layers, onReorder }) {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5, // Prevent accidental drags
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
function handleDragEnd(event) {
const { active, over } = event
if (active.id !== over?.id) {
const oldIndex = layers.findIndex(l => l.id === active.id)
const newIndex = layers.findIndex(l => l.id === over.id)
onReorder(arrayMove(layers, oldIndex, newIndex))
}
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={layers.map(l => l.id)}
strategy={verticalListSortingStrategy}
>
{layers.map(layer => (
<SortableLayer key={layer.id} layer={layer} />
))}
</SortableContext>
</DndContext>
)
}Canvas Element Dragging
Reposition elements on the canvas
import { useDraggable } from '@dnd-kit/core'
function DraggableElement({ element, zoom }) {
const {
attributes,
listeners,
setNodeRef,
transform,
isDragging,
} = useDraggable({
id: element.id,
data: {
type: 'canvas-element',
element,
},
})
// Convert drag transform to canvas coordinates
const dragOffset = transform ? {
x: transform.x / zoom,
y: transform.y / zoom,
} : { x: 0, y: 0 }
const position = {
x: element.x + dragOffset.x,
y: element.y + dragOffset.y,
}
return (
<g
ref={setNodeRef}
transform={`translate(${position.x}, ${position.y})`}
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
{...attributes}
{...listeners}
>
<ElementRenderer element={element} />
{/* Selection handles */}
{element.selected && (
<SelectionHandles element={element} />
)}
</g>
)
}File Drop Zone
Import files by dropping onto the canvas
import { useDroppable } from '@dnd-kit/core'
import { useCallback, useState } from 'react'
function CanvasDropZone({ children, onFileDrop }) {
const [isDragOver, setIsDragOver] = useState(false)
const { setNodeRef, isOver } = useDroppable({
id: 'canvas-drop-zone',
})
// Handle native file drops (from OS)
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
if (e.dataTransfer.types.includes('Files')) {
setIsDragOver(true)
}
}, [])
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
}, [])
const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
const files = Array.from(e.dataTransfer.files)
const position = screenToCanvas(e.clientX, e.clientY)
for (const file of files) {
await onFileDrop(file, position)
}
}, [onFileDrop])
return (
<div
ref={setNodeRef}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
"relative",
isDragOver && "ring-2 ring-primary ring-inset"
)}
>
{children}
{isDragOver && (
<div className="absolute inset-0 bg-primary/10 flex items-center justify-center">
<p className="text-lg font-medium">Drop files to import</p>
</div>
)}
</div>
)
}Accessibility
Keyboard and screen reader support
// Keyboard navigation for drag and drop
// @dnd-kit provides built-in keyboard support:
// - Tab to focus draggable items
// - Space/Enter to pick up item
// - Arrow keys to move
// - Space/Enter to drop
// - Escape to cancel
// Screen reader announcements
const announcements = {
onDragStart({ active }) {
return `Picked up ${active.data.current.label}. ` +
`Use arrow keys to move, space to drop.`
},
onDragOver({ active, over }) {
if (over) {
return `${active.data.current.label} is over ${over.data.current.label}`
}
return `${active.data.current.label} is no longer over a droppable area`
},
onDragEnd({ active, over }) {
if (over) {
return `${active.data.current.label} was dropped on ${over.data.current.label}`
}
return `${active.data.current.label} was dropped`
},
onDragCancel({ active }) {
return `Dragging ${active.data.current.label} was cancelled`
},
}
// Usage
<DndContext
announcements={announcements}
screenReaderInstructions={{
draggable: 'Press space or enter to pick up. ' +
'Use arrow keys to move. Press space or enter to drop. ' +
'Press escape to cancel.'
}}
>
{/* ... */}
</DndContext>
// Additional ARIA attributes
<div
role="listbox"
aria-label="Layers"
aria-describedby="layers-instructions"
>
<span id="layers-instructions" className="sr-only">
Drag layers to reorder them. Use arrow keys for keyboard control.
</span>
{/* Layer items */}
</div>useDragDrop Hook
Custom hook for drag-and-drop state management
import { useDragDrop } from '@/hooks/use-drag-drop'
function Editor() {
const {
// State
isDragging,
draggedItem,
dropTarget,
activeGuides,
// Handlers
handleDragStart,
handleDragMove,
handleDragEnd,
handleDragCancel,
// File drop
handleFileDrop,
isFileDropActive,
// Config
setSnapEnabled,
setSnapThreshold,
} = useDragDrop({
onReorder: (items) => store.reorderLayers(items),
onMove: (id, position) => store.moveElement(id, position),
onFileDrop: (file, position) => store.addElement(file, position),
})
return (
<DndContext
onDragStart={handleDragStart}
onDragMove={handleDragMove}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<LayersPanel />
<Canvas
guides={activeGuides}
isFileDropActive={isFileDropActive}
onFileDrop={handleFileDrop}
/>
</DndContext>
)
}Touch Device Support
Drag and drop on mobile and tablet
import { TouchSensor, MouseSensor, useSensor, useSensors } from '@dnd-kit/core'
function useDndSensors() {
return useSensors(
// Mouse sensor with activation delay
useSensor(MouseSensor, {
activationConstraint: {
distance: 5, // 5px movement to activate
},
}),
// Touch sensor with hold delay
useSensor(TouchSensor, {
activationConstraint: {
delay: 200, // 200ms hold to activate
tolerance: 5, // Allow 5px movement during delay
},
})
)
}
// Touch-friendly drag handle
function TouchDragHandle({ listeners, attributes }) {
return (
<button
{...listeners}
{...attributes}
className={cn(
"touch-manipulation", // Improve touch responsiveness
"p-3 -m-1", // Larger touch target
"cursor-grab active:cursor-grabbing"
)}
style={{ touchAction: 'none' }} // Prevent scroll during drag
>
<GripVertical className="h-5 w-5" />
</button>
)
}Related Documentation
- Learn about Canvas Engine
- Explore Design Elements
- See Export System