LearnAI ToolsCareerPractice BuildsPlayContact
Tailwind CSSIntermediate~2 hours

Admin Dashboard Layout

Build a sidebar + content dashboard layout using flexbox and grid.

FlexboxGridLayout

Overview

An admin dashboard is where Flexbox and Grid stop being separate topics and start working together in the same layout: Flexbox handles the big-picture skeleton — a sidebar sitting beside a main content column — while Grid handles the evenly-spaced stat cards inside that main content area. This project builds both halves, plus a mobile off-canvas sidebar that slides in over the content instead of squeezing it.

The layout deliberately separates two different jobs: `flex` at the top level for "these pieces sit in a row/column relative to each other," and `grid` inside the main content for "these cards form a precise, evenly-sized matrix." Knowing when to reach for which is exactly the skill this project builds.

What You'll Build
  • A full-height app shell with a fixed-width sidebar and a flexible main content area, built with Flexbox.
  • A dark sidebar with a logo and a vertical nav list.
  • A top header bar with a mobile-only hamburger button and a user avatar.
  • A responsive stat-card Grid (1 → 2 → 4 columns) inside the scrollable main content area.
  • A mobile off-canvas sidebar that slides in over a dimmed backdrop, toggled by real JavaScript.

Prerequisites

  • Flexbox Utilities — `flex`, `flex-1`, `flex-col`, `shrink-0`, and how flex children share available space.
  • Grid Utilities — `grid-cols-*` and `gap-*` for the stat-card row inside the main content area.
  • Responsive prefixes — `md:` in particular, used throughout to distinguish mobile from desktop behavior.
  • Sizing utilities — `w-64`, `h-16`, `h-screen`, and the difference between a fixed and a flexible dimension.
  • Basic DOM JavaScript — `classList.add/remove` and `addEventListener`, for the mobile sidebar toggle.

Project Structure

This is again a single HTML file with the Tailwind Play CDN script in `<head>`. Structurally the page has three moving parts: a fixed/off-canvas `<aside>` sidebar, a `<header>` bar, and a scrollable `<main>` — the header and main are wrapped together in their own flex column so the sidebar can stay full-height and independent of whatever the main content does.

Step 1: Build the Page Shell with Flexbox

The very first decision is the outermost container: `h-screen` locks it to exactly the viewport height, and `flex` lays its two direct children — the sidebar and everything to its right — out side by side as a single horizontal row.

<!-- h-screen locks the whole shell to exactly the viewport height (no page-level scrolling) -->
<!-- flex lays the sidebar and the right-hand column out SIDE BY SIDE, as the two direct children -->
<body class="h-screen flex bg-slate-100 overflow-hidden">
<!-- Sidebar: fixed width, Step 2 fills this in -->
<aside class="w-64 shrink-0 bg-slate-900 text-white flex flex-col">
<!-- shrink-0 stops flex from squeezing the sidebar narrower than w-64 when space gets tight —
it's the MAIN content area that should give way under pressure, never the sidebar -->
</aside>
<!-- Right-hand side: its OWN vertical flex column, stacking the header above the scrollable content -->
<div class="flex-1 flex flex-col min-w-0">
<!-- flex-1 makes this fill all leftover horizontal space beside the fixed-width sidebar -->
<!-- min-w-0 is a classic flexbox fix: without it, a flex child won't shrink below its content's
natural width, which can silently push the sidebar off-screen on a narrow viewport -->
</div>
</body>

Step 2: Build the Sidebar Navigation

The sidebar itself becomes its own `flex flex-col`, stacking a logo header above a scrollable nav list. It also gets the classes that make Step 5's mobile off-canvas behavior possible: `fixed` positioning and a `-translate-x-full` starting position below the `md` breakpoint, both cancelled out again from `md` up where the sidebar behaves as a normal, permanently visible column.

<aside id="sidebar" class="w-64 shrink-0 bg-slate-900 text-white flex flex-col
fixed md:static inset-y-0 left-0 z-30
-translate-x-full md:translate-x-0 transition-transform duration-300">
<!-- fixed md:static: an OFF-CANVAS overlay below md (can slide over the page), but a normal
in-flow column from md up, where there's permanent room for it beside the content -->
<!-- -translate-x-full md:translate-x-0: starts slid fully off-screen to the left below md;
Step 5's JS removes -translate-x-full to slide it in — from md up it's always visible -->
<div class="h-16 flex items-center px-6 font-bold text-lg border-b border-slate-800">
Aurora Admin
</div>
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
<!-- space-y-1 adds a small vertical gap BETWEEN each link, without margin on every individual one -->
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg bg-slate-800 text-white">Dashboard</a>
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white">Users</a>
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white">Orders</a>
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white">Settings</a>
</nav>
</aside>

