Navigation and disclosure patterns
Tabs, breadcrumbs, dropdowns, scrollspy and offcanvas — and how to choose between them instead of stacking all five.
Tabs, pills and when a link is better
<ul class="nav nav-tabs" id="accountTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="profile-tab" data-bs-toggle="tab"
data-bs-target="#profile" type="button" role="tab"
aria-controls="profile" aria-selected="true">Profile</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="billing-tab" data-bs-toggle="tab"
data-bs-target="#billing" type="button" role="tab"
aria-controls="billing" aria-selected="false">Billing</button>
</li>
</ul>
<div class="tab-content pt-3">
<div class="tab-pane fade show active" id="profile" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
Profile settings.
</div>
<div class="tab-pane fade" id="billing" role="tabpanel" aria-labelledby="billing-tab" tabindex="0">
Billing settings.
</div>
</div>| Pattern | Use when | Avoid when |
|---|---|---|
nav-tabs | Peer views of the same object | The content deserves its own URL and back button |
nav-pills | Compact filter or step switch | The labels are long |
| Plain links | Each view is a real page | You only want to swap one panel |
accordion | Long optional detail, mobile-heavy | Several panes must be comparable side by side |
| Offcanvas | Navigation on small screens | The content is needed at the same time as the page |
The trigger is a <button> because activating a tab changes state rather than navigating. If a designer wants tabs but users would bookmark or share the views, use links and a router instead — the tab pattern will fight the browser's history.
Breadcrumbs, pagination and scrollspy
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/reports/">Reports</a></li>
<li class="breadcrumb-item active" aria-current="page">September</li>
</ol>
</nav>
<nav aria-label="Report pages">
<ul class="pagination justify-content-center">
<li class="page-item"><a class="page-link" href="?p=1" aria-label="Previous"><span aria-hidden="true">«</span></a></li>
<li class="page-item"><a class="page-link" href="?p=1">1</a></li>
<li class="page-item active" aria-current="page"><a class="page-link" href="?p=2">2</a></li>
<li class="page-item"><a class="page-link" href="?p=3">3</a></li>
<li class="page-item"><a class="page-link" href="?p=3" aria-label="Next"><span aria-hidden="true">»</span></a></li>
</ul>
</nav>
<!-- scrollspy: one of the few Bootstrap features that wants a body-level target -->
<body data-bs-spy="scroll" data-bs-target="#toc" data-bs-root-margin="0px 0px -40%" data-bs-smooth-scroll="true" tabindex="0">
<nav id="toc" class="nav flex-column position-sticky top-0">
<a class="nav-link" href="#install">Install</a>
<a class="nav-link" href="#configure">Configure</a>
</nav>
<section id="install">...</section>
<section id="configure">...</section>
</body>- Breadcrumbs describe hierarchy, not history. If the path changes as you browse, that is a toolbar, not a breadcrumb.
- Mark the current page with
aria-current="page"— visually hidden or not, it is what assistive tech announces. data-bs-root-marginwith a negative bottom value is the standard trick for scrollspy: it stops the last section from activating too early.- Scrollspy observes elements by
id, so sections need stable ids that also work as link fragments.
Dropdowns and offcanvas navigation
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-bs-toggle="dropdown" aria-expanded="false">Actions</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="#">Duplicate</a></li>
<li><a class="dropdown-item" href="#">Move…</a></li>
<li><hr class="dropdown-divider"></li>
<li><button class="dropdown-item text-danger" type="button">Delete</button></li>
</ul>
</div>
<button class="btn btn-primary d-lg-none" type="button"
data-bs-toggle="offcanvas" data-bs-target="#nav" aria-controls="nav">Menu</button>
<div class="offcanvas offcanvas-start" tabindex="-1" id="nav" aria-labelledby="navLabel">
<div class="offcanvas-header">
<h2 class="offcanvas-title h5" id="navLabel">Navigation</h2>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="nav flex-column">
<a class="nav-link" href="/dashboard/">Dashboard</a>
<a class="nav-link" href="/settings/">Settings</a>
</nav>
</div>
</div>💡
Dropdowns and offcanvas close on
Escape and restore focus to the trigger, but a dropdown menu inside an offcanvas will be clipped unless it uses data-bs-display="static". When a menu must escape a scroll container, render it in a portal or accept static positioning.FAQ
Why do my tabs stop working after a partial page update?
The plugin attaches on load. After replacing markup, either call
bootstrap.Tab.getOrCreateInstance(el).show() to rebind, or use event delegation on a stable ancestor — Bootstrap 5 dropped the delegated data-toggle handling that Bootstrap 3 had.How do I open the offcanvas from JavaScript instead of a data attribute?
const oc = bootstrap.Offcanvas.getOrCreateInstance('#nav'); oc.show();. getOrCreateInstance is safe to call on an element that already has an instance, which avoids the double-initialisation problems that new Offcanvas() twice causes.Related
The JavaScript API, plugins and events Overlays: tooltips, popovers and toasts
Last refreshed 2026-09-18.