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.
- 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 menuoverlay.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>Sample Run
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.