Step 3: Build the Top Header Bar

The header sits inside the right-hand flex column from Step 1, above `<main>`. `md:hidden` on the hamburger button is what makes it disappear entirely from `md` up — on desktop, the sidebar is already permanently visible, so there is nothing left for that button to toggle.

<header class="h-16 shrink-0 bg-white border-b border-slate-200 flex items-center justify-between px-6">
<div class="flex items-center gap-4">
<!-- md:hidden: this button ONLY exists below the md breakpoint — it's the sole way to open
the off-canvas sidebar from Step 2 on a phone or narrow tablet -->
<button id="sidebar-toggle" class="md:hidden text-slate-600 text-2xl leading-none">☰</button>
<h1 class="text-lg font-semibold text-slate-900">Dashboard</h1>
</div>
<img src="https://i.pravatar.cc/40?img=12" alt="User avatar" class="w-9 h-9 rounded-full">
</header>

Step 4: Lay Out Stat Cards with Grid

Inside `<main>`, the layout tool switches from Flexbox to Grid: four stat cards need to line up as a precise, evenly-sized matrix rather than a row that merely wraps, which is exactly the distinction between Flexbox and Grid that this project is built to highlight.

<!-- flex-1 fills the remaining vertical space below the header; overflow-y-auto makes THIS
area scroll independently, leaving the sidebar and header fixed in place -->
<main class="flex-1 overflow-y-auto p-6">
<!-- grid-cols-1 on phones, 2 on tablets, 4 once there's room for four equal cards in one row -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 mb-8">
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<p class="text-sm text-slate-500 mb-1">Total Revenue</p>
<p class="text-2xl font-bold text-slate-900">$48,230</p>
<p class="text-xs text-emerald-600 mt-1">▲ 12% this month</p>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<p class="text-sm text-slate-500 mb-1">Active Users</p>
<p class="text-2xl font-bold text-slate-900">2,845</p>
<p class="text-xs text-emerald-600 mt-1">▲ 5% this month</p>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<p class="text-sm text-slate-500 mb-1">Open Orders</p>
<p class="text-2xl font-bold text-slate-900">132</p>
<p class="text-xs text-rose-500 mt-1">▼ 2% this month</p>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<p class="text-sm text-slate-500 mb-1">Support Tickets</p>
<p class="text-2xl font-bold text-slate-900">18</p>
<p class="text-xs text-emerald-600 mt-1">▼ 9% this month</p>
</div>
</div>
<!-- A wider widget below the stat row — Grid handled the precise 4-up row above;
a single full-width block like this one doesn't need Grid at all -->
<div class="bg-white rounded-xl p-6 shadow-sm border border-slate-200">
<h2 class="font-semibold text-slate-900 mb-4">Recent Orders</h2>
<p class="text-sm text-slate-500">Order table content would go here.</p>
</div>
</main>

Step 5: Make the Sidebar Collapse on Small Screens

Two pieces finish the mobile behavior: a dimmed backdrop element (`<div id="sidebar-overlay">`) that sits behind the sidebar and in front of the page content, and a small JavaScript file that toggles both the sidebar's `-translate-x-full` class and the overlay's visibility together.

<!-- Placed as a sibling right after </aside>: hidden by default, only ever shown below md -->
<div id="sidebar-overlay" class="hidden fixed inset-0 bg-black/50 z-20 md:hidden"></div>
<!-- z-20 sits above the page content but below the sidebar's z-30, so it visually sits BEHIND
the open sidebar while still covering (and dimming/blocking clicks on) everything else -->
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.getElementById('sidebar-toggle');
const overlay = document.getElementById('sidebar-overlay');
function openSidebar() {
// Removing the -translate-x-full class added in Step 2 slides the fixed sidebar into view
sidebar.classList.remove('-translate-x-full');
overlay.classList.remove('hidden');
}
function closeSidebar() {
sidebar.classList.add('-translate-x-full');
overlay.classList.add('hidden');
}
toggleBtn.addEventListener('click', openSidebar);
// Clicking the dimmed backdrop behind the open sidebar is the standard way to dismiss an off-canvas menu
overlay.addEventListener('click', closeSidebar);

