Skip to content
drag-and-drop logo

Drag and Drop with Pragmatic DnD

drag-and-drop

Implement drag and drop using @atlaskit/pragmatic-drag-and-drop. Use when implementing sortable lists, reorderable items, kanban boards, or any drag-drop interactions. Covers draggable setup, drop targets, edge detection, drag previews, and critical state management patterns to avoid performance ...

SKILL.md

Full skill instructions

Drag and Drop with Pragmatic DnD

This project uses @atlaskit/​pragmatic-drag-and-drop for drag-and-drop functionality.

Required Imports

import { combine } from "@atlaskit/​pragmatic-drag-and-drop/​combine"
import {
  draggable,
  dropTargetForElements,
  type ElementDropTargetEventBasePayload,
} from "@atlaskit/​pragmatic-drag-and-drop/​element/​adapter"
import { pointerOutsideOfPreview } from "@atlaskit/​pragmatic-drag-and-drop/​element/​pointer-outside-of-preview"
import { setCustomNativeDragPreview } from "@atlaskit/​pragmatic-drag-and-drop/​element/​set-custom-native-drag-preview"
import {
  attachClosestEdge,
  type Edge,
  extractClosestEdge,
} from "@atlaskit/​pragmatic-drag-and-drop-hitbox/​closest-edge"
import { DropIndicator } from "@atlaskit/​pragmatic-drag-and-drop-react-drop-indicator/​box"

Critical Pattern: Refs for Volatile State

NEVER put volatile drag state in useEffect dependencies. This causes handlers to re-register on every state change.

// BAD - re-registers handlers on every edge change
const [closestEdge, setClosestEdge] = useState<Edge | null>(null)
useEffect(() => {
  // ...handlers using closestEdge
}, [closestEdge]) // Re-runs on every drag movement!

// GOOD - use ref + useCallback for volatile state
const [closestEdge, setClosestEdge] = useState<Edge | null>(null)
const closestEdgeRef = useRef<Edge | null>(null)

// Wrap in useCallback for lint compliance (exhaustive-deps)
const updateClosestEdge = useCallback((edge: Edge | null) => {
  closestEdgeRef.current = edge
  setClosestEdge(edge) // Still update state for rendering
}, [])

useEffect(() => {
  // ...handlers read closestEdgeRef.current instead
}, [/​* stable deps */, updateClosestEdge]) // Include updateClosestEdge

Import useCallback:

import { useCallback, useEffect, useRef, useState } from "react"

Basic Draggable Item Pattern

