drag-drop-builder
Implement drag and drop with dnd-kit, react-beautiful-dnd. Sortable lists, drag between containers, file upload. Keywords - drag and drop, dnd kit, sortable list, react beautiful dnd, draggable, droppable, file drag drop
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: drag-drop-builder
description: Implement drag and drop with dnd-kit, react-beautiful-dnd. Sortable lists, drag between containers, file upload. Keywords - drag and drop, dnd kit, sortable list, react beautiful dnd, draggable, droppable, file drag drop
---
# Drag and Drop Builder
## When to Use This Skill
Activate when:
- Building sortable lists (reorder items)
- Drag and drop between containers (Kanban boards)
- File upload with drag and drop
- Building visual editors or dashboards
- Creating interactive UI components
## What This Skill Does
Implements drag and drop with:
- Sortable lists (reorder items)
- Multi-container drag and drop (Kanban)
- Touch support (mobile)
- Smooth animations
- Accessibility (keyboard navigation)
- File drag and drop upload
## Supported Technologies
**React**:
- dnd-kit (recommended - modern, performant)
- react-beautiful-dnd (simple, deprecated)
- react-dnd (powerful but complex)
**Vue 3**:
- @vueuse/integrations (Sortable.js)
- vuedraggable-next
## Example: Sortable List with dnd-kit (React)
```tsx
// components/SortableList.tsx
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useState } from 'react';
interface Item {
id: string;
title: string;
}
// Sortable Item Component
function SortableItem({ id, title }: Item) {
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}
{...attributes}
{...listeners}
className="rounded-lg border bg-white p-4 shadow-sm hover:shadow-md cursor-grab active:cursor-grabbing"
>
<div className="flex items-center gap-3">
<svg
className="h-5 w-5 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 8h16M4 16h16"
/>
</svg>
<span className="font-medium">{title}</span>
</div>
</div>
);
}
// Sortable List Component
export function SortableList() {
const [items, setItems] = useState<Item[]>([
{ id: '1', title: 'Task 1' },
{ id: '2', title: 'Task 2' },
{ id: '3', title: 'Task 3' },
{ id: '4', title: 'Task 4' },
]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
setItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={items}
strategy={verticalListSortingStrategy}
>
<div className="space-y-2">
{items.map((item) => (
<SortableItem key={item.id} {...item} />
))}
</div>
</SortableContext>
</DndContext>
);
}
```
## Example: Kanban Board (Multi-Container)
```tsx
// components/KanbanBoard.tsx
import { useState } from 'react';
import {
DndContext,
DragOverlay,
closestCorners,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragStartEvent,
DragEndEvent,
DragOverEvent,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { DroppableColumn } from './DroppableColumn';
import { SortableTask } from './SortableTask';
interface Task {
id: string;
title: string;
columnId: string;
}
interface Column {
id: string;
title: string;
}
export function KanbanBoard() {
const [columns] = useState<Column[]>([
{ id: 'todo', title: 'To Do' },
{ id: 'inProgress', title: 'In Progress' },
{ id: 'done', title: 'Done' },
]);
const [tasks, setTasks] = useState<Task[]>([
{ id: '1', title: 'Task 1', columnId: 'todo' },
{ id: '2', title: 'Task 2', columnId: 'todo' },
{ id: '3', title: 'Task 3', columnId: 'inProgress' },
{ id: '4', title: 'Task 4', columnId: 'done' },
]);
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor)
);
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string);
};
const handleDragOver = (event: DragOverEvent) => {
const { active, over } = event;
if (!over) return;
const activeId = active.id;
const overId = over.id;
if (activeId === overId) return;
const activeTask = tasks.find((t) => t.id === activeId);
const overTask = tasks.find((t) => t.id === overId);
if (!activeTask) return;
// Moving to a different column
const overColumn = columns.find((c) => c.id === overId);
if (overColumn) {
setTasks((tasks) =>
tasks.map((task) =>
task.id === activeId
? { ...task, columnId: overColumn.id }
: task
)
);
} else if (overTask && activeTask.columnId !== overTask.columnId) {
setTasks((tasks) =>
tasks.map((task) =>
task.id === activeId
? { ...task, columnId: overTask.columnId }
: task
)
);
}
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over) return;
const activeTask = tasks.find((t) => t.id === active.id);
const overTask = tasks.find((t) => t.id === over.id);
if (activeTask && overTask && activeTask.columnId === overTask.columnId) {
const columnTasks = tasks.filter((t) => t.columnId === activeTask.columnId);
const oldIndex = columnTasks.findIndex((t) => t.id === active.id);
const newIndex = columnTasks.findIndex((t) => t.id === over.id);
const reorderedTasks = arrayMove(columnTasks, oldIndex, newIndex);
setTasks((tasks) => [
...tasks.filter((t) => t.columnId !== activeTask.columnId),
...reorderedTasks,
]);
}
setActiveId(null);
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex gap-4">
{columns.map((column) => (
<DroppableColumn key={column.id} id={column.id} title={column.title}>
<SortableContext
items={tasks.filter((t) => t.columnId === column.id)}
strategy={verticalListSortingStrategy}
>
{tasks
.filter((task) => task.columnId === column.id)
.map((task) => (
<SortableTask key={task.id} {...task} />
))}
</SortableContext>
</DroppableColumn>
))}
</div>
<DragOverlay>
{activeId ? (
<div className="rounded-lg bg-white p-4 shadow-lg">
{tasks.find((t) => t.id === activeId)?.title}
</div>
) : null}
</DragOverlay>
</DndContext>
);
}
```
## Example: File Drag and Drop Upload
```tsx
// components/FileDropzone.tsx
import { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
interface FileWithPreview extends File {
preview: string;
}
export function FileDropzone() {
const [files, setFiles] = useState<FileWithPreview[]>([]);
const onDrop = useCallback((acceptedFiles: File[]) => {
setFiles(
acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
)
);
}, []);
const {
getRootProps,
getInputProps,
isDragActive,
isDragAccept,
isDragReject,
} = useDropzone({
onDrop,
accept: {
'image/*': ['.png', '.jpg', '.jpeg', '.gif'],
},
maxSize: 5 * 1024 * 1024, // 5MB
multiple: true,
});
return (
<div>
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition ${
isDragActive
? 'border-blue-500 bg-blue-50'
: isDragReject
? 'border-red-500 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<input {...getInputProps()} />
{isDragActive ? (
<p className="text-blue-600">Drop the files here...</p>
) : (
<div>
<p className="text-gray-600">
Drag and drop files here, or click to select
</p>
<p className="text-sm text-gray-400 mt-2">
PNG, JPG, GIF up to 5MB
</p>
</div>
)}
</div>
{/* File previews */}
{files.length > 0 && (
<div className="mt-4 grid grid-cols-3 gap-4">
{files.map((file, index) => (
<div key={index} className="relative">
<img
src={file.preview}
alt={file.name}
className="w-full h-32 object-cover rounded-lg"
onLoad={() => URL.revokeObjectURL(file.preview)}
/>
<button
onClick={() =>
setFiles((files) => files.filter((_, i) => i !== index))
}
className="absolute top-2 right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600"
>
×
</button>
</div>
))}
</div>
)}
</div>
);
}
```
## Example: Vue 3 Sortable List
```vue
<!-- components/SortableList.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { useSortable } from '@vueuse/integrations/useSortable';
interface Item {
id: string;
title: string;
}
const items = ref<Item[]>([
{ id: '1', title: 'Task 1' },
{ id: '2', title: 'Task 2' },
{ id: '3', title: 'Task 3' },
]);
const listRef = ref<HTMLElement | null>(null);
useSortable(listRef, items, {
animation: 150,
handle: '.drag-handle',
});
</script>
<template>
<div ref="listRef" class="space-y-2">
<div
v-for="item in items"
:key="item.id"
class="rounded-lg border bg-white p-4 shadow-sm hover:shadow-md"
>
<div class="flex items-center gap-3">
<svg
class="drag-handle h-5 w-5 text-gray-400 cursor-grab"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 8h16M4 16h16"
/>
</svg>
<span class="font-medium">{{ item.title }}</span>
</div>
</div>
</div>
</template>
```
## Best Practices
1. **Use dnd-kit** - Modern, performant, accessible (React)
2. **Keyboard support** - Enable keyboard navigation for accessibility
3. **Visual feedback** - Show drag state with opacity/shadows
4. **Drag handles** - Use specific handles to initiate drag
5. **Touch support** - Works on mobile devices
6. **Animations** - Smooth transitions for better UX
7. **Prevent default** - Prevent browser's default drag behavior
## Accessibility Checklist
✅ **Keyboard navigation** - Arrow keys to move items
✅ **Screen reader support** - Announce drag state
✅ **Focus management** - Focus follows dragged item
✅ **ARIA attributes** - `aria-grabbed`, `aria-dropeffect`
## Integration with Other Skills
- **state-management-setup** - Store dragged items in global state
- **api-integration-layer** - Save reordered items to backend
- **loading-states-handler** - Show loading during save
---
**Version**: 1.0.0
**Category**: Frontend Extended
**Complexity**: Medium-High