Skip to content

Tailwind CSS v4 Project — Build a Modern Dashboard from Scratch

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Tailwind CSS v4 Project. We cover key concepts, practical examples, and best practices to help you master this topic.

Build a complete modern admin dashboard using Tailwind CSS v4's CSS-first configuration, container queries, dark mode, custom animations, and responsive layout — applying every major v4 feature in a single project.

What You'll Learn

You will build a production-quality admin dashboard from scratch using Tailwind v4, applying CSS-first configuration, container queries, dark mode, custom keyframe animations, responsive utilities, and deployment optimization.

Why It Matters

Building a complete project reinforces every concept from the course. DodaTech's tools use similar dashboard patterns for Durga Antivirus Pro's admin panel and Doda Browser's extension settings.

Real-World Use

Durga Antivirus Pro's admin dashboard follows this exact architecture: sidebar navigation, stat cards, data tables, and notification panels built with Tailwind v4's CSS-first approach.

flowchart LR
    A[Setup] --> B[Config]
    B --> C[Layout]
    C --> D[Sidebar]
    C --> E[Main Content]
    D --> F[Navigation]
    E --> G[Stats]
    E --> H[Table]
    E --> I[Notifications]
    F --> J[Dashboard Complete]
    style A fill:#38bdf8,stroke:#0284c7,color:#fff
    style J fill:#22c55e,stroke:#16a34a,color:#fff

Project Setup

# Create project
mkdir dodatech-dashboard
cd dodatech-dashboard
npm init -y
npm install -D @tailwindcss/vite vite

# Create source files
mkdir -p src/components
// vite.config.js
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [tailwindcss()],
  build: {
    cssMinify: "lightningcss",
  },
});

Expected output: A minimal Vite project with Tailwind v4 and Lightning CSS for production builds.

CSS Configuration

/* src/style.css */
@import "tailwindcss";
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/preflight" layer(base);
@import "tailwindcss/utilities" layer(utilities);

