Capstone: Building an Admin Dashboard

Advanced
16 min

Capstone: Building an Admin Dashboard

This capstone assembles a complete admin dashboard from the pieces covered in the course: a sidebar that becomes an offcanvas drawer on small screens, a top bar with a theme toggle, stat cards, a responsive orders table with badges and dropdown actions, a form in a modal, and toast feedback. Each of the six steps leaves you with a working page, so you can stop and extend at any point.

Step 1: Page Shell and Sidebar

The layout is a full-height flex row: a fixed-width sidebar and a column that grows to fill the rest, exactly as in the sample above. The responsive trick is the .offcanvas-lg class: below the lg breakpoint the sidebar behaves as an offcanvas drawer opened by the menu button; from lg upward it renders inline as a normal column with no JavaScript involved.

Load the assets and give the content column min-width: 0 so wide tables scroll instead of stretching the layout (flex items otherwise refuse to shrink below their content width):

html
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> </head> <body> <!-- shell from the sample code --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script> </body>

Step 2: Top Bar with Theme Toggle

The header is a .navbar used as a toolbar: menu button (hidden from lg), a search input group hidden on phones, and a right-aligned group with the theme button and a user dropdown.

html
<header class="navbar bg-body border-bottom px-3 sticky-top"> <button class="btn btn-outline-secondary d-lg-none" data-bs-toggle="offcanvas" data-bs-target="#sidebar" aria-controls="sidebar" aria-label="Open menu"> <i class="bi bi-list" aria-hidden="true"></i> </button> <form class="d-none d-md-flex ms-3" role="search" style="width: 320px"> <div class="input-group"> <span class="input-group-text"><i class="bi bi-search" aria-hidden="true"></i></span> <input class="form-control" type="search" placeholder="Search orders" aria-label="Search orders"> </div> </form> <div class="ms-auto d-flex align-items-center gap-2"> <button class="btn btn-outline-secondary" id="themeToggle" aria-label="Toggle dark mode"> <i class="bi bi-moon" aria-hidden="true"></i> </button> <div class="dropdown"> <button class="btn btn-light dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Ada</button> <ul class="dropdown-menu dropdown-menu-end"> <li><a class="dropdown-item" href="#">Profile</a></li> <li><hr class="dropdown-divider"></li> <li><a class="dropdown-item" href="#">Sign out</a></li> </ul> </div> </div> </header>

The toggle flips data-bs-theme and remembers the choice:

javascript
const root = document.documentElement; root.setAttribute("data-bs-theme", localStorage.getItem("theme") || "light"); document.getElementById("themeToggle").addEventListener("click", () => { const next = root.getAttribute("data-bs-theme") === "dark" ? "light" : "dark"; root.setAttribute("data-bs-theme", next); localStorage.setItem("theme", next); });

Step 3: Stat Cards

Four equal cards that reflow from one column on phones to four on desktop. h-100 equalizes heights; *-subtle and *-emphasis colors keep the icon tile readable in dark mode.

html
<div class="row row-cols-1 row-cols-sm-2 row-cols-xl-4 g-3 mb-4"> <div class="col"> <div class="card h-100"> <div class="card-body d-flex align-items-center gap-3"> <span class="rounded-3 bg-primary-subtle text-primary-emphasis p-3"> <i class="bi bi-cart3 fs-4" aria-hidden="true"></i> </span> <div> <div class="text-body-secondary small">Orders today</div> <div class="fs-3 fw-semibold">128</div> <span class="badge text-bg-success">+12%</span> </div> </div> </div> </div> <!-- repeat for Revenue, New customers, Refunds --> </div>

Step 4: Orders Table with Actions

The table lives inside a card with a header action, a .table-responsive wrapper and a footer holding .pagination.pagination-sm.justify-content-end:

