PrivatAI Projects
Need Custom Development?
Get professional help with your Next.js project from our expert team.
Modern Dashboard Template
A comprehensive guide to creating a powerful dashboard interface using Next.js and Tailwind CSS
Project Information
Type
Dashboard UI
Difficulty
Intermediate
Technologies
Next.js, Tailwind CSS, shadcn/ui, React
Last Updated
March 2025
Overview
A well-designed dashboard is essential for modern web applications, providing users with an intuitive interface to monitor, analyze, and interact with data. This guide will walk you through creating a comprehensive dashboard template using Next.js and Tailwind CSS.
This guide covers everything you need to build a professional dashboard, including:
- Setting up the project structure
- Creating responsive layouts with sidebar navigation
- Building reusable dashboard components
- Implementing data visualization elements
- Adding dark/light mode support
- Optimizing for accessibility and performance
By the end of this guide, you'll have a solid understanding of how to create a modern, component-based dashboard that can be customized for various applications.
Setting Up Your Dashboard Project
1. Create a Next.js Project
Start by creating a new Next.js project with the App Router:
npx create-next-app@latest dashboard-template --typescript --tailwind --eslint --app
2. Install Dependencies
Install the necessary packages for our dashboard:
npm install @radix-ui/react-dropdown-menu @radix-ui/react-slot @radix-ui/react-avatar @radix-ui/react-dialog lucide-react recharts framer-motion class-variance-authority clsx tailwind-merge
3. Project Structure
Organize your dashboard with the following structure:
/app
/dashboard
/analytics
page.tsx
/settings
page.tsx
/users
page.tsx
layout.tsx
page.tsx
/components
/dashboard
/cards
analytics-card.tsx
overview-card.tsx
stats-card.tsx
/charts
area-chart.tsx
bar-chart.tsx
pie-chart.tsx
/layout
dashboard-header.tsx
dashboard-sidebar.tsx
dashboard-shell.tsx
/ui
avatar.tsx
button.tsx
card.tsx
dropdown-menu.tsx
...
/lib
utils.ts
/hooks
use-media-query.ts
use-theme.ts
Creating the Dashboard Layout
1. Dashboard Shell
First, let's create the main dashboard shell component:
// components/dashboard/layout/dashboard-shell.tsx
import { ReactNode } from 'react'
import DashboardSidebar from './dashboard-sidebar'
import DashboardHeader from './dashboard-header'
interface DashboardShellProps {
children: ReactNode
}
export default function DashboardShell({ children }: DashboardShellProps) {
return (
<div className="flex h-screen overflow-hidden bg-background">
<DashboardSidebar />
<div className="flex flex-col flex-1 overflow-hidden">
<DashboardHeader />
<main className="flex-1 overflow-y-auto p-4 md:p-6">
{children}
</main>
</div>
</div>
)
}
2. Dashboard Sidebar
Create a responsive sidebar with navigation links:
// components/dashboard/layout/dashboard-sidebar.tsx
"use client"
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
import { LayoutDashboard, BarChart, Users, Settings, Menu, X } from 'lucide-react'
import { cn } from '@/lib/utils'
export default function DashboardSidebar() {
const pathname = usePathname()
const [isOpen, setIsOpen] = useState(false)
const toggleSidebar = () => setIsOpen(!isOpen)
const navItems = [
{
title: "Dashboard",
href: "/dashboard",
icon: <LayoutDashboard className="h-5 w-5" />
},
{
title: "Analytics",
href: "/dashboard/analytics",
icon: <BarChart className="h-5 w-5" />
},
{
title: "Users",
href: "/dashboard/users",
icon: <Users className="h-5 w-5" />
},
{
title: "Settings",
href: "/dashboard/settings",
icon: <Settings className="h-5 w-5" />
}
]
return (
<>
{/* Mobile overlay */}
{isOpen && (
<div
className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm md:hidden"
onClick={toggleSidebar}
/>
)}
{/* Mobile toggle button */}
<button
className="fixed left-4 top-4 z-50 rounded-md p-2 bg-primary text-primary-foreground md:hidden"
onClick={toggleSidebar}
aria-label={isOpen ? "Close sidebar" : "Open sidebar"}
>
{isOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
{/* Sidebar */}
<aside className={cn(
"fixed inset-y-0 left-0 z-40 w-64 transform bg-card border-r border-border transition-transform duration-200 ease-in-out md:translate-x-0 md:static md:h-screen",
isOpen ? "translate-x-0" : "-translate-x-full"
)}>
<div className="flex flex-col h-full">
<div className="flex items-center h-16 px-6 border-b border-border">
<h1 className="text-xl font-bold">Dashboard</h1>
</div>
<nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center px-3 py-2 rounded-md text-sm font-medium transition-colors",
pathname === item.href
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
{item.icon}
<span className="ml-3">{item.title}</span>
</Link>
))}
</nav>
<div className="p-4 border-t border-border">
<div className="flex items-center gap-3 px-3 py-2">
<div className="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center">
<span className="text-sm font-medium text-primary">JD</span>
</div>
<div>
<p className="text-sm font-medium">John Doe</p>
<p className="text-xs text-muted-foreground">john@example.com</p>
</div>
</div>
</div>
</div>
</aside>
</>
)
}
3. Dashboard Header
Create a header with search and user actions:
// components/dashboard/layout/dashboard-header.tsx
"use client"
import { Bell, Search } from 'lucide-react'
import { ThemeToggle } from '@/components/theme-toggle'
export default function DashboardHeader() {
return (
<header className="h-16 border-b border-border flex items-center justify-between px-6">
<div className="relative w-full max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
type="search"
placeholder="Search..."
className="w-full h-9 rounded-md border border-input bg-background pl-10 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div className="flex items-center space-x-4">
<button className="relative p-2 rounded-md hover:bg-muted">
<Bell className="h-5 w-5 text-muted-foreground" />
<span className="absolute top-1 right-1 w-2 h-2 rounded-full bg-primary"></span>
</button>
<ThemeToggle />
</div>
</header>
)
}
4. Dashboard Layout
Set up the dashboard layout in Next.js:
// app/dashboard/layout.tsx
import DashboardShell from '@/components/dashboard/layout/dashboard-shell'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <DashboardShell>{children}</DashboardShell>
}
Building Dashboard Components
1. Stats Card Component
Create a reusable stats card for displaying metrics:
// components/dashboard/cards/stats-card.tsx
import { type LucideIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface StatsCardProps {
title: string
value: string
description?: string
icon: LucideIcon
trend?: {
value: number
isPositive: boolean
}
className?: string
}
export function StatsCard({
title,
value,
description,
icon: Icon,
trend,
className
}: StatsCardProps) {
return (
<Card className={cn("overflow-hidden", className)}>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{description && (
<p className="text-xs text-muted-foreground mt-1">{description}</p>
)}
{trend && (
<div className="flex items-center mt-2">
<span
className={cn(
"text-xs font-medium",
trend.isPositive ? "text-green-500" : "text-red-500"
)}
>
{trend.isPositive ? "+" : "-"}{Math.abs(trend.value)}%
</span>
<span className="text-xs text-muted-foreground ml-1">from last month</span>
</div>
)}
</CardContent>
</Card>
)
}
2. Overview Card with Chart
Create a card with an area chart for data visualization:
// components/dashboard/cards/overview-card.tsx
"use client"
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Area,
AreaChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts'
// Sample data - in a real app, this would come from an API or props
const data = [
{ name: 'Jan', value: 400 },
{ name: 'Feb', value: 300 },
{ name: 'Mar', value: 500 },
{ name: 'Apr', value: 280 },
{ name: 'May', value: 590 },
{ name: 'Jun', value: 320 },
{ name: 'Jul', value: 600 }
]
interface OverviewCardProps {
title: string
description?: string
}
export function OverviewCard({ title, description }: OverviewCardProps) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle>{title}</CardTitle>
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#0ea5e9" stopOpacity={0.8} />
<stop offset="95%" stopColor="#0ea5e9" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
/>
<YAxis
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}`}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
borderColor: 'hsl(var(--border))'
}}
itemStyle={{ color: 'hsl(var(--foreground))' }}
labelStyle={{ color: 'hsl(var(--foreground))' }}
/>
<Area
type="monotone"
dataKey="value"
stroke="#0ea5e9"
fillOpacity={1}
fill="url(#colorValue)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)
}
3. Analytics Card with Bar Chart
Create an analytics card with a bar chart:
// components/dashboard/cards/analytics-card.tsx
"use client"
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Bar,
BarChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts'
// Sample data - in a real app, this would come from an API or props
const data = [
{ name: 'Mon', value: 120 },
{ name: 'Tue', value: 160 },
{ name: 'Wed', value: 180 },
{ name: 'Thu', value: 140 },
{ name: 'Fri', value: 200 },
{ name: 'Sat', value: 90 },
{ name: 'Sun', value: 60 }
]
interface AnalyticsCardProps {
title: string
description?: string
}
export function AnalyticsCard({ title, description }: AnalyticsCardProps) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle>{title}</CardTitle>
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
>
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
/>
<YAxis
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}`}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
borderColor: 'hsl(var(--border))'
}}
itemStyle={{ color: 'hsl(var(--foreground))' }}
labelStyle={{ color: 'hsl(var(--foreground))' }}
/>
<Bar
dataKey="value"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)
}
Creating Dashboard Pages
1. Main Dashboard Page
Create the main dashboard page with multiple components:
// app/dashboard/page.tsx
import { Metadata } from 'next'
import { Users, CreditCard, Activity, DollarSign } from 'lucide-react'
import { StatsCard } from '@/components/dashboard/cards/stats-card'
import { OverviewCard } from '@/components/dashboard/cards/overview-card'
import { AnalyticsCard } from '@/components/dashboard/cards/analytics-card'
export const metadata: Metadata = {
title: 'Dashboard',
description: 'Example dashboard page',
}
export default function DashboardPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<p className="text-muted-foreground">
Overview of your account activity and performance metrics.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatsCard
title="Total Revenue"
value="$45,231.89"
description="Monthly revenue"
icon={DollarSign}
trend={{ value: 12.5, isPositive: true }}
/>
<StatsCard
title="Subscriptions"
value="2,350"
description="Active users"
icon={Users}
trend={{ value: 5.2, isPositive: true }}
/>
<StatsCard
title="Sales"
value="12,234"
description="Total orders"
icon={CreditCard}
trend={{ value: 2.1, isPositive: false }}
/>
<StatsCard
title="Active Sessions"
value="573"
description="Current users"
icon={Activity}
/>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<OverviewCard
title="Revenue Overview"
description="Monthly revenue for the current year"
className="lg:col-span-4"
/>
<AnalyticsCard
title="Weekly Activity"
description="User activity for the current week"
className="lg:col-span-3"
/>
</div>
{/* Additional dashboard content would go here */}
</div>
)
}
2. Analytics Page
Create a dedicated analytics page:
// app/dashboard/analytics/page.tsx
import { Metadata } from 'next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
export const metadata: Metadata = {
title: 'Analytics',
description: 'Detailed analytics and metrics',
}
export default function AnalyticsPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Analytics</h1>
<p className="text-muted-foreground">
Detailed metrics and performance analytics.
</p>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="traffic">Traffic</TabsTrigger>
<TabsTrigger value="engagement">Engagement</TabsTrigger>
<TabsTrigger value="conversion">Conversion</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Performance Overview</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px] flex items-center justify-center border border-dashed rounded-md">
<p className="text-muted-foreground">Chart visualization would go here</p>
</div>
</CardContent>
</Card>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Top Referrers</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[200px] flex items-center justify-center border border-dashed rounded-md">
<p className="text-muted-foreground">Referrers table would go here</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>User Demographics</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[200px] flex items-center justify-center border border-dashed rounded-md">
<p className="text-muted-foreground">Demographics chart would go here</p>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="traffic" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Traffic Sources</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px] flex items-center justify-center border border-dashed rounded-md">
<p className="text-muted-foreground">Traffic sources chart would go here</p>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Other tab contents would follow the same pattern */}
</Tabs>
</div>
)
}
Implementing Dark/Light Mode
1. Theme Provider
Create a theme provider component:
// components/theme-provider.tsx
"use client"
import { createContext, useContext, useEffect, useState } from "react"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(defaultTheme)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
setTheme(theme)
localStorage.setItem("theme", theme)
},
}
return (
<ThemeProviderContext.Provider value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}
2. Theme Toggle Component
Create a toggle for switching between themes:
// components/theme-toggle.tsx
"use client"
import { Moon, Sun } from 'lucide-react'
import { useTheme } from "@/components/theme-provider"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-9 w-9">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
3. Adding Theme Provider to Layout
Add the theme provider to your root layout:
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
import "./globals.css"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</ThemeProvider>
</body>
</html>
)
}
Responsive Design Considerations
Creating a responsive dashboard is crucial for providing a good user experience across all devices. Here are some key considerations:
1. Mobile-First Approach
- Design for mobile screens first, then progressively enhance for larger screens
- Use Tailwind's responsive modifiers (sm:, md:, lg:, xl:) to adjust layouts at different breakpoints
- Implement collapsible sidebars that can be toggled on smaller screens
2. Flexible Grid Layouts
- Use CSS Grid with responsive column counts to reorganize content based on screen size
- Consider stacking elements vertically on mobile and horizontally on desktop
- Adjust spacing and padding based on screen size
3. Responsive Components
- Ensure charts and data visualizations resize appropriately
- Use responsive tables that can scroll horizontally on small screens
- Implement dropdown menus for navigation on mobile
4. Media Query Hook
Create a custom hook for responsive design:
// hooks/use-media-query.ts
"use client"
import { useEffect, useState } from "react"
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false)
useEffect(() => {
const media = window.matchMedia(query)
if (media.matches !== matches) {
setMatches(media.matches)
}
const listener = () => setMatches(media.matches)
window.addEventListener("resize", listener)
return () => window.removeEventListener("resize", listener)
}, [matches, query])
return matches
}
// Usage example
// const isMobile = useMediaQuery("(max-width: 768px)")
Accessibility Considerations
Building an accessible dashboard ensures that all users, including those with disabilities, can effectively use your application.
1. Semantic HTML
- Use appropriate HTML elements (nav, main, header, footer, etc.)
- Implement proper heading hierarchy (h1, h2, h3, etc.)
- Use landmarks to help screen reader users navigate
2. Keyboard Navigation
- Ensure all interactive elements are keyboard accessible
- Implement visible focus indicators
- Create logical tab order
3. ARIA Attributes
- Add appropriate ARIA roles, states, and properties
- Use aria-label for elements without visible text
- Implement aria-expanded for collapsible elements
4. Color Contrast
- Ensure sufficient contrast between text and background colors
- Don't rely solely on color to convey information
- Test your dashboard with color blindness simulators
5. Screen Reader Support
// Example of accessible chart component
<div role="region" aria-label="Monthly revenue chart">
<h3 id="chart-title">Revenue Overview</h3>
<div aria-describedby="chart-description">
{/* Chart component */}
</div>
<div id="chart-description" className="sr-only">
This chart shows monthly revenue from January to July, with the highest revenue of $600 in July.
</div>
<table className="sr-only">
<caption>Monthly Revenue Data</caption>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Revenue</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">January</th>
<td>$400</td>
</tr>
{/* Additional rows */}
</tbody>
</table>
</div>
Best Practices
- ✓Component Composition
Break down your dashboard into small, reusable components that can be composed together.
- ✓Performance Optimization
Use code splitting, lazy loading, and memoization to improve dashboard performance.
- ✓Consistent Design Language
Maintain consistent spacing, typography, and color schemes throughout your dashboard.
- ✓Error Handling
Implement proper error boundaries and fallback UI for when data fetching fails.
- ✓Loading States
Show appropriate loading indicators when fetching data to improve user experience.
Additional Resources
Ready to Build Your Dashboard?
Start creating a modern, responsive dashboard with Next.js and Tailwind CSS today!