Overview
An admin dashboard is the project that finally puts Bootstrap's grid system to real work instead of a two-column demo. A dashboard genuinely needs a persistent navigation rail down one side, a scrollable content area on the other, a row of at-a-glance stat cards, and a real data table underneath — four different layout jobs, all handled by the same 12-column grid plus a handful of components, with no custom CSS grid or flexbox rules of your own required.
In this tutorial you will build a dashboard for a fictional storefront called Nimbus Admin: a sidebar with five nav links that stays fixed on desktop and tucks away into a slide-out offcanvas panel on mobile, three colored stat cards summarizing revenue, orders, and new customers, and a striped, hoverable table of recent orders.
- A responsive 12-column grid shell splitting the page into a sidebar column and a main content column.
- A sidebar nav (`nav flex-column`) that is visible by default from the `md` breakpoint up.
- A mobile-only topbar with a hamburger button that opens the same navigation in a Bootstrap `offcanvas` panel below `md`.
- Three colored stat `card`s (`text-bg-primary`, `text-bg-success`, `text-bg-warning`) laid out with `row-cols-*`.
- A `table-striped table-hover` data table of recent orders, wrapped so it scrolls instead of overflowing on small screens.
Prerequisites
- Bootstrap's grid system — `container`/`container-fluid`, `row`, and `col-*`/`col-md-*`/`col-lg-*`.
- Responsive breakpoints, and how classes like `d-none`/`d-md-block` change behavior at different screen widths.
- The `card` and `table` components at a basic level — this project goes deeper with both.
- How Bootstrap's JavaScript components (like the offcanvas panel in Step 4) are driven by `data-bs-*` attributes rather than custom JavaScript you write yourself.
Project Structure
The whole dashboard lives in one file, `dashboard.html`, pulling in Bootstrap's compiled CSS and JS bundle straight from the jsDelivr CDN — no build step, no `npm install`, just two tags. Inside `<body>`, a `container-fluid` holds a single `row` split into exactly two columns: a `<nav>` sidebar and a `<main>` content area, sized so their widths always add up to 12 at every breakpoint (`col-md-3`/`col-md-9`, then `col-lg-2`/`col-lg-10` on wider screens).
Below the `md` breakpoint, the sidebar column is hidden outright (`d-none d-md-block`) and replaced by a slim topbar with a button that opens an `offcanvas` panel containing the same navigation — the standard Bootstrap pattern for "sidebar on desktop, drawer on mobile", and it needs zero custom media queries.
Step 1: Set Up the Page and Load Bootstrap
Start with a standard HTML5 document and load Bootstrap's CSS in `<head>` and its JS bundle just before `</body>`. Loading the script at the end, not in `<head>`, lets the page's HTML render immediately instead of pausing to fetch and run JavaScript first — and it's what makes the offcanvas toggle in Step 4 and every other interactive component actually work, since `bootstrap.bundle.min.js` also includes Popper, which some components depend on internally.
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Required for Bootstrap's grid breakpoints to respond to real device widths --> <title>Nimbus Admin - Dashboard</title> <!-- Bootstrap's compiled CSS - gives every class used below (container-fluid, row, col-*, card, table, offcanvas, etc.) its actual styling --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"></head><body>
<!-- Grid shell, sidebar, topbar, cards, and table are added in the steps that follow -->
<!-- Bootstrap's JS bundle (includes Popper) - loaded last so it doesn't block the page from rendering; this is what powers the offcanvas toggle in Step 4 --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script></body></html>Step 2: Build the Grid Shell (Sidebar + Main Columns)
The dashboard's whole layout comes down to one `row` split into two columns whose widths sum to 12. `container-fluid` is used instead of `container` because a dashboard should fill the browser's full width, not be capped and centered like a marketing page. A couple of lines of plain CSS (not from Bootstrap) are also needed here so the sidebar's dark background stretches the full viewport height instead of stopping wherever its nav links end.
<!-- Add to <head>, after the Bootstrap CSS <link> - min-height is the one thing Bootstrap's grid classes don't give you for free, since a column's height normally comes from its own content --><style> .sidebar { min-height: 100vh; }</style>
<!-- Add inside <body> --><div class="container-fluid"> <!-- container-fluid, not container: a dashboard should use the full viewport width --> <div class="row"> <!-- row is the required horizontal wrapper every column below must sit inside -->
<!-- Sidebar column: hidden by default (d-none) and only shown from the md breakpoint up (d-md-block) - below md, the offcanvas built in Step 4 takes over --> <nav class="col-md-3 col-lg-2 d-none d-md-block bg-dark sidebar"> <!-- content added in Step 3 --> </nav>
<!-- Main column takes the remaining width. col-md-9/col-lg-10 pairs with the sidebar's col-md-3/col-lg-2 so together they always sum to 12 --> <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4"> <!-- topbar, cards, and table added in later steps --> </main>
</div></div>Step 3: Build the Sidebar Navigation
Inside the sidebar `<nav>`, the links are stacked with `nav flex-column` — Bootstrap's `.nav` class defaults to a horizontal row, and `flex-column` is the one utility class that switches it to vertical without writing custom flexbox CSS. `aria-current="page"` on the active link tells assistive technology which item represents the page the user is currently on, which is information sighted users get for free from the highlighted styling alone.
<!-- Goes inside the <nav class="... sidebar"> from Step 2 --><div class="position-sticky pt-3"> <!-- sticky so the nav stays visible while main content scrolls past it --> <h5 class="text-white px-3">Nimbus Admin</h5> <ul class="nav flex-column"> <!-- flex-column stacks items vertically instead of .nav's default horizontal row --> <li class="nav-item"> <a class="nav-link active text-white" href="#" aria-current="page"> <!-- aria-current="page": tells assistive tech this is the current page, not just a styled link --> Dashboard </a> </li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Orders</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Products</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Customers</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Settings</a></li> </ul></div>Step 4: Add a Mobile Topbar with an Offcanvas Toggle
Below `md`, the sidebar is invisible (`d-none`), so a topbar with a toggle button is added instead — visible only below `md` via `d-md-none`, the exact mirror image of the sidebar's own breakpoint classes. The button carries `data-bs-toggle="offcanvas"` and `data-bs-target="#sidebarOffcanvas"`; Bootstrap's JS bundle reads those two attributes on its own, so no click handler needs to be written by hand.
<!-- Add directly inside <main>, above everything else - d-md-none means this whole topbar only exists below md, since from md up the sidebar is already visible --><nav class="navbar navbar-dark bg-dark d-md-none"> <div class="container-fluid"> <span class="navbar-brand">Nimbus Admin</span> <button class="btn btn-outline-light" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarOffcanvas" aria-controls="sidebarOffcanvas"> <!-- data-bs-toggle/data-bs-target are read by bootstrap.bundle.min.js - no onclick attribute or JavaScript file of your own is needed --> ☰ Menu </button> </div></nav>
<!-- The offcanvas panel itself - placed anywhere in <body>, since it's positioned off-screen by CSS until the button above toggles it open --><div class="offcanvas offcanvas-start bg-dark text-white" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel"> <div class="offcanvas-header"> <h5 class="offcanvas-title" id="sidebarOffcanvasLabel">Nimbus Admin</h5> <!-- data-bs-dismiss="offcanvas" closes the panel; btn-close-white flips the default dark close icon to white so it's visible on this dark background --> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button> </div> <div class="offcanvas-body"> <!-- Same nav markup as Step 3, duplicated here because the offcanvas is a separate DOM node, not a copy that moves the desktop sidebar around --> <ul class="nav flex-column"> <li class="nav-item"><a class="nav-link active text-white" href="#">Dashboard</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Orders</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Products</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Customers</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Settings</a></li> </ul> </div></div>Step 5: Add Stat Cards
The three summary cards use `row-cols-1 row-cols-md-3` to control how many sit per row at each breakpoint — one per row on phones, three from `md` up — without a single custom media query. `text-bg-*` sets both a background color and a matching, readable text color together, which is why it is preferred here over combining `bg-*` and `text-*` separately. `h-100` keeps all three cards the same height regardless of how much text each one holds.
<!-- Add inside <main>, below the topbar from Step 4 --><div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom"> <h1 class="h2">Dashboard</h1></div>
<!-- g-4: consistent gutter spacing between cards, both horizontally and vertically --><div class="row row-cols-1 row-cols-md-3 g-4 mb-4"> <div class="col"> <div class="card text-bg-primary h-100"> <!-- text-bg-primary: background + readable text color in one class --> <div class="card-body"> <h5 class="card-title">Revenue</h5> <p class="card-text fs-3">$48,290</p> <p class="card-text"><small>+12% vs last month</small></p> </div> </div> </div> <div class="col"> <div class="card text-bg-success h-100"> <div class="card-body"> <h5 class="card-title">Orders</h5> <p class="card-text fs-3">1,204</p> <p class="card-text"><small>+5% vs last month</small></p> </div> </div> </div> <div class="col"> <div class="card text-bg-warning h-100"> <div class="card-body"> <h5 class="card-title">New Customers</h5> <p class="card-text fs-3">86</p> <p class="card-text"><small>-2% vs last month</small></p> </div> </div> </div></div>Step 6: Add a Data Table
The recent-orders table wraps in `table-responsive`, which turns the table into its own independently scrolling box below its breakpoint — the table itself stays intact and readable, instead of squeezing every column or forcing the whole page to scroll sideways. `table-striped` shades alternating rows so a reader's eye can track one row across several columns; `table-hover` highlights whichever row the cursor is over for the same reason.
<!-- Add inside <main>, below the stat cards from Step 5 --><h2 class="h4">Recent Orders</h2><!-- table-responsive: scrolls horizontally on its own below its breakpoint, instead of letting a wide table blow out the whole page's width --><div class="table-responsive"> <table class="table table-striped table-hover align-middle"> <thead> <tr> <th scope="col">Order #</th> <th scope="col">Customer</th> <th scope="col">Product</th> <th scope="col">Status</th> <th scope="col">Total</th> </tr> </thead> <tbody> <tr> <td>#10231</td> <td>Ravi Kapoor</td> <td>Wireless Mouse</td> <td><span class="badge text-bg-success">Shipped</span></td> <td>$24.99</td> </tr> <tr> <td>#10232</td> <td>Meera Iyer</td> <td>Mechanical Keyboard</td> <td><span class="badge text-bg-warning">Processing</span></td> <td>$89.00</td> </tr> <tr> <td>#10233</td> <td>Farhan Ali</td> <td>USB-C Hub</td> <td><span class="badge text-bg-danger">Cancelled</span></td> <td>$34.50</td> </tr> <tr> <td>#10234</td> <td>Sana Sheikh</td> <td>27" Monitor</td> <td><span class="badge text-bg-success">Shipped</span></td> <td>$219.99</td> </tr> </tbody> </table></div>Complete Code
Here is the full dashboard with every piece from Steps 1 through 6 assembled in order.
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Nimbus Admin - Dashboard</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <style> .sidebar { min-height: 100vh; } </style></head><body>
<div class="container-fluid"> <div class="row">
<nav class="col-md-3 col-lg-2 d-none d-md-block bg-dark sidebar"> <div class="position-sticky pt-3"> <h5 class="text-white px-3">Nimbus Admin</h5> <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link active text-white" href="#" aria-current="page">Dashboard</a> </li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Orders</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Products</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Customers</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Settings</a></li> </ul> </div> </nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<nav class="navbar navbar-dark bg-dark d-md-none"> <div class="container-fluid"> <span class="navbar-brand">Nimbus Admin</span> <button class="btn btn-outline-light" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarOffcanvas" aria-controls="sidebarOffcanvas"> ☰ Menu </button> </div> </nav>
<div class="offcanvas offcanvas-start bg-dark text-white" tabindex="-1" id="sidebarOffcanvas" aria-labelledby="sidebarOffcanvasLabel"> <div class="offcanvas-header"> <h5 class="offcanvas-title" id="sidebarOffcanvasLabel">Nimbus Admin</h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button> </div> <div class="offcanvas-body"> <ul class="nav flex-column"> <li class="nav-item"><a class="nav-link active text-white" href="#">Dashboard</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Orders</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Products</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Customers</a></li> <li class="nav-item"><a class="nav-link text-white-50" href="#">Settings</a></li> </ul> </div> </div>
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom"> <h1 class="h2">Dashboard</h1> </div>
<div class="row row-cols-1 row-cols-md-3 g-4 mb-4"> <div class="col"> <div class="card text-bg-primary h-100"> <div class="card-body"> <h5 class="card-title">Revenue</h5> <p class="card-text fs-3">$48,290</p> <p class="card-text"><small>+12% vs last month</small></p> </div> </div> </div> <div class="col"> <div class="card text-bg-success h-100"> <div class="card-body"> <h5 class="card-title">Orders</h5> <p class="card-text fs-3">1,204</p> <p class="card-text"><small>+5% vs last month</small></p> </div> </div> </div> <div class="col"> <div class="card text-bg-warning h-100"> <div class="card-body"> <h5 class="card-title">New Customers</h5> <p class="card-text fs-3">86</p> <p class="card-text"><small>-2% vs last month</small></p> </div> </div> </div> </div>
<h2 class="h4">Recent Orders</h2> <div class="table-responsive"> <table class="table table-striped table-hover align-middle"> <thead> <tr> <th scope="col">Order #</th> <th scope="col">Customer</th> <th scope="col">Product</th> <th scope="col">Status</th> <th scope="col">Total</th> </tr> </thead> <tbody> <tr> <td>#10231</td> <td>Ravi Kapoor</td> <td>Wireless Mouse</td> <td><span class="badge text-bg-success">Shipped</span></td> <td>$24.99</td> </tr> <tr> <td>#10232</td> <td>Meera Iyer</td> <td>Mechanical Keyboard</td> <td><span class="badge text-bg-warning">Processing</span></td> <td>$89.00</td> </tr> <tr> <td>#10233</td> <td>Farhan Ali</td> <td>USB-C Hub</td> <td><span class="badge text-bg-danger">Cancelled</span></td> <td>$34.50</td> </tr> <tr> <td>#10234</td> <td>Sana Sheikh</td> <td>27" Monitor</td> <td><span class="badge text-bg-success">Shipped</span></td> <td>$219.99</td> </tr> </tbody> </table> </div>
</main> </div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script></body></html>Sample Run
Click Run to see what this code prints.
Extend This Project
- Add a Bootstrap `dropdown` in the topbar for a user account menu (profile, settings, log out).
- Add `breadcrumb`s above the page title so deeper dashboard pages show their place in the hierarchy.
- Give the active sidebar link a couple of lines of vanilla JavaScript so it updates automatically based on which page is open, instead of being hardcoded.
- Add a light/dark mode toggle using Bootstrap 5.3's `data-bs-theme` attribute on `<html>`.
- Add `pagination` below the orders table once the order list is long enough to need it.
Summary
You built a complete admin dashboard using only Bootstrap's grid, card, table, and offcanvas components — a fixed sidebar and scrollable main area from the grid, at-a-glance metrics from colored cards, tabular data from a striped hoverable table, and a mobile-friendly navigation drawer from the offcanvas component, all wired together with `data-bs-*` attributes instead of custom JavaScript. That combination — grid for structure, components for content, utilities for the responsive edge cases — is the pattern nearly every real Bootstrap dashboard follows.