html
<div class="card"> <div class="card-header d-flex align-items-center"> <h2 class="h5 mb-0">Recent orders</h2> <button class="btn btn-primary btn-sm ms-auto" data-bs-toggle="modal" data-bs-target="#orderModal"> <i class="bi bi-plus-lg me-1" aria-hidden="true"></i>New order </button> </div> <div class="table-responsive"> <table class="table table-hover align-middle mb-0"> <thead class="table-light"> <tr><th>Order</th><th>Customer</th><th>Status</th><th class="text-end">Total</th><th></th></tr> </thead> <tbody> <tr> <td>#4821</td> <td>Grace Hopper</td> <td><span class="badge text-bg-warning">Pending</span></td> <td class="text-end">$240.00</td> <td class="text-end"> <div class="dropdown"> <button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" data-bs-boundary="viewport" aria-label="Actions for order 4821"> <i class="bi bi-three-dots" aria-hidden="true"></i> </button> <ul class="dropdown-menu dropdown-menu-end"> <li><button class="dropdown-item">View</button></li> <li><button class="dropdown-item text-danger">Cancel</button></li> </ul> </div> </td> </tr> </tbody> </table> </div> </div>

data-bs-boundary="viewport" stops the action menu from being clipped by the .table-responsive overflow container.

Step 5: New Order Modal with Validation

html
<div class="modal fade" id="orderModal" tabindex="-1" aria-labelledby="orderModalLabel" aria-hidden="true"> <div class="modal-dialog"> <form class="modal-content needs-validation" novalidate id="orderForm"> <div class="modal-header"> <h2 class="modal-title h5" id="orderModalLabel">New order</h2> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body vstack gap-3"> <div class="form-floating"> <input class="form-control" id="customer" placeholder="Customer" required> <label for="customer">Customer</label> <div class="invalid-feedback">Customer is required.</div> </div> <div class="form-floating"> <input type="number" class="form-control" id="total" placeholder="Total" min="1" required> <label for="total">Total (USD)</label> <div class="invalid-feedback">Enter an amount of at least 1.</div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <button type="submit" class="btn btn-primary">Create</button> </div> </form> </div> </div>

Step 6: Wiring Feedback

Place a .toast-container.position-fixed.bottom-0.end-0.p-3 with a #savedToast at the end of <body> (as built in the toasts lesson), then connect the form:

javascript
const form = document.getElementById("orderForm"); const modal = bootstrap.Modal.getOrCreateInstance("#orderModal"); const toast = bootstrap.Toast.getOrCreateInstance("#savedToast"); form.addEventListener("submit", (event) => { event.preventDefault(); if (!form.checkValidity()) { form.classList.add("was-validated"); return; } modal.hide(); form.reset(); form.classList.remove("was-validated"); toast.show(); });

Finishing Checks and Extensions

  • Resize to phone width: the sidebar hides behind the menu button, stat cards stack, the table scrolls horizontally.
  • Toggle dark mode: every surface switches; anything that stays white is using bg-white and needs bg-body.
  • Tab through the page: menu button, search, toggle, table actions and modal fields are all reachable, and closing the modal returns focus to the "New order" button.

To extend the project: render table rows from fetch() with a placeholder skeleton while loading, add a settings page built from vertical pills and a list group, drop a chart library into a card, and move from the CDN to an npm build with $primary set to your brand color.

Quick Quiz
Question 1 of 3

What does the `.offcanvas-lg` class do?

Key Takeaways

  • A dashboard shell is a full-height flex row: an .offcanvas-lg sidebar and a flex-grow-1 column with min-width: 0.
  • The top bar combines a navbar, an input group search, a dropdown and a data-bs-theme toggle persisted in localStorage.
  • row-cols-* with h-100 cards builds stat grids; *-subtle/*-emphasis colors keep them dark-mode safe.
  • Tables live in .card plus .table-responsive, with badges and dropdown actions using data-bs-boundary="viewport".
  • Modals with floating labels and validation, followed by a toast, complete the create-and-confirm flow.

Next lesson: Bootstrap Best Practices and Interview Questions - the conventions professionals follow and the questions you should be able to answer.

Capstone: Building an Admin Dashboard - Bootstrap | CodeYourCraft | CodeYourCraft