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.
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):
<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>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.
<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:
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);
});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.
<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>The table lives inside a card with a header action, a .table-responsive wrapper and a footer holding .pagination.pagination-sm.justify-content-end:
<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.
<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>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:
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();
});bg-white and needs bg-body.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.
What does the `.offcanvas-lg` class do?
.offcanvas-lg sidebar and a flex-grow-1 column with min-width: 0.data-bs-theme toggle persisted in localStorage.row-cols-* with h-100 cards builds stat grids; *-subtle/*-emphasis colors keep them dark-mode safe..card plus .table-responsive, with badges and dropdown actions using data-bs-boundary="viewport".Next lesson: Bootstrap Best Practices and Interview Questions - the conventions professionals follow and the questions you should be able to answer.