EmpireUI
Get Pro
← Blog9 min read#tailwind#admin#dashboard

Admin Dashboard in Tailwind: Full Layout With Charts and Tables

Build a production-ready admin dashboard with Tailwind CSS — sidebar nav, stat cards, Recharts data viz, and sortable tables in one clean layout.

Code editor showing admin dashboard layout with charts and data tables

What You're Actually Building

Let's skip the fluff. This article walks you through a full admin dashboard layout using Tailwind CSS — sidebar, topbar, stat cards, a line chart, and a data table — all wired together in a single-page React component. No opinionated UI kit forcing its design system on you. Just Tailwind utility classes and one charting library.

The layout follows a classic three-region pattern: a fixed 240px sidebar on the left, a sticky topbar across the top, and a scrollable main content area that holds everything else. You've almost certainly used a dashboard that looks exactly like this. There's a reason — it works.

Worth noting: this setup is component-based but not tied to any specific routing library. Drop it into Next.js App Router, Vite + React Router, or a plain CRA project and it'll behave the same. The examples use React 18 with TypeScript but the concepts translate to plain JS without a rewrite.

Sidebar and Topbar Layout

Start with your shell. The outer wrapper is a flex h-screen overflow-hidden div — that overflow-hidden is doing real work here. Without it, the sidebar and main area scroll together instead of independently, which breaks the sticky sidebar illusion instantly.

// DashboardShell.tsx
export function DashboardShell({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen overflow-hidden bg-gray-950 text-gray-100">
      <Sidebar />
      <div className="flex flex-col flex-1 overflow-hidden">
        <Topbar />
        <main className="flex-1 overflow-y-auto p-6">{children}</main>
      </div>
    </div>
  )
}

The sidebar itself is w-60 shrink-0 border-r border-white/10 flex flex-col. That shrink-0 stops it from collapsing when content gets wide. Each nav item is a simple anchor with flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-white/5 transition-colors — nothing special, but the 10px vertical padding (2.5 × 4px base) hits the right touch target without feeling bloated.

Topbar is h-14 border-b border-white/10 flex items-center justify-between px-6. Fixed height, flex, done. If you want a search input in the center, add flex-1 max-w-sm mx-auto to the input wrapper. Honestly, most admin topbars don't need anything fancier than a page title on the left and an avatar button on the right.

One more thing — mobile. Wrap your sidebar in a hidden lg:flex and add a hamburger in the topbar for small screens. The sidebar becomes a sheet/drawer on mobile. That's a separate component entirely; don't try to do it with CSS alone or you'll end up with a pile of translate-x hacks you'll hate in three months.

Stat Cards: The First Thing Users See

Stat cards are the MVP of any dashboard. Users hit your page and immediately scan the four numbers at the top. Get this wrong and the whole dashboard feels untrustworthy before they've scrolled a pixel.

const stats = [
  { label: 'Total Revenue', value: '$48,295', change: '+12.5%', up: true },
  { label: 'Active Users', value: '3,842', change: '+4.1%', up: true },
  { label: 'Churn Rate', value: '2.3%', change: '-0.4%', up: false },
  { label: 'Avg. Session', value: '4m 12s', change: '+8s', up: true },
]

export function StatCards() {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
      {stats.map((s) => (
        <div key={s.label} className="rounded-xl border border-white/10 bg-white/5 p-5">
          <p className="text-sm text-gray-400">{s.label}</p>
          <p className="text-3xl font-semibold mt-1">{s.value}</p>
          <p className={`text-xs mt-2 ${s.up ? 'text-emerald-400' : 'text-red-400'}`}>
            {s.change} vs last month
          </p>
        </div>
      ))}
    </div>
  )
}

That bg-white/5 trick is underrated. On a dark background it reads as a subtle card lift without needing a box-shadow. Pair it with border-white/10 and you get definition without hard edges. If your background is light, flip those to bg-black/5 and border-black/10.

In practice, the change indicator color matters more than people think. Red for anything negative, green for positive. Don't get creative here — this isn't the place for orange or amber. Users have been trained by spreadsheets since 1995 and their eyes parse red/green in under 200ms.

Adding Charts With Recharts

Recharts is the right call for most admin dashboards in 2026. It's built on D3, has solid React bindings, and doesn't weigh 400kb like some alternatives. Run npm install recharts and you're ready.

import {
  LineChart, Line, XAxis, YAxis, CartesianGrid,
  Tooltip, ResponsiveContainer
} from 'recharts'

