Tailwind CSS v4 Project — Build a Modern Dashboard from Scratch
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
What is the purpose of the
@containerclass in this project? It enables container query variants (@sm:,@lg:) on child elements based on the container's width rather than viewport.How is dark mode implemented in this dashboard? The
darkclass on<html>triggersdark:variant styles. The@custom-variant darkdirective defines the detection Strategy.What are the three custom animations defined in the project?
animate-fade-in(opacity),animate-slide-in(translate + opacity), andanimate-pulse-slow(slower pulse).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.
What is the purpose of the
stat-cardclass? 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
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