@theme {
  --color-primary-50: #eef2ff;
  --color-primary-100: #e0e7ff;
  --color-primary-200: #c7d2fe;
  --color-primary-500: #6366f1;
  --color-primary-600: #4f46e5;
  --color-primary-700: #4338ca;
  --color-primary-800: #3730a3;

  --color-surface: #ffffff;
  --color-surface-dark: #1e1e2e;

  --animate-fade-in: fade-in 0.3s ease-out;
  --animate-slide-in: slide-in 0.3s ease-out;
  --animate-pulse-slow: pulse 3s infinite;
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slide-in {
  from { opacity: 0; transform: translateX(-10px); }
  to { opacity: 1; transform: translateX(0); }
}

Expected output: CSS-first configuration defines custom brand colors, surface colors, and three custom animations. The @theme block makes all values available as utility classes.

/* src/components/sidebar.css */
@layer components {
  .sidebar-link {
    @apply
      flex items-center gap-3
      px-4 py-3 rounded-lg
      text-gray-600 dark:text-gray-400
      hover:bg-primary-50 dark:hover:bg-primary-800/20
      hover:text-primary-600 dark:hover:text-primary-400
      transition-colors duration-200
      animate-fade-in;
  }

  .sidebar-link-active {
    @apply sidebar-link
      bg-primary-50 dark:bg-primary-800/20
      text-primary-600 dark:text-primary-400
      font-medium;
  }
}

Expected output: Component classes composed from utilities. The @layer components directive organizes custom component styles within Tailwind's cascade layers.

HTML Structure

<!-- index.html -->
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DodaTech Dashboard</title>
  <link rel="stylesheet" href="/src/style.css">
</head>
<body class="bg-gray-50 dark:bg-surface-dark text-gray-900 dark:text-gray-100 min-h-dvh">
  <div class="flex h-dvh overflow-hidden">
    <!-- Sidebar -->
    <aside class="
      hidden md:flex md:flex-col
      w-64 bg-white dark:bg-gray-900
      border-r border-gray-200 dark:border-gray-800
      p-4
    ">
      <div class="flex items-center gap-3 mb-8 px-2">
        <div class="w-8 h-8 bg-primary-500 rounded-lg flex items-center justify-center text-white font-bold">D</div>
        <span class="font-semibold text-lg">DodaTech</span>
      </div>
      <nav class="flex-1 space-y-1">
        <a href="#" class="sidebar-link-active">Dashboard</a>
        <a href="#" class="sidebar-link">Analytics</a>
        <a href="#" class="sidebar-link">Users</a>
        <a href="#" class="sidebar-link">Settings</a>
      </nav>
    </aside>

    <!-- Main Content -->
    <main class="flex-1 flex flex-col overflow-hidden">
      <!-- Header -->
      <header class="
        flex items-center justify-between
        px-6 py-4 bg-white dark:bg-gray-900
        border-b border-gray-200 dark:border-gray-800
      ">
        <h1 class="text-xl font-semibold">Dashboard</h1>
        <div class="flex items-center gap-4">
          <button class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
            <span class="sr-only">Notifications</span>
            <span class="w-5 h-5 block bg-gray-400 rounded-full"></span>
          </button>
          <div class="w-8 h-8 bg-primary-500 rounded-full flex items-center justify-center text-white text-sm font-medium">AD</div>
        </div>
      </header>

      <!-- Scrollable Content -->
      <div class="flex-1 overflow-y-auto p-6">
        <!-- Stats Grid -->
        <div class="@container max-w-full mb-8">
          <div class="
            grid grid-cols-1 @sm:grid-cols-2 @lg:grid-cols-4
            gap-4
          ">
            <div class="stat-card">Total Users</div>
            <div class="stat-card">Revenue</div>
            <div class="stat-card">Active Now</div>
            <div class="stat-card">Alerts</div>
          </div>
        </div>

        <!-- Charts and Tables -->
        <div class="@container max-w-full">
          <div class="
            grid grid-cols-1 @lg:grid-cols-3
            gap-6
          ">
            <!-- Main Table -->
            <div class="@lg:col-span-2 bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-6">
              <h2 class="text-lg font-semibold mb-4">Recent Activity</h2>
              <div class="overflow-x-auto">
                <table class="w-full text-sm">
                  <thead>
                    <tr class="text-left text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-800">
                      <th class="pb-3 font-medium">User</th>
                      <th class="pb-3 font-medium">Action</th>
                      <th class="pb-3 font-medium">Status</th>
                      <th class="pb-3 font-medium text-right">Time</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr class="border-b border-gray-100 dark:border-gray-800 animate-fade-in">
                      <td class="py-3">Alice</td>
                      <td class="py-3">File upload</td>
                      <td class="py-3"><span class="px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded-full text-xs font-medium">Completed</span></td>
                      <td class="py-3 text-right text-gray-500">2 min ago</td>
                    </tr>
                    <tr class="border-b border-gray-100 dark:border-gray-800 animate-fade-in">
                      <td class="py-3">Bob</td>
                      <td class="py-3">Scan started</td>
                      <td class="py-3"><span class="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 rounded-full text-xs font-medium">In Progress</span></td>
                      <td class="py-3 text-right text-gray-500">5 min ago</td>
                    </tr>
                    <tr class="animate-fade-in">
                      <td class="py-3">Carol</td>
                      <td class="py-3">Login alert</td>
                      <td class="py-3"><span class="px-2 py-1 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 rounded-full text-xs font-medium">Flagged</span></td>
                      <td class="py-3 text-right text-gray-500">12 min ago</td>
                    </tr>
                  </tbody>
                </table>
              </div>
            </div>

            <!-- Notifications Panel -->
            <div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-6">
              <h2 class="text-lg font-semibold mb-4">Notifications</h2>
              <div class="space-y-4">
                <div class="flex gap-3 p-3 bg-primary-50 dark:bg-primary-900/20 rounded-lg animate-slide-in">
                  <div class="w-2 h-2 mt-2 bg-primary-500 rounded-full shrink-0"></div>
                  <div>
                    <p class="text-sm font-medium">Update Available</p>
                    <p class="text-xs text-gray-500 mt-1">v2.5.0 is ready to install</p>
                  </div>
                </div>
                <div class="flex gap-3 p-3 rounded-lg animate-slide-in">
                  <div class="w-2 h-2 mt-2 bg-gray-400 rounded-full shrink-0"></div>
                  <div>
                    <p class="text-sm font-medium">Backup Complete</p>
                    <p class="text-xs text-gray-500 mt-1">System backup finished successfully</p>
                  </div>
                </div>
                <div class="flex gap-3 p-3 rounded-lg animate-slide-in">
                  <div class="w-2 h-2 mt-2 bg-yellow-500 rounded-full shrink-0"></div>
                  <div>
                    <p class="text-sm font-medium">License Expiring</p>
                    <p class="text-xs text-gray-500 mt-1">Your license expires in 7 days</p>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </main>
  </div>
</body>
</html>

Expected output: A complete admin dashboard with sidebar navigation, responsive grid using container queries, dark mode support, data table with status badges, notification panel with custom slide-in animations, and scroll-driven fade-in effects.

Responsive Behavior

<!-- Mobile sidebar toggle using dialog element -->
<dialog class="
  md:hidden
  fixed inset-0 z-50
  bg-black/50
  backdrop-blur-sm
  open:flex
  open:animate-fade-in
">
  <aside class="
    w-64 h-full bg-white dark:bg-gray-900
    p-4
    animate-slide-in
  ">
    <!-- Same sidebar content -->
  </aside>
</dialog>

Expected output: On mobile (below md breakpoint), a dialog-based sidebar overlay with backdrop blur provides navigation access.

Dark Mode Implementation

/* Dark mode via class strategy on <html> */
@custom-variant dark (&:where(.dark, .dark *));

/* Or use media query strategy */
@custom-variant dark (@media (prefers-color-scheme: dark));

Expected output: Dark mode is controlled by the dark class on the <html> element. The @custom-variant directive defines how dark mode is detected — either class-based or system preference-based.

Build and Deploy

# Development
npx vite

# Production build
NODE_ENV=production npx vite build

# Output size
du -sh dist/assets/*.css
# ~6KB gzipped

Expected output: Development server with HMR. Production build outputs a ~6KB CSS bundle with only used utilities.

# Deploy to Netlify
npx netlify-cli deploy --dir=dist --prod

Expected output: The dashboard is deployed to production with CDN caching and HTTPS.

Common Mistakes

1. Not Testing Dark Mode Toggle

Animating between dark and light modes without transition-colors on root elements causes jarring flashes. Add transition-colors duration-200 to body and main containers.

2. Container Query Without @container Class

Container query variants require the parent to have the @container class. Without it, @sm: variants fall back to viewport breakpoint behavior.

3. Over-nesting Component Classes

Keep component classes flat. Deeply nested @apply chains are harder to debug and maintain. Prefer composing utility classes directly in HTML.

4. Missing Dark Variants on All Elements

Every colored element needs both light and dark variants. Forgetting dark: variants produces broken layouts in dark mode.

5. Not Handling Mobile Navigation

The sidebar must be accessible on mobile. A dialog-based overlay or off-canvas pattern is essential for responsive dashboards.

Practice Questions

  1. What is the purpose of the @container class in this project? It enables container query variants (@sm:, @lg:) on child elements based on the container's width rather than viewport.

  2. How is dark mode implemented in this dashboard? The dark class on <html> triggers dark: variant styles. The @custom-variant dark directive defines the detection Strategy.

  3. What are the three custom animations defined in the project? animate-fade-in (opacity), animate-slide-in (translate + opacity), and animate-pulse-slow (slower pulse).

  4. How does the sidebar behave on mobile? It is hidden below md breakpoint and shown in a dialog overlay when a toggle button is clicked.

  5. What is the purpose of the stat-card class? It is a component class (defined via @layer components) that provides consistent card styling for the stat grid.

Challenge

Extend the dashboard with: a real-time clock widget using CSS animations, a settings panel with form controls styled using Tailwind v4 form utilities, a chart placeholder using CSS gradients, a language switcher with state-based styling, and automated dark mode based on system preference using @custom-variant dark (@media (prefers-color-scheme: dark)).

FAQ

Can I add interactive JavaScript to this dashboard?

Yes. Tailwind v4 works alongside any JavaScript framework. Add Alpine.js, React, or vanilla JS for interactive features.

How do I add more pages to this dashboard?

Create additional HTML files or use a framework like React Router or Alpine.js for client-side routing. Each page shares the shell layout.

Does this work with a backend API?

Yes. Use fetch() or a library like Axios to connect the dashboard to a REST or GraphQL API for real data.

How do I customize the color scheme further?

Add more color tokens to the @theme block. Every --color-* value becomes available as bg-, text-, border-*, etc.

Can I use this dashboard as a template?

Yes. This project is designed as a starting point. Customize the layout, colors, and components for your own projects.

Mini Project

This dashboard is the mini project. Extend it by adding: a sidebar toggle with smooth width transition, a notifications panel with count badge using animate-ping, a data table with sortable column headers, a dark/light mode toggle with prefers-color-scheme detection and manual override, and a footer with system status indicators using animate-pulse-slow.

What's Next

Congratulations on completing the Tailwind CSS v4 course! You have mastered CSS-first configuration, container queries, custom animations, dark mode, Responsive Design, and production deployment. Continue your frontend journey with Tailwind CSS v3 for Migration context or explore CSS for deeper foundational knowledge.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro