LearnAI ToolsCareerPractice BuildsPlayContact
CSSIntermediate~2 hours

Admin Dashboard Layout

Build a sidebar + content dashboard layout using CSS Grid and Flexbox together.

GridFlexboxCSS Variables

Overview

An admin dashboard is the classic example of a layout with two very different jobs happening at two different scales: at the page level, a sidebar, a top bar, and a content area need to be arranged relative to each other — a job CSS Grid is built for. Inside each of those regions, smaller rows of elements (nav links, a title next to a user name, a row of stat cards) need to line up along one axis — a job Flexbox is built for. This project uses both together, exactly the way real production dashboards do.

It also introduces CSS custom properties (variables). Instead of repeating the same color and spacing values across dozens of rules, this project defines them once on `:root` and reuses them everywhere with `var(--name)` — so changing the whole dashboard's theme later means editing a handful of values in one place instead of hunting through the entire stylesheet.

What You'll Build
  • A `:root` block of CSS custom properties defining the dashboard's color palette and shared sizing.
  • A named-area Grid skeleton arranging a sidebar, top bar, and content region.
  • A Flexbox-based sidebar with a brand logo and a vertical stack of nav links.
  • A Flexbox-based top bar with a title on one side and a user greeting on the other.
  • A grid of stat cards and a data table styled with the same shared CSS variables.
  • A responsive breakpoint that collapses the sidebar and restacks the stat cards on small screens.

Prerequisites

  • CSS Grid basics — `display: grid`, `grid-template-columns`/`rows`, and named `grid-template-areas`.
  • Flexbox basics — `display: flex`, `flex-direction`, and `justify-content`.
  • CSS custom properties — declaring `--name: value` on `:root` and reading it with `var(--name)`.
  • Basic table markup — `<table>`, `<thead>`, `<tbody>`, `<tr>`, `<th>`, and `<td>`.
  • Media queries — changing `grid-template-areas` and `grid-template-columns` at a given breakpoint.

Project Structure

The markup is fixed for the rest of the project: a `.dashboard` wrapper containing an `<aside class="sidebar">`, a `<header class="topbar">`, and a `<main class="content">` holding a row of `.stat-card` elements plus a `.panel` with a table. These three top-level regions — sidebar, topbar, content — map directly onto the named Grid areas built in Step 2.

<div class="dashboard">
<!-- Sidebar: brand logo plus a vertical stack of nav links -->
<aside class="sidebar">
<div class="sidebar__brand">Admin<span>Panel</span></div>
<nav class="sidebar__nav">
<a href="#" class="sidebar__link sidebar__link--active">Dashboard</a>
<a href="#" class="sidebar__link">Users</a>
<a href="#" class="sidebar__link">Orders</a>
<a href="#" class="sidebar__link">Reports</a>
<a href="#" class="sidebar__link">Settings</a>
</nav>
</aside>
<!-- Top bar: page title on the left, user greeting on the right -->
<header class="topbar">
<h1 class="topbar__title">Dashboard</h1>
<div class="topbar__user">Hi, Priya</div>
</header>
<!-- Main content: a row of stat cards, then a data table panel -->
<main class="content">
<section class="stats">
<div class="stat-card">
<p class="stat-card__label">Total Revenue</p>
<p class="stat-card__value">$48,200</p>
</div>
<div class="stat-card">
<p class="stat-card__label">New Users</p>
<p class="stat-card__value">1,204</p>
</div>
<div class="stat-card">
<p class="stat-card__label">Orders</p>
<p class="stat-card__value">386</p>
</div>
<div class="stat-card">
<p class="stat-card__label">Refunds</p>
<p class="stat-card__value">12</p>
</div>
</section>
<section class="panel">
<h2 class="panel__title">Recent Orders</h2>
<table class="panel__table">
<thead>
<tr><th>Order</th><th>Customer</th><th>Status</th><th>Amount</th></tr>
</thead>
<tbody>
<tr><td>#1042</td><td>Rohan Mehta</td><td>Shipped</td><td>$120</td></tr>
<tr><td>#1041</td><td>Asha Verma</td><td>Pending</td><td>$85</td></tr>
<tr><td>#1040</td><td>Karan Singh</td><td>Delivered</td><td>$230</td></tr>
</tbody>
</table>
</section>
</main>
</div>

Step 1: Define CSS Variables and Reset Defaults

Every color, spacing unit, and dimension the dashboard needs gets declared once as a custom property on `:root`, which makes it available anywhere in the stylesheet via `var(--name)`. Notice `--sidebar-width` and `--topbar-height` in particular — those two values will be reused in Step 2's Grid definition, guaranteeing the sizes never drift out of sync between the sidebar's own styles and the grid that positions it.

:root {
/* Centralizing values as custom properties means retheming means editing one place, not hunting through every rule */
--color-bg: #f1f5f9; /* Overall page background */
--color-surface: #ffffff; /* Cards, topbar, and panel surfaces */
--color-primary: #6366f1; /* Brand accent used for active states and highlights */
--color-text: #1e293b; /* Primary body text color */
--color-text-muted: #64748b; /* Secondary/label text color */
--color-border: #e2e8f0; /* Shared border color across cards, table, and topbar */
--sidebar-width: 240px; /* Single source of truth, reused by the Grid layout in Step 2 */
--topbar-height: 64px; /* Single source of truth, reused by the Grid layout in Step 2 */
--radius: 10px; /* Shared corner rounding across cards and panels */
--spacing: 1.5rem; /* Shared gap/padding unit reused throughout the whole layout */
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--color-bg); /* Reads from the variable instead of repeating the hex value */
color: var(--color-text);
line-height: 1.5;
}

Step 2: Build the Page Skeleton with Grid Areas

Named `grid-template-areas` is one of Grid's most readable features — each quoted string is literally one visual row of the page, and each word in it names which region occupies that cell. Here, `sidebar` spans both rows on the left while `topbar` and `content` stack on the right, exactly matching the classic dashboard layout.

.dashboard {
display: grid;
/* Each quoted row below is one visual row of the page; "sidebar" repeated in both rows means it spans them */
grid-template-areas:
"sidebar topbar"
"sidebar content";
grid-template-columns: var(--sidebar-width) 1fr; /* Fixed sidebar width, content fills all remaining space */
grid-template-rows: var(--topbar-height) 1fr; /* Fixed topbar height, content fills the remaining height */
min-height: 100vh; /* Dashboard fills at least the full viewport height */
}
.sidebar { grid-area: sidebar; } /* Assigns this element to the "sidebar" region named above */
.topbar { grid-area: topbar; }
.content { grid-area: content; }

Step 3: Style the Sidebar with Flexbox

While Grid places the sidebar as a whole region on the page, Flexbox arranges what is inside it: `flex-direction: column` stacks the brand logo above the nav links, and the nav links themselves are also a column flex container, one link per row.

.sidebar {
background: #111827; /* Deliberately not a variable — a distinct dark tone from the light surfaces */
color: #ffffff;
display: flex;
flex-direction: column; /* Stacks the brand block above the nav links, top to bottom */
padding: var(--spacing) 0;
}
.sidebar__brand {
font-size: 1.25rem;
font-weight: 700;
padding: 0 var(--spacing);
margin-bottom: 2rem;
}
.sidebar__brand span {
color: var(--color-primary); /* Two-tone logo: "Admin" in white, "Panel" in the accent color */
}
.sidebar__nav {
display: flex;
flex-direction: column; /* Links stack one per row, in source order */
}
.sidebar__link {
color: #cbd5e1;
text-decoration: none;
padding: 0.75rem var(--spacing);
border-left: 3px solid transparent; /* Reserves space for the active indicator so nothing shifts on hover */
transition: background 0.2s ease, color 0.2s ease;
}
.sidebar__link:hover {
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
}
.sidebar__link--active {
color: #ffffff;
background: rgba(99, 102, 241, 0.15);
border-left-color: var(--color-primary); /* Colored left border marks the current page */
}

Step 4: Style the Top Bar with Flexbox

The top bar is a single Flexbox row: `justify-content: space-between` pushes the page title to the left edge and the user greeting to the right edge, with `align-items: center` keeping both vertically centered against the bar's fixed height from Step 2.

.topbar {
background: var(--color-surface);
display: flex;
align-items: center;
justify-content: space-between; /* Title on the left, user info on the right, in one flex row */
padding: 0 var(--spacing);
border-bottom: 1px solid var(--color-border);
}
.topbar__title {
font-size: 1.25rem;
}
.topbar__user {
color: var(--color-text-muted);
font-size: 0.9rem;
}

Step 5: Lay Out the Stat Cards

The four stat cards form their own small Grid inside the content area — `repeat(4, 1fr)` gives four equal columns, echoing the same technique used for the pricing page's three columns. Inside each individual card, Flexbox stacks the label above the big number.

.content {
padding: var(--spacing);
overflow-y: auto; /* Lets the main content scroll independently if it ever grows taller than the viewport */
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr); /* Four equal stat cards in a row on desktop */
gap: var(--spacing);
margin-bottom: var(--spacing);
}
.stat-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1.25rem;
display: flex;
flex-direction: column; /* Label stacked directly above the big value */
gap: 0.4rem;
}
.stat-card__label {
font-size: 0.85rem;
color: var(--color-text-muted);
}
.stat-card__value {
font-size: 1.5rem;
font-weight: 700;
}

Step 6: Style the Table Panel

The recent-orders table reuses the same `var(--color-surface)`/`var(--color-border)`/`var(--radius)` variables as the stat cards above it, which is exactly why defining them once in Step 1 pays off — the panel automatically matches the rest of the dashboard's visual language without repeating any hex codes.

.panel {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: var(--spacing);
}
.panel__title {
font-size: 1.1rem;
margin-bottom: 1rem;
}
.panel__table {
width: 100%;
border-collapse: collapse; /* Merges adjacent cell borders into single lines instead of doubled ones */
}
.panel__table th,
.panel__table td {
text-align: left;
padding: 0.75rem 0.5rem;
border-bottom: 1px solid var(--color-border);
font-size: 0.9rem;
}
.panel__table th {
color: var(--color-text-muted);
font-weight: 600;
}

Step 7: Make the Dashboard Responsive

Below 800px, the grid's named areas are redefined entirely — `sidebar` is dropped from the template and the element itself is hidden, leaving just `topbar` and `content` stacked in a single column. This is the simplest workable mobile pattern for a CSS-only dashboard; the "Extend This Project" section below suggests a way to bring the sidebar back as a toggleable drawer instead.

@media (max-width: 800px) {
.dashboard {
grid-template-areas:
"topbar"
"content";
grid-template-columns: 1fr; /* Sidebar column is dropped from the layout entirely */
grid-template-rows: var(--topbar-height) 1fr;
}
.sidebar {
display: none; /* Simplest mobile approach for a CSS-only tutorial: hide the sidebar below tablet width */
}
.stats {
grid-template-columns: repeat(2, 1fr); /* Two stat cards per row instead of four on narrower screens */
}
}
@media (max-width: 420px) {
.stats {
grid-template-columns: 1fr; /* Single column of stat cards on phone-sized screens */
}
.panel__table {
font-size: 0.8rem; /* Slightly smaller text so the table doesn't force horizontal scrolling */
}
}

Complete Code

Here is the fixed HTML markup again for reference, followed by the complete stylesheet with every rule from Steps 1 through 7 assembled in order.

