Building a Drag-and-Drop Dashboard in React
Introduction
Interactive dashboards with drag-and-drop functionality enhance user experience. This guide shows how to build a customizable dashboard using React DnD.
Prerequisites
- React project setup
- Basic understanding of React hooks
Step 1: Install Dependencies
npm install react-dnd react-dnd-html5-backend uuid
npm install --save-dev @types/uuid
Step 2: Setup DnD Provider
Update app/layout.tsx
:
'use client'
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<DndProvider backend={HTML5Backend}>
{children}
</DndProvider>
</body>
</html>
);
}
Step 3: Define Widget Types
Create types/dashboard.ts
:
export interface Widget {
id: string;
type: "chart" | "stats" | "table" | "text";
title: string;
x: number;
y: number;
width: number;
height: number;
data?: any;
}
export interface DragItem {
type: string;
id: string;
index: number;
}
Step 4: Create Draggable Widget Component
Create components/DraggableWidget.tsx
:
import { useDrag } from 'react-dnd';
import { Widget } from '@/types/dashboard';
interface DraggableWidgetProps {
widget: Widget;
index: number;
onMove: (dragIndex: number, hoverIndex: number) => void;
children: React.ReactNode;
}
export default function DraggableWidget({
widget,
index,
onMove,
children,
}: DraggableWidgetProps) {
const [{ isDragging }, drag] = useDrag({
type: 'widget',
item: { type: 'widget', id: widget.id, index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
return (
<div
ref={drag}
className={`bg-white rounded-lg shadow-md border transition-all cursor-move ${
isDragging ? 'opacity-50 rotate-2' : 'hover:shadow-lg'
}`}
style={{
width: widget.width,
height: widget.height,
}}
>
<div className="p-4 border-b border-gray-200 bg-gray-50 rounded-t-lg">
<h3 className="font-semibold text-gray-800">{widget.title}</h3>
<div className="flex space-x-1 mt-1">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
</div>
</div>
<div className="p-4">{children}</div>
</div>
);
}
Step 5: Create Drop Zone
Create components/DropZone.tsx
:
import { useDrop } from 'react-dnd';
import { DragItem } from '@/types/dashboard';
interface DropZoneProps {
onDrop: (item: DragItem, targetIndex: number) => void;
index: number;
children: React.ReactNode;
}
export default function DropZone({ onDrop, index, children }: DropZoneProps) {
const [{ isOver, canDrop }, drop] = useDrop({
accept: 'widget',
drop: (item: DragItem) => onDrop(item, index),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const isActive = canDrop && isOver;
return (
<div
ref={drop}
className={`min-h-[200px] border-2 border-dashed rounded-lg transition-colors ${
isActive
? 'border-blue-500 bg-blue-50'
: canDrop
? 'border-gray-300 bg-gray-50'
: 'border-gray-200'
}`}
>
{children}
{isActive && (
<div className="absolute inset-0 flex items-center justify-center bg-blue-100 bg-opacity-75 rounded-lg">
<p className="text-blue-600 font-medium">Drop widget here</p>
</div>
)}
</div>
);
}
Step 6: Create Widget Components
Create components/widgets/ChartWidget.tsx
:
export default function ChartWidget({ data }: { data?: any }) {
return (
<div className="h-full flex items-center justify-center">
<div className="w-full h-32 bg-gradient-to-r from-blue-400 to-purple-500 rounded flex items-end p-2">
{[40, 70, 45, 60, 85].map((height, i) => (
<div
key={i}
className="bg-white bg-opacity-80 rounded-sm mr-1 flex-1"
style={{ height: `${height}%` }}
/>
))}
</div>
</div>
);
}
Create components/widgets/StatsWidget.tsx
:
export default function StatsWidget({ data }: { data?: any }) {
const stats = data || { value: '42.5K', label: 'Users', change: '+12%' };
return (
<div className="text-center">
<div className="text-3xl font-bold text-gray-800">{stats.value}</div>
<div className="text-gray-600">{stats.label}</div>
<div className="text-green-600 text-sm font-medium">{stats.change}</div>
</div>
);
}
Step 7: Create Dashboard Manager
Create components/Dashboard.tsx
:
import { useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import DraggableWidget from './DraggableWidget';
import DropZone from './DropZone';
import ChartWidget from './widgets/ChartWidget';
import StatsWidget from './widgets/StatsWidget';
import { Widget, DragItem } from '@/types/dashboard';
const initialWidgets: Widget[] = [
{
id: uuidv4(),
type: 'stats',
title: 'Total Users',
x: 0,
y: 0,
width: 300,
height: 200,
},
{
id: uuidv4(),
type: 'chart',
title: 'Revenue Chart',
x: 320,
y: 0,
width: 400,
height: 200,
},
];
export default function Dashboard() {
const [widgets, setWidgets] = useState<Widget[]>(initialWidgets);
const moveWidget = (dragIndex: number, hoverIndex: number) => {
const draggedWidget = widgets[dragIndex];
const newWidgets = [...widgets];
newWidgets.splice(dragIndex, 1);
newWidgets.splice(hoverIndex, 0, draggedWidget);
setWidgets(newWidgets);
};
const handleDrop = (item: DragItem, targetIndex: number) => {
moveWidget(item.index, targetIndex);
};
const renderWidget = (widget: Widget) => {
switch (widget.type) {
case 'chart':
return <ChartWidget data={widget.data} />;
case 'stats':
return <StatsWidget data={widget.data} />;
default:
return <div>Unknown widget type</div>;
}
};
const addWidget = (type: Widget['type']) => {
const newWidget: Widget = {
id: uuidv4(),
type,
title: `New ${type} Widget`,
x: 0,
y: 0,
width: 300,
height: 200,
};
setWidgets([...widgets, newWidget]);
};
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
<div className="space-x-2">
<button
onClick={() => addWidget('stats')}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Add Stats Widget
</button>
<button
onClick={() => addWidget('chart')}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Add Chart Widget
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{widgets.map((widget, index) => (
<DropZone key={widget.id} onDrop={handleDrop} index={index}>
<DraggableWidget
widget={widget}
index={index}
onMove={moveWidget}
>
{renderWidget(widget)}
</DraggableWidget>
</DropZone>
))}
</div>
</div>
);
}
Step 8: Usage in App
import Dashboard from '@/components/Dashboard';
export default function Home() {
return (
<main className="min-h-screen bg-gray-100">
<Dashboard />
</main>
);
}
Summary
React DnD enables sophisticated drag-and-drop interfaces for dashboards. The pattern separates drag logic from UI components, making it maintainable and extensible for complex dashboard requirements.