const data = [
  { month: 'Jan', revenue: 4200 },
  { month: 'Feb', revenue: 5800 },
  { month: 'Mar', revenue: 5100 },
  { month: 'Apr', revenue: 7400 },
  { month: 'May', revenue: 6900 },
  { month: 'Jun', revenue: 9200 },
]

export function RevenueChart() {
  return (
    <div className="rounded-xl border border-white/10 bg-white/5 p-5 mb-6">
      <h2 className="text-sm font-medium text-gray-400 mb-4">Revenue (6 months)</h2>
      <ResponsiveContainer width="100%" height={240}>
        <LineChart data={data}>
          <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
          <XAxis dataKey="month" tick={{ fill: '#9ca3af', fontSize: 12 }} axisLine={false} tickLine={false} />
          <YAxis tick={{ fill: '#9ca3af', fontSize: 12 }} axisLine={false} tickLine={false} />
          <Tooltip
            contentStyle={{ background: '#111', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8 }}
            labelStyle={{ color: '#e5e7eb' }}
          />
          <Line type="monotone" dataKey="revenue" stroke="#818cf8" strokeWidth={2} dot={false} />
        </LineChart>
      </ResponsiveContainer>
    </div>
  )
}

The ResponsiveContainer is non-negotiable. Don't hard-code a pixel width for the chart — you'll regret it the moment someone opens the sidebar or resizes the browser. Set height to a fixed px value (240px works well for secondary charts, 320px for primary ones) and let the width stay fluid.

Quick aside: custom Tooltip styling is where most implementations fall apart. The default Recharts tooltip looks like it's from 2014. Pass a contentStyle object directly — background, border, border-radius, padding. Match it to your card style and it'll feel native. See the code above for the exact values that work on a dark #111 background.

For a BarChart on a separate card, just swap LineChart/Line for BarChart/Bar and add fill="#818cf8" instead of stroke. The API is identical — that's one of Recharts' genuine strengths.

Data Tables That Don't Suck

Tables are where most Tailwind dashboard tutorials phone it in. They paste a plain <table> with border-collapse and call it done. But a real admin table needs: alternating row shading, a sticky header, column sorting indicators, and a loading skeleton. Let's do all four.

const columns = ['Order ID', 'Customer', 'Amount', 'Status', 'Date']
const rows = [
  { id: '#4821', customer: 'Alice Park', amount: '$340', status: 'Paid', date: 'Aug 10' },
  { id: '#4820', customer: 'Ben Müller', amount: '$120', status: 'Pending', date: 'Aug 10' },
  { id: '#4819', customer: 'Carlos Lima', amount: '$980', status: 'Paid', date: 'Aug 9' },
  { id: '#4818', customer: 'Dana Wu', amount: '$55', status: 'Failed', date: 'Aug 9' },
]

const statusColors: Record<string, string> = {
  Paid: 'bg-emerald-500/15 text-emerald-400',
  Pending: 'bg-amber-500/15 text-amber-400',
  Failed: 'bg-red-500/15 text-red-400',
}

