Table of Contents
1. What You'll Build
In this tutorial, we'll build a complete admin dashboard that includes features commonly found in production applications. By the end, you'll have a project that demonstrates real-world React skills.
Features Overview
- Sidebar navigation with collapse/expand toggle and active route highlighting
- Stats cards showing key metrics with trend indicators
- Interactive charts for revenue, users, and sales data
- Data table with sorting, filtering, and pagination
- Dark/Light mode toggle with CSS variables
- Multi-page routing with React Router
- Responsive design — works on mobile, tablet, and desktop
Tech Stack
- React 18 with Vite
- Tailwind CSS for styling
- Recharts for data visualization
- React Router v6 for navigation
- Lucide React for icons
2. Setting Up the Project
We'll use Vite for fast development and hot module replacement.
Create the Project
# Create a new Vite + React project
npm create vite@latest admin-dashboard -- --template react
cd admin-dashboard
# Install dependencies
npm install
# Install additional packages
npm install react-router-dom recharts lucide-react
# Install Tailwind CSS
npm install -D tailwindcss @tailwindcss/vite
Configure Tailwind
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
/* src/index.css */
@import "tailwindcss";
@tailwindcss/vite plugin is the fastest setup for Tailwind CSS v4. It handles PostCSS configuration automatically.
3. Project Structure
A well-organized folder structure makes your project maintainable and easy to navigate.
src/
├── components/
│ ├── layout/
│ │ ├── Sidebar.jsx
│ │ ├── Header.jsx
│ │ └── DashboardLayout.jsx
│ ├── charts/
│ │ ├── RevenueChart.jsx
│ │ └── UsersChart.jsx
│ ├── ui/
│ │ ├── StatsCard.jsx
│ │ ├── DataTable.jsx
│ │ └── ThemeToggle.jsx
│ └── common/
│ └── SearchBar.jsx
├── pages/
│ ├── Dashboard.jsx
│ ├── Analytics.jsx
│ ├── Users.jsx
│ └── Settings.jsx
├── context/
│ └── ThemeContext.jsx
├── hooks/
│ └── useTheme.js
├── data/
│ └── mockData.js
├── App.jsx
└── main.jsx
Each component has a single responsibility. Layout components handle structure, chart components handle visualization, and UI components are reusable building blocks.
5. Creating the Dashboard Layout
The layout component coordinates the sidebar, header, and main content area with a responsive grid system.
// components/layout/DashboardLayout.jsx
import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import Sidebar from './Sidebar';
import Header from './Header';
export default function DashboardLayout() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
return (
<div className="min-h-screen bg-gray-100 dark:bg-gray-950">
<Sidebar
collapsed={sidebarCollapsed}
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
/>
<div className={`
transition-all duration-300
${sidebarCollapsed ? 'ml-20' : 'ml-64'}
`}>
<Header />
<main className="p-6">
<Outlet />
</main>
</div>
</div>
);
}
Responsive Considerations
On mobile, the sidebar should overlay the content with a backdrop, not push it. Use a media query or hook to detect screen width:
// hooks/useMediaQuery.js
import { useState, useEffect } from 'react';
export function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mq = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [query]);
return matches;
}
6. Stats Cards with Charts
Stats cards provide a quick overview of key metrics. Each card should show the metric, its value, and a trend indicator.
// components/ui/StatsCard.jsx
import { TrendingUp, TrendingDown } from 'lucide-react';
export default function StatsCard({ title, value, change, icon: Icon, color }) {
const isPositive = change >= 0;
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400">{title}</p>
<p className="text-2xl font-bold mt-1">{value}</p>
<p className={`
text-sm mt-2 flex items-center gap-1
${isPositive ? 'text-green-600' : 'text-red-600'}
`}>
{isPositive ? <TrendingUp size={16} /> : <TrendingDown size={16} />}
{Math.abs(change)}% from last month
</p>
</div>
<div className={`p-3 rounded-lg ${color}`}>
<Icon size={24} className="text-white" />
</div>
</div>
</div>
);
}
Revenue Chart with Recharts
// components/charts/RevenueChart.jsx
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
const data = [
{ month: 'Jan', revenue: 4000, expenses: 2400 },
{ month: 'Feb', revenue: 3000, expenses: 1398 },
{ month: 'Mar', revenue: 5000, expenses: 3800 },
{ month: 'Apr', revenue: 4780, expenses: 3908 },
{ month: 'May', revenue: 5890, expenses: 4800 },
{ month: 'Jun', revenue: 6390, expenses: 3800 },
];
export default function RevenueChart() {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">Revenue Overview</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="revenue" stroke="#6366f1" fill="#6366f1" fillOpacity={0.2} />
<Area type="monotone" dataKey="expenses" stroke="#ef4444" fill="#ef4444" fillOpacity={0.2} />
</AreaChart>
</ResponsiveContainer>
</div>
);
}
ResponsiveContainer makes your charts automatically resize with the viewport.
7. Data Table Component
A reusable data table with sorting, filtering, and pagination — one of the most common components in admin dashboards.
// components/ui/DataTable.jsx
import { useState, useMemo } from 'react';
import { ChevronUp, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
export default function DataTable({ columns, data, rowsPerPage = 10 }) {
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
const [filter, setFilter] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const filteredData = useMemo(() => {
let result = data;
if (filter) {
result = result.filter((row) =>
Object.values(row).some((val) =>
String(val).toLowerCase().includes(filter.toLowerCase())
)
);
}
if (sortConfig.key) {
result.sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) return sortConfig.direction === 'asc' ? -1 : 1;
if (a[sortConfig.key] > b[sortConfig.key]) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
return result;
}, [data, filter, sortConfig]);
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
const paginatedData = filteredData.slice(
(currentPage - 1) * rowsPerPage,
currentPage * rowsPerPage
);
const handleSort = (key) => {
setSortConfig((prev) => ({
key,
direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc',
}));
};
return (
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm overflow-hidden">
<div className="p-4 border-b dark:border-gray-700">
<input
type="text"
placeholder="Search..."
value={filter}
onChange={(e) => { setFilter(e.target.value); setCurrentPage(1); }}
className="px-4 py-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600"
/>
</div>
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
{columns.map((col) => (
<th
key={col.key}
onClick={() => col.sortable && handleSort(col.key)}
className={`px-6 py-3 text-left text-sm font-medium text-gray-500
${col.sortable ? 'cursor-pointer hover:text-gray-700' : ''}`}
>
{col.label}
{sortConfig.key === col.key && (
sortConfig.direction === 'asc' ? <ChevronUp size={14} /> : <ChevronDown size={14} />
)}
</th>
))}
</tr>
</thead>
<tbody>
{paginatedData.map((row, i) => (
<tr key={i} className="border-t dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700">
{columns.map((col) => (
<td key={col.key} className="px-6 py-4 text-sm">
{col.render ? col.render(row[col.key], row) : row[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
<div className="flex items-center justify-between p-4 border-t dark:border-gray-700">
<span className="text-sm text-gray-500">
Showing {((currentPage - 1) * rowsPerPage) + 1} to {Math.min(currentPage * rowsPerPage, filteredData.length)} of {filteredData.length}
</span>
<div className="flex gap-2">
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={currentPage === 1}>
<ChevronLeft />
</button>
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages}>
<ChevronRight />
</button>
</div>
</div>
</div>
);
}
react-window or @tanstack/react-virtual to avoid rendering all rows at once.
8. Dark/Light Mode Toggle
Dark mode is a must-have feature for modern dashboards. We'll use React Context and CSS variables to implement a smooth theme switch.
// context/ThemeContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem('theme');
if (saved) return saved;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark');
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'));
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
Theme Toggle Component
// components/ui/ThemeToggle.jsx
import { Sun, Moon } from 'lucide-react';
import { useTheme } from '../../context/ThemeContext';
export default function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
aria-label="Toggle theme"
>
{theme === 'light' ? <Moon size={20} /> : <Sun size={20} />}
</button>
);
}
prefers-color-scheme to match the user's OS setting. Store the choice in localStorage so it persists across sessions.
9. Adding React Router
React Router v6 provides clean, declarative routing for multi-page dashboards.
// App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import DashboardLayout from './components/layout/DashboardLayout';
import Dashboard from './pages/Dashboard';
import Analytics from './pages/Analytics';
import Users from './pages/Users';
import Settings from './pages/Settings';
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<DashboardLayout />}>
<Route index element={<Dashboard />} />
<Route path="analytics" element={<Analytics />} />
<Route path="users" element={<Users />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
}
The nested route structure means the DashboardLayout wraps all pages. The <Outlet /> component renders the matched child route.
10. Styling with Tailwind CSS
Tailwind CSS lets us build complex layouts quickly without writing custom CSS. Here are the key patterns used in this dashboard:
Dark Mode Classes
<!-- Dark mode with Tailwind -->
<div class="bg-white dark:bg-gray-800">
<h2 class="text-gray-900 dark:text-white">Dashboard</h2>
<p class="text-gray-500 dark:text-gray-400">Welcome back</p>
</div>
Responsive Grid
{/* Stats cards grid — 1 col mobile, 2 col tablet, 4 col desktop */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatsCard title="Revenue" value="$45,231" change={12.5} icon={DollarSign} color="bg-indigo-500" />
<StatsCard title="Users" value="2,345" change={8.2} icon={Users} color="bg-green-500" />
<StatsCard title="Orders" value="1,234" change={-3.1} icon={ShoppingCart} color="bg-yellow-500" />
<StatsCard title="Conversion" value="3.2%" change={5.7} icon={TrendingUp} color="bg-red-500" />
</div>
Utility Classes Cheat Sheet
flex items-center justify-between— flexbox alignmentgrid grid-cols-1 md:grid-cols-3 gap-4— responsive gridtransition-all duration-300— smooth animationshover:bg-gray-100 dark:hover:bg-gray-700— hover statesshadow-sm rounded-xl— card styling
tailwind.config.js to match your brand. Use consistent colors across all charts and UI elements for a polished look.
That's it! You now have a fully functional admin dashboard with sidebar navigation, interactive charts, a sortable data table, dark mode, and responsive design. This project demonstrates core React skills that employers look for.