Back to Blog

React Admin Dashboard Project for Beginners

Build a production-quality admin panel from scratch with React, Vite, and Tailwind CSS.

React Admin Dashboard Project for Beginners

A complete walkthrough to building a responsive admin panel with sidebar, charts, data tables, and dark mode.

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
Why this project? Admin dashboards are one of the most in-demand UI patterns in web development. Building one teaches you layout management, state coordination, data visualization, and responsive design — all essential skills for a frontend developer.

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";
Tip: Vite with the @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;
}
Mobile first: On screens below 768px, the sidebar should collapse to icons only or become a slide-out drawer. Don't just scale down the desktop layout — redesign for touch interactions.

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>
  );
}
Always use ResponsiveContainer: Recharts requires a parent with defined dimensions. 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>
  );
}
Performance: For tables with 1000+ rows, consider virtualization with 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>
  );
}
Respect user preference: On first visit, check 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.

Route configuration: Use nested routes to share layout components. This avoids duplicating the sidebar and header across every page.

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 alignment
  • grid grid-cols-1 md:grid-cols-3 gap-4 — responsive grid
  • transition-all duration-300 — smooth animations
  • hover:bg-gray-100 dark:hover:bg-gray-700 — hover states
  • shadow-sm rounded-xl — card styling
Customize your palette: Extend Tailwind's default colors in 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.

Back to Top