export function OrdersTable() {
  return (
    <div className="rounded-xl border border-white/10 overflow-hidden">
      <table className="w-full text-sm">
        <thead className="bg-white/5 sticky top-0">
          <tr>
            {columns.map((col) => (
              <th key={col} className="text-left text-xs font-medium text-gray-400 px-4 py-3">
                {col}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map((row, i) => (
            <tr key={row.id} className={i % 2 === 0 ? 'bg-transparent' : 'bg-white/[0.02]'}>
              <td className="px-4 py-3 text-gray-300 font-mono">{row.id}</td>
              <td className="px-4 py-3">{row.customer}</td>
              <td className="px-4 py-3 font-medium">{row.amount}</td>
              <td className="px-4 py-3">
                <span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-medium ${statusColors[row.status]}`}>
                  {row.status}
                </span>
              </td>
              <td className="px-4 py-3 text-gray-400">{row.date}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

That bg-white/[0.02] on alternating rows is subtle — barely visible — but it helps users track horizontal rows across 8+ columns without needing full border lines. Full horizontal borders on every row feel heavy. The odd-row tint is cleaner.

Status badges deserve their own treatment. The bg-emerald-500/15 text-emerald-400 pattern (15% opacity background, full-saturation text) gives you a pill badge that reads on both dark and light backgrounds without being garish. Avoid solid color backgrounds for badges in tables — they compete with the data itself.

Look, if you need sorting, reach for TanStack Table v8 (@tanstack/react-table). Don't hand-roll a sort comparator for a production table — TanStack handles pagination, sorting, filtering, and column visibility out of the box. Use your Tailwind classes for the UI shell and let TanStack own the logic. Your future self will thank you.

Putting the Full Layout Together

Here's what the final page component looks like wired up:

// app/dashboard/page.tsx (Next.js App Router)
import { DashboardShell } from '@/components/dashboard/DashboardShell'
import { StatCards } from '@/components/dashboard/StatCards'
import { RevenueChart } from '@/components/dashboard/RevenueChart'
import { OrdersTable } from '@/components/dashboard/OrdersTable'

export default function DashboardPage() {
  return (
    <DashboardShell>
      <div className="max-w-7xl mx-auto">
        <h1 className="text-xl font-semibold mb-6">Overview</h1>
        <StatCards />
        <RevenueChart />
        <h2 className="text-sm font-medium text-gray-400 mb-3">Recent Orders</h2>
        <OrdersTable />
      </div>
    </DashboardShell>
  )
}

That max-w-7xl mx-auto on the inner wrapper is important. Without it, your content stretches to fill ultra-wide monitors and the stats cards become absurdly wide. A 1280px cap keeps the layout readable on a 27-inch display.

If you want a two-column section — say, the revenue chart on the left and a donut chart on the right — wrap them in grid grid-cols-1 lg:grid-cols-3 gap-4 with the line chart taking lg:col-span-2. That gives you a 2/3 + 1/3 split that collapses to full-width on tablet. It's the same grid pattern bento grid layouts use, just more structured.

For dark/light mode support, check out the tailwind dark mode article — the dark: variant approach works perfectly with this shell. You'd swap bg-gray-950 for bg-white and text-gray-100 for text-gray-900 using a class-based strategy on the <html> element.

Polish, Performance, and What's Next

A few things that separate a shipped dashboard from a demo. First: loading skeletons. Before your data fetches, render a skeleton version of the stat cards and table rows using animate-pulse bg-white/10 rounded divs at the same dimensions. It's 20 lines of markup and it makes the app feel 10x more professional.

Second: chart responsiveness on resize. Recharts' ResponsiveContainer re-measures on window resize, but it can cause layout thrash. Debounce your resize handler if you're doing anything custom, and make sure chart containers don't have display: contents parents — that breaks the resize observer.

Third — and this is the one teams skip — keyboard navigation and focus management in the sidebar. Every nav item needs a visible :focus-visible ring. Tailwind v4 ships focus-visible:ring-2 focus-visible:ring-indigo-500 as a utility and it's the easiest accessibility win you'll ever add. Do it before you ship.

If you want prebuilt components to drop into this layout instead of building from scratch, browse the Empire UI library. There are card components, animated stat displays, and gradient-based decorative elements that slot straight into the pattern above. And if your dashboard needs to look dramatically different — more cyberpunk, more glassmorphic — the glassmorphism components and style hubs like aurora or vaporwave give you a visual direction without starting from zero.

FAQ

Can I use this Tailwind admin layout with Next.js App Router?

Yes, the shell component is just a layout wrapper — drop it into app/dashboard/layout.tsx and it works as a persistent layout. Server components render inside <main>, client components (like charts) stay isolated.

Which charting library works best with Tailwind dashboards?

Recharts is the practical choice — React-native API, good TypeScript support, and custom Tooltip/styling that actually works. Victory and Chart.js with react-chartjs-2 are alternatives, but Recharts' component model fits React patterns most naturally.

How do I add sorting to the data table?

Use TanStack Table v8 (@tanstack/react-table). It handles sort state, pagination, and filtering while you own all the Tailwind markup. Don't hand-roll sorting logic for anything beyond a toy example.

How do I make the sidebar collapsible?

Track a collapsed boolean in state, toggle the sidebar width between w-60 and w-16 with a Tailwind transition (transition-all duration-200), and hide text labels with hidden when collapsed. Icons stay visible; labels slide away.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Tailwind Dashboard Layout: Sidebar, Header and Content GridStat Card in Tailwind: KPI Metrics, Delta Indicator, SparklineGlassmorphism Dashboard: Full Admin UI with Frosted-Glass CardsChart Dashboard in React: Recharts, Filters, Export to PNG