<div class="dashboard">
<aside class="sidebar">
<div class="sidebar__brand">Admin<span>Panel</span></div>
<nav class="sidebar__nav">
<a href="#" class="sidebar__link sidebar__link--active">Dashboard</a>
<a href="#" class="sidebar__link">Users</a>
<a href="#" class="sidebar__link">Orders</a>
<a href="#" class="sidebar__link">Reports</a>
<a href="#" class="sidebar__link">Settings</a>
</nav>
</aside>
<header class="topbar">
<h1 class="topbar__title">Dashboard</h1>
<div class="topbar__user">Hi, Priya</div>
</header>
<main class="content">
<section class="stats">
<div class="stat-card">
<p class="stat-card__label">Total Revenue</p>
<p class="stat-card__value">$48,200</p>
</div>
<div class="stat-card">
<p class="stat-card__label">New Users</p>
<p class="stat-card__value">1,204</p>
</div>
<div class="stat-card">
<p class="stat-card__label">Orders</p>
<p class="stat-card__value">386</p>
</div>
<div class="stat-card">
<p class="stat-card__label">Refunds</p>
<p class="stat-card__value">12</p>
</div>
</section>
<section class="panel">
<h2 class="panel__title">Recent Orders</h2>
<table class="panel__table">
<thead>
<tr><th>Order</th><th>Customer</th><th>Status</th><th>Amount</th></tr>
</thead>
<tbody>
<tr><td>#1042</td><td>Rohan Mehta</td><td>Shipped</td><td>$120</td></tr>
<tr><td>#1041</td><td>Asha Verma</td><td>Pending</td><td>$85</td></tr>
<tr><td>#1040</td><td>Karan Singh</td><td>Delivered</td><td>$230</td></tr>
</tbody>
</table>
</section>
</main>
</div>
:root {
--color-bg: #f1f5f9;
--color-surface: #ffffff;
--color-primary: #6366f1;
--color-text: #1e293b;
--color-text-muted: #64748b;
--color-border: #e2e8f0;
--sidebar-width: 240px;
--topbar-height: 64px;
--radius: 10px;
--spacing: 1.5rem;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--color-bg);
color: var(--color-text);
line-height: 1.5;
}
.dashboard {
display: grid;
grid-template-areas:
"sidebar topbar"
"sidebar content";
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-rows: var(--topbar-height) 1fr;
min-height: 100vh;
}
.sidebar { grid-area: sidebar; }
.topbar { grid-area: topbar; }
.content { grid-area: content; }
.sidebar {
background: #111827;
color: #ffffff;
display: flex;
flex-direction: column;
padding: var(--spacing) 0;
}
.sidebar__brand {
font-size: 1.25rem;
font-weight: 700;
padding: 0 var(--spacing);
margin-bottom: 2rem;
}
.sidebar__brand span {
color: var(--color-primary);
}
.sidebar__nav {
display: flex;
flex-direction: column;
}
.sidebar__link {
color: #cbd5e1;
text-decoration: none;
padding: 0.75rem var(--spacing);
border-left: 3px solid transparent;
transition: background 0.2s ease, color 0.2s ease;
}
.sidebar__link:hover {
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
}
.sidebar__link--active {
color: #ffffff;
background: rgba(99, 102, 241, 0.15);
border-left-color: var(--color-primary);
}
.topbar {
background: var(--color-surface);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing);
border-bottom: 1px solid var(--color-border);
}
.topbar__title {
font-size: 1.25rem;
}
.topbar__user {
color: var(--color-text-muted);
font-size: 0.9rem;
}
.content {
padding: var(--spacing);
overflow-y: auto;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing);
margin-bottom: var(--spacing);
}
.stat-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.stat-card__label {
font-size: 0.85rem;
color: var(--color-text-muted);
}
.stat-card__value {
font-size: 1.5rem;
font-weight: 700;
}
.panel {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: var(--spacing);
}
.panel__title {
font-size: 1.1rem;
margin-bottom: 1rem;
}
.panel__table {
width: 100%;
border-collapse: collapse;
}
.panel__table th,
.panel__table td {
text-align: left;
padding: 0.75rem 0.5rem;
border-bottom: 1px solid var(--color-border);
font-size: 0.9rem;
}
.panel__table th {
color: var(--color-text-muted);
font-weight: 600;
}
@media (max-width: 800px) {
.dashboard {
grid-template-areas:
"topbar"
"content";
grid-template-columns: 1fr;
grid-template-rows: var(--topbar-height) 1fr;
}
.sidebar {
display: none;
}
.stats {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 420px) {
.stats {
grid-template-columns: 1fr;
}
.panel__table {
font-size: 0.8rem;
}
}
Live Preview

Sample Run

The preview above shows the dark sidebar pinned along the left edge with its "Dashboard" link highlighted as active, a light top bar to its right showing the page title and a user greeting, and the main content area below the top bar holding four stat cards in a row above the recent-orders table. Narrow the preview past 800px and the sidebar disappears entirely, leaving a single-column layout with the stat cards regrouping from four per row, to two, to one as the width keeps shrinking.

Extend This Project

  • Bring the sidebar back on mobile as a slide-in drawer using the same checkbox-hack toggle technique from the Responsive Navbar project instead of hiding it outright.
  • Add a `data-theme="dark"` style dark mode by defining a second set of `--color-*` variable values and swapping them with a class on `<body>`.
  • Add status "pills" to the orders table (colored background badges for Shipped/Pending/Delivered) using small utility classes.
  • Add a collapsed/icon-only sidebar state (fixed narrow width, labels hidden) toggled independently from the mobile drawer.
  • Turn each `.stat-card` into a mini two-column grid internally, adding a trend icon or percentage-change value beside the big number.

Summary

You built a two-level dashboard layout: CSS Grid with named template areas for the page-level skeleton, and Flexbox for the components living inside each region, tied together with a shared set of CSS custom properties defined once on `:root`. This Grid-for-skeleton, Flexbox-for-components, variables-for-theming combination is the standard approach behind the vast majority of real admin interfaces you will encounter or build.