Drag and Drop
Intuitive interaction patterns for reordering, organizing, and transferring content through direct manipulation. Essential for productivity tools, builders, and content management interfaces.
Reorderable lists
Allow users to reorganize items by dragging and dropping. Provides immediate visual feedback during the drag operation.
Project tasks
Research competitors
Analyze top 5 competitors
Design wireframes
Create low-fidelity mockups
User testing
Conduct 5 user interviews
Finalize designs
Prepare handoff documentation
Drag items to reorder (demo only)
File drop zone
Designated areas where users can drag and drop files for upload. Provides clear visual feedback during the drag operation.
Drop files here to upload
or click to browse
Kanban board
Visual task management using columns and draggable cards. Move items between stages to track progress.
To Do (2)
Set up project
Initialize repo and tooling
Research
Gather requirements and constraints
In Progress (1)
Design mockups
Create high-fidelity designs
Done (1)
Kickoff meeting
Align with stakeholders
Use cases
Reorderable lists
Drag items to re-prioritize tasks, sort tables, or organize content
File uploads
Drag and drop files onto designated zones for quick uploads with visual feedback
Kanban boards
Move cards between columns to track workflow state and progress
Builder interfaces
Compose layouts by dragging components onto a canvas or container
Drag handle anatomy
Interaction states
| State | Appearance | Notes |
|---|---|---|
| Idle | Normal cursor, no visual feedback | Default state; ready for interaction |
| Hover | Border highlight, cursor changes to grab | Indicates item is draggable on mouseover |
| Dragging | Opacity reduction, cursor changes to grabbing | Visual clone follows cursor; original item dims |
| Drag over | Drop zone highlight, dashed border | Indicates valid drop target area |
| Drop | Smooth transition to new position | Immediate reordering with animation |
Usage guidelines
- ·Provide clear visual affordances for draggable items (e.g., grip handles)
- ·Show immediate feedback during drag (ghost image, opacity change)
- ·Indicate valid drop targets with highlighting
- ·Support keyboard alternatives for accessibility
- ·Don't require drag for essential tasks (provide alternative controls)
- ·Avoid drag in small click targets (hard to initiate)
- ·Don't allow dropping in invalid areas without feedback
- ·Avoid excessive drag distance for simple reordering
Implementation
"use client";
import { useState, useRef, DragEvent } from "react";
import { cn } from "@/lib";
// ── Reorderable List ───────────────────────────────────────
interface DraggableItem {
id: string;
title: string;
description?: string;
}
interface DraggableListProps<T extends DraggableItem> {
items: T[];
onChange: (items: T[]) => void;
renderItem: (item: T) => React.ReactNode;
className?: string;
}
export function DraggableList<T extends DraggableItem>({
items,
onChange,
renderItem,
className,
}: DraggableListProps<T>) {
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
function handleDragStart(e: DragEvent<HTMLDivElement>, index: number) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", index.toString());
setDraggedIndex(index);
}
function handleDragOver(e: DragEvent<HTMLDivElement>, targetIndex: number) {
e.preventDefault();
if (draggedIndex === null || draggedIndex === targetIndex) return;
const newItems = [...items];
const [draggedItem] = newItems.splice(draggedIndex, 1);
newItems.splice(targetIndex, 0, draggedItem);
onChange(newItems);
setDraggedIndex(targetIndex);
}
function handleDragEnd() {
setDraggedIndex(null);
}
return (
<div className={cn("space-y-2", className)}>
{items.map((item, index) => (
<div
key={item.id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={(e) => handleDragOver(e, index)}
onDragEnd={handleDragEnd}
className={cn(
"p-4 rounded-lg border cursor-grab active:cursor-grabbing transition-all",
"bg-[rgb(var(--surface))] border-[rgb(var(--border))]",
"hover:border-[rgb(var(--accent))] hover:bg-[rgb(var(--surface-raised))]",
draggedIndex === index && "opacity-50 border-[rgb(var(--accent))]",
)}
>
{renderItem(item)}
</div>
))}
</div>
);
}
// ── Drop Zone ──────────────────────────────────────────────
interface DropZoneProps {
onDrop: (files: File[]) => void;
children: React.ReactNode;
className?: string;
multiple?: boolean;
accept?: string;
}
export function DropZone({
onDrop,
children,
className,
multiple = true,
accept,
}: DropZoneProps) {
const [isOver, setIsOver] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
function handleDrop(e: DragEvent<HTMLDivElement>) {
e.preventDefault();
setIsOver(false);
const files = Array.from(e.dataTransfer.files);
if (files.length) onDrop(files);
}
function handleDragOver(e: DragEvent<HTMLDivElement>) {
e.preventDefault();
setIsOver(true);
}
function handleDragLeave() {
setIsOver(false);
}
function handleFileInput(e: React.ChangeEvent<HTMLInputElement>) {
const files = Array.from(e.target.files || []);
if (files.length) onDrop(files);
}
return (
<div className="space-y-2">
<div
role="button"
tabIndex={0}
onClick={() => inputRef.current?.click()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
inputRef.current?.click();
}
}}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
className={cn(
"p-8 rounded-xl border-2 border-dashed transition-all text-center cursor-pointer",
"bg-[rgb(var(--surface))] border-[rgb(var(--border))]",
isOver
? "border-[rgb(var(--accent))] bg-[rgb(var(--accent-subtle))]"
: "hover:border-[rgb(var(--accent))] hover:bg-[rgb(var(--surface-raised))]",
className
)}
aria-label="Drop files here or click to browse"
>
{children}
</div>
<input
ref={inputRef}
type="file"
multiple={multiple}
accept={accept}
onChange={handleFileInput}
className="sr-only"
/>
</div>
);
}
// ── Kanban Board ──────────────────────────────────────────
interface Column<T> {
id: string;
title: string;
items: T[];
}
interface KanbanBoardProps<T extends { id: string }> {
columns: Column<T>[];
onChange: (columns: Column<T>[]) => void;
renderItem: (item: T) => React.ReactNode;
}
export function KanbanBoard<T extends { id: string }>({
columns,
onChange,
renderItem,
}: KanbanBoardProps<T>) {
const [draggedItem, setDraggedItem] = useState<{ item: T; fromCol: string } | null>(null);
function handleDragStart(item: T, fromCol: string) {
setDraggedItem({ item, fromCol });
}
function handleDrop(toColId: string) {
if (!draggedItem) return;
const newColumns = columns.map((col) => {
if (col.id === draggedItem.fromCol) {
return {
...col,
items: col.items.filter((i) => i.id !== draggedItem.item.id),
};
}
if (col.id === toColId) {
return {
...col,
items: [...col.items, draggedItem.item],
};
}
return col;
});
onChange(newColumns);
setDraggedItem(null);
}
function handleDragOver(e: DragEvent<HTMLDivElement>) {
e.preventDefault();
}
return (
<div className="flex gap-4 overflow-x-auto pb-4">
{columns.map((col) => (
<div
key={col.id}
className="w-72 flex-shrink-0 rounded-xl border bg-[rgb(var(--surface-raised))]"
onDragOver={handleDragOver}
onDrop={() => handleDrop(col.id)}
>
<div className="px-4 py-3 border-b border-[rgb(var(--border))]">
<h3 className="text-[12px] font-semibold uppercase tracking-wider text-[rgb(var(--text-secondary))]">
{col.title} ({col.items.length})
</h3>
</div>
<div className="p-2 space-y-2 min-h-[100px]">
{col.items.map((item) => (
<div
key={item.id}
draggable
onDragStart={() => handleDragStart(item, col.id)}
className="p-3 rounded-lg border cursor-grab active:cursor-grabbing transition-all bg-[rgb(var(--surface))] border-[rgb(var(--border))] hover:border-[rgb(var(--accent))]"
>
{renderItem(item)}
</div>
))}
</div>
</div>
))}
</div>
);
}Accessibility
- →Provide keyboard alternatives for all drag operations (e.g., Move Up/Down buttons).
- →Use aria-grabbed to indicate drag state for screen readers.
- →Announce drops with aria-live regions when order changes.
- →Ensure drop targets are focusable and indicate focus state.
- →Support Enter/Space to pick up and drop items via keyboard.
- →Provide visual focus indicators for keyboard navigation.