7.4 KiB
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
horizontalListSortingStrategyinstead 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
- Virtualization: For lists >100 items
- Throttling: Limit drag event frequency
- Memoization: Prevent unnecessary re-renders
- CSS Transforms: Use transform, not position
- 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