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