function DraggableItem({ item, index, instanceId, onDrop }) {
  const ref = useRef<HTMLDivElement>(null)
  const dragHandleRef = useRef<HTMLButtonElement>(null)
  const [isDragging, setIsDragging] = useState(false)
  const [closestEdge, setClosestEdge] = useState<Edge | null>(null)
  const closestEdgeRef = useRef<Edge | null>(null)

  const updateClosestEdge = useCallback((edge: Edge | null) => {
    closestEdgeRef.current = edge
    setClosestEdge(edge)
  }, [])

  useEffect(() => {
    const element = ref.current
    const dragHandle = dragHandleRef.current
    if (!element || !dragHandle) return

    const itemData = { id: item.id, index, instanceId }

    return combine(
      draggable({
        element: dragHandle,
        getInitialData: () => itemData,
        onDragStart: () => setIsDragging(true),
        onDrop: () => setIsDragging(false),
        onGenerateDragPreview({ nativeSetDragImage }) {
          setCustomNativeDragPreview({
            nativeSetDragImage,
            getOffset: pointerOutsideOfPreview({ x: "16px", y: "8px" }),
            render({ container }) {
              const preview = document.createElement("div")
              preview.style.cssText = `
                background: hsl(var(--background));
                border: 2px solid hsl(var(--border));
                border-radius: 6px;
                padding: 8px 12px;
                font-size: 14px;
                color: hsl(var(--foreground));
                box-shadow: 0 2px 8px rgba(0,0,0,0.15);
              `
              preview.textContent = item.label
              container.appendChild(preview)
            },
          })
        },
      }),
      dropTargetForElements({
        element,
        canDrop: ({ source }) =>
          source.data.instanceId === instanceId && source.data.index !== index,
        getData({ input }) {
          return attachClosestEdge(itemData, {
            element,
            input,
            allowedEdges: ["top", "bottom"],
          })
        },
        onDrag({ source, self }: ElementDropTargetEventBasePayload) {
          if (source.data.index === index) {
            updateClosestEdge(null)
            return
          }

          const edge = extractClosestEdge(self.data)
          const sourceIndex = source.data.index
          if (typeof sourceIndex !== "number") return

          // Hide indicator when it would be redundant
          const isItemBeforeSource = index === sourceIndex - 1
          const isItemAfterSource = index === sourceIndex + 1
          const isDropIndicatorHidden =
            (isItemBeforeSource && edge === "bottom") ||
            (isItemAfterSource && edge === "top")

          updateClosestEdge(isDropIndicatorHidden ? null : edge)
        },
        onDragLeave: () => updateClosestEdge(null),
        onDrop({ source }) {
          const sourceIndex = source.data.index
          if (typeof sourceIndex === "number" && sourceIndex !== index) {
            const edge = closestEdgeRef.current // Read from ref!
            const targetIndex = edge === "top" ? index : index + 1
            const adjustedTargetIndex =
              sourceIndex < targetIndex ? targetIndex - 1 : targetIndex
            onDrop(sourceIndex, adjustedTargetIndex)
          }
          updateClosestEdge(null)
        },
      }),
    )
  }, [item.id, item.label, index, instanceId, onDrop, updateClosestEdge])

  return (
    <div ref={ref} className="relative">
      {closestEdge && <DropIndicator edge={closestEdge} gap="2px" />}
      <div className={isDragging ? "opacity-50" : ""}>
        <button ref={dragHandleRef} type="button" aria-label="Drag to reorder">
          <GripVertical />
        </​button>
        {/​* Item content */​}
      </​div>
    </​div>
  )
}

Instance ID for Multiple Lists

Use Symbol to scope drag operations to a single list:

function SortableList({ items }) {
  const [instanceId] = useState(() => Symbol("list"))
  // Pass instanceId to each item
}

Reorder Handler

const handleDrop = async (sourceIndex: number, targetIndex: number) => {
  const newItems = [...items]
  const [movedItem] = newItems.splice(sourceIndex, 1)
  if (movedItem) {
    newItems.splice(targetIndex, 0, movedItem)
    const updated = newItems.map((item, i) => ({ ...item, position: i }))
    setItems(updated) // Optimistic update
    await saveOrder(updated) // Persist
  }
}

Checklist

  • Refs for volatile state (closestEdge, etc.)
  • Wrap updateClosestEdge in useCallback (lint compliance)
  • Include updateClosestEdge in useEffect deps
  • Instance ID for list scoping
  • Drop indicator with edge detection
  • Custom drag preview
  • Optimistic UI updates

More Other skills

Delegate independent tasks to specialized agents working concurrently with isolated context.

295.4K 206.3K
View

Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.

179.7K 100.1K
View

Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.

179.7K 89.5K
View

A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).

179.7K 76.1K
View

Fundamental design principles for writing maintainable, testable Python code.

40.2K 21.9K
View

Modern responsive layouts using container queries, fluid typography, CSS Grid, and mobile-first strategies.

40.2K 20K
View

Generate Playwright tests from scenarios using interactive browser exploration and validation.

39.7K 18.2K
View

Modern React state management with Redux Toolkit, Zustand, Jotai, and React Query for every state category.

40.2K 14.7K
View

GDPR-compliant data handling with consent management, data subject rights, and privacy controls.

40.2K 14.3K
View

Python error handling patterns including input validation, exception hierarchies, and partial failure handling. Use when implementing validation logic, designing exception strategies, handling batch processing failures, or building robust APIs.

40.2K 13.5K
View

Detailed multi-file refactor planning with safety checks and rollback guidance.

39.7K 13K
View