skills/implementing-drag-drop/references/dnd-patterns.md

7.4 KiB

Drag-and-Drop Patterns

Table of Contents

Sortable Lists

Vertical List Pattern

Most common drag-and-drop pattern for priority ordering, task lists, and sequential content.

Implementation with dnd-kit:

import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable';

function VerticalSortableList({ items, onReorder }) {
  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    })
  );

  function handleDragEnd(event) {
    const { active, over } = event;

    if (active.id !== over.id) {
      const oldIndex = items.findIndex(item => item.id === active.id);
      const newIndex = items.findIndex(item => item.id === over.id);
      onReorder(arrayMove(items, oldIndex, newIndex));
    }
  }

  return (
    <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
      <SortableContext items={items} strategy={verticalListSortingStrategy}>
        {/* Sortable items here */}
      </SortableContext>
    </DndContext>
  );
}

Horizontal List Pattern

Used for tab reordering, carousel management, and timeline elements.

Key Differences:

  • Use horizontalListSortingStrategy instead of vertical
  • Adjust drag handle positioning (typically on edges)
  • Consider touch scrolling conflicts on mobile
import { horizontalListSortingStrategy } from '@dnd-kit/sortable';

// In SortableContext:
<SortableContext items={items} strategy={horizontalListSortingStrategy}>
  {/* Horizontal items */}
</SortableContext>

Drag Handle Pattern

Provides explicit drag affordance while keeping content interactive.

import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

function SortableItem({ id, children }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
  };

  return (
    <div ref={setNodeRef} style={style}>
      <button
        className="drag-handle"
        {...attributes}
        {...listeners}
        aria-label="Drag to reorder"
      >
        ⋮⋮
      </button>
      {children}
    </div>
  );
}

Grid Layouts

2D Grid Pattern

For dashboard widgets, image galleries, and card layouts.

import { rectSortingStrategy } from '@dnd-kit/sortable';

function GridLayout({ items, columns = 3 }) {
  return (
    <SortableContext items={items} strategy={rectSortingStrategy}>
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: `repeat(${columns}, 1fr)`,
          gap: '1rem'
        }}
      >
        {items.map(item => (
          <SortableGridItem key={item.id} {...item} />
        ))}
      </div>
    </SortableContext>
  );
}

Masonry Layout Pattern

For Pinterest-style layouts with variable heights.

Considerations:

  • Calculate positions dynamically
  • Handle reflow on drop
  • Optimize for performance with many items
// Use custom collision detection for masonry
function masonryCollisionDetection(args) {
  // Custom logic to handle variable heights
  // Consider item positions and sizes
  return closestCenter(args);
}

Nested Containers

Parent-Child Dragging

For tree structures, nested lists, and hierarchical content.

function NestedSortable({ items, depth = 0 }) {
  const maxDepth = 3; // Prevent infinite nesting

  return (
    <SortableContext items={items}>
      {items.map(item => (
        <div key={item.id} style={{ marginLeft: depth * 20 }}>
          <SortableItem {...item} />
          {item.children && depth < maxDepth && (
            <NestedSortable items={item.children} depth={depth + 1} />
          )}
        </div>
      ))}
    </SortableContext>
  );
}

Auto-Scrolling

Edge Detection Pattern

Automatically scroll when dragging near container edges.

import { AutoScrollActivator } from '@dnd-kit/auto-scroll';

function ScrollableList() {
  const autoScrollOptions = {
    canScroll: (element) => true,
    threshold: {
      x: 0.2, // 20% from edge
      y: 0.2,
    },
    maxSpeed: 10,
    acceleration: 10,
  };

  return (
    <DndContext autoScroll={autoScrollOptions}>
      {/* Scrollable content */}
    </DndContext>
  );
}

Viewport Scrolling

For full-page draggable interfaces.

// Enable window scrolling during drag
const sensors = useSensors(
  useSensor(PointerSensor, {
    activationConstraint: {
      distance: 8, // Prevent accidental drags
    },
  })
);

// Auto-scroll viewport
useEffect(() => {
  if (isDragging) {
    // Custom scroll logic based on cursor position
  }
}, [isDragging, cursorPosition]);

Multi-Select Dragging

Selection State Pattern

Allow dragging multiple items simultaneously.

function MultiSelectDraggable() {
  const [selectedIds, setSelectedIds] = useState(new Set());

  function handleDragStart(event) {
    const { active } = event;

    if (!selectedIds.has(active.id)) {
      // If dragging unselected item, clear selection
      setSelectedIds(new Set([active.id]));
    }
    // Otherwise drag all selected items
  }

  function handleDragEnd(event) {
    const { active, over } = event;

    if (over && selectedIds.size > 0) {
      // Move all selected items relative to drop position
      const itemsToMove = Array.from(selectedIds);
      // Reorder logic here
    }
  }

  return (
    <DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
      {/* Multi-selectable items */}
    </DndContext>
  );
}

Visual Feedback for Multi-Select

function MultiSelectItem({ id, isSelected, isDragging }) {
  const style = {
    backgroundColor: isSelected ? 'var(--color-primary-100)' : 'white',
    opacity: isDragging && isSelected ? 0.5 : 1,
    border: isSelected ? '2px solid var(--color-primary)' : '1px solid var(--color-border)',
  };

  return <div style={style}>{/* Content */}</div>;
}

Common Patterns Summary

Pattern Selection Guide

Use Case Pattern Key Considerations
Task lists Vertical sortable Drag handles, keyboard nav
Tabs Horizontal sortable Touch scrolling conflicts
Dashboard Grid layout Responsive columns
File browser Nested containers Depth limits
Kanban Multi-container Drop zones, auto-scroll
Batch operations Multi-select Visual selection state

Performance Tips

  1. Virtualization: For lists >100 items
  2. Throttling: Limit drag event frequency
  3. Memoization: Prevent unnecessary re-renders
  4. CSS Transforms: Use transform, not position
  5. Will-change: Hint browser about animations

Accessibility Requirements

Every pattern must include:

  • Keyboard navigation support
  • Screen reader announcements
  • Focus management
  • Alternative UI options
  • Clear visual feedback

Mobile Considerations

  • Long press (300ms) to initiate drag
  • Prevent scroll during drag
  • Larger touch targets (44px minimum)
  • Handle gesture conflicts
  • Test on actual devices