Complete Code

Here is the full dashboard shell — sidebar, header, stat grid, mobile overlay, and toggle script — assembled from every step above.

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="h-screen flex bg-slate-100 overflow-hidden">
<aside id="sidebar" class="w-64 shrink-0 bg-slate-900 text-white flex flex-col fixed md:static inset-y-0 left-0 z-30 -translate-x-full md:translate-x-0 transition-transform duration-300">
<div class="h-16 flex items-center px-6 font-bold text-lg border-b border-slate-800">Aurora Admin</div>
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg bg-slate-800 text-white">Dashboard</a>
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white">Users</a>
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white">Orders</a>
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white">Settings</a>
</nav>
</aside>
<div id="sidebar-overlay" class="hidden fixed inset-0 bg-black/50 z-20 md:hidden"></div>
<div class="flex-1 flex flex-col min-w-0">
<header class="h-16 shrink-0 bg-white border-b border-slate-200 flex items-center justify-between px-6">
<div class="flex items-center gap-4">
<button id="sidebar-toggle" class="md:hidden text-slate-600 text-2xl leading-none">☰</button>
<h1 class="text-lg font-semibold text-slate-900">Dashboard</h1>
</div>
<img src="https://i.pravatar.cc/40?img=12" alt="User avatar" class="w-9 h-9 rounded-full">
</header>
<main class="flex-1 overflow-y-auto p-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 mb-8">
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<p class="text-sm text-slate-500 mb-1">Total Revenue</p>
<p class="text-2xl font-bold text-slate-900">$48,230</p>
<p class="text-xs text-emerald-600 mt-1">▲ 12% this month</p>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<p class="text-sm text-slate-500 mb-1">Active Users</p>
<p class="text-2xl font-bold text-slate-900">2,845</p>
<p class="text-xs text-emerald-600 mt-1">▲ 5% this month</p>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<p class="text-sm text-slate-500 mb-1">Open Orders</p>
<p class="text-2xl font-bold text-slate-900">132</p>
<p class="text-xs text-rose-500 mt-1">▼ 2% this month</p>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
<p class="text-sm text-slate-500 mb-1">Support Tickets</p>
<p class="text-2xl font-bold text-slate-900">18</p>
<p class="text-xs text-emerald-600 mt-1">▼ 9% this month</p>
</div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border border-slate-200">
<h2 class="font-semibold text-slate-900 mb-4">Recent Orders</h2>
<p class="text-sm text-slate-500">Order table content would go here.</p>
</div>
</main>
</div>
<script>
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.getElementById('sidebar-toggle');
const overlay = document.getElementById('sidebar-overlay');
function openSidebar() {
sidebar.classList.remove('-translate-x-full');
overlay.classList.remove('hidden');
}
function closeSidebar() {
sidebar.classList.add('-translate-x-full');
overlay.classList.add('hidden');
}
toggleBtn.addEventListener('click', openSidebar);
overlay.addEventListener('click', closeSidebar);
</script>
</body>
</html>
Live Preview

Sample Run

What You'll See

Click Run to see what this code prints.

Extend This Project

  • Highlight the active nav link automatically by adding/removing the `bg-slate-800` classes with JavaScript based on which link was last clicked.
  • Add a collapsible "icon-only" mode for desktop too, shrinking the sidebar to `w-16` and hiding link labels with `hidden`.
  • Add a search input to the header, using `flex-1` on the input's wrapper so it fills the space between the title and the avatar.
  • Reuse the dark-mode toggle pattern from the Dark Mode Landing Page project to give the whole dashboard a `dark:` theme.
  • Replace the "Recent Orders" placeholder with a real `<table>`, styled with Tailwind's table utilities and zebra-striped rows.

Summary

You built a real admin dashboard shell that combines Flexbox and Grid deliberately rather than interchangeably: Flexbox arranged the sidebar and content as a whole-page skeleton, while Grid handled the precise, evenly-sized stat-card row inside it. The `fixed`/`-translate-x-full` off-canvas pattern you used for the mobile sidebar is the same technique behind most production dashboard and admin-panel layouts.