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>
PatternUse whenAvoid when
nav-tabsPeer views of the same objectThe content deserves its own URL and back button
nav-pillsCompact filter or step switchThe labels are long
Plain linksEach view is a real pageYou only want to swap one panel
accordionLong optional detail, mobile-heavySeveral panes must be comparable side by side
OffcanvasNavigation on small screensThe 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">&laquo;</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">&raquo;</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-margin with 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.

The JavaScript API, plugins and events Overlays: tooltips, popovers and toasts

Last refreshed 2026-09-18.