Navs, Tabs and Pills

Intermediate
12 min

Navs, Tabs and Pills

The .nav component is the base for every horizontal or vertical set of links in Bootstrap, including the links inside a navbar. Adding .nav-tabs, .nav-pills or .nav-underline changes the look, and the tab JavaScript plugin turns any nav into a set of switchable panels. After this lesson you will be able to build styled navigation, accessible tabbed interfaces, and control them from JavaScript.

The Base Nav

html
<ul class="nav"> <li class="nav-item"><a class="nav-link active" aria-current="page" href="#">Active</a></li> <li class="nav-item"><a class="nav-link" href="#">Link</a></li> <li class="nav-item"><a class="nav-link disabled" aria-disabled="true">Disabled</a></li> </ul>

.nav is a flex container, so all flex utilities apply:

| Goal | Classes | |---|---| | Center the links | nav justify-content-center | | Right-align | nav justify-content-end | | Vertical list | nav flex-column | | Equal-width links | nav nav-fill or nav nav-justified | | Vertical on phones, horizontal from md | nav flex-column flex-md-row |

.nav-fill gives each item proportional width based on content; .nav-justified gives every item exactly the same width.

Visual Styles

  • .nav-tabs: classic tabbed look with a bottom border; the active tab appears attached to the content.
  • .nav-pills: rounded, filled background on the active item.
  • .nav-underline: a slim underline on the active item, added in Bootstrap 5.3.
html
<ul class="nav nav-pills"> <li class="nav-item"><a class="nav-link active" href="#">Day</a></li> <li class="nav-item"><a class="nav-link" href="#">Week</a></li> <li class="nav-item"><a class="nav-link" href="#">Month</a></li> </ul>

A nav item may also contain a dropdown: give the .nav-link data-bs-toggle="dropdown" and add a .dropdown-menu inside the .nav-item.dropdown.

Tabbable Panels with the Tab Plugin

The tab plugin needs three things: triggers with data-bs-toggle="tab" (or "pill") whose data-bs-target or href points at a pane, a .tab-content wrapper, and .tab-pane panels. Exactly one pane starts with active:

html
<ul class="nav nav-pills mb-3" role="tablist"> <li class="nav-item" role="presentation"> <button class="nav-link active" data-bs-toggle="pill" data-bs-target="#overview" type="button" role="tab">Overview</button> </li> <li class="nav-item" role="presentation"> <button class="nav-link" data-bs-toggle="pill" data-bs-target="#specs" type="button" role="tab">Specs</button> </li> </ul> <div class="tab-content"> <div class="tab-pane fade show active" id="overview" role="tabpanel" tabindex="0">Overview text</div> <div class="tab-pane fade" id="specs" role="tabpanel" tabindex="0">Specifications</div> </div>

Notes on the markup:

  • Use <button> triggers when tabs switch in-page content; use <a href="#pane"> only if you also want the URL fragment.
  • .fade animates the switch; the active pane also needs .show.
  • The plugin manages aria-selected and keyboard navigation (arrow keys move between tabs) as long as role="tablist", role="tab" and role="tabpanel" are present.

Vertical tabs are the same markup with .nav.flex-column in one grid column and .tab-content in another; add aria-orientation="vertical" to the tab list.

Controlling Tabs with JavaScript

javascript
import { Tab } from "bootstrap"; // Activate a tab programmatically const specsTab = new Tab(document.querySelector('[data-bs-target="#specs"]')); specsTab.show(); // Or get an existing instance without creating a second one Tab.getOrCreateInstance(document.querySelector("#general-tab")).show(); // Open the tab named in the URL hash on load const hash = window.location.hash; if (hash) { const trigger = document.querySelector(`[data-bs-target="${hash}"]`); if (trigger) Tab.getOrCreateInstance(trigger).show(); }

Events fire on the trigger element in this order: hide.bs.tab (current tab), show.bs.tab (new tab), hidden.bs.tab, shown.bs.tab. Each event exposes event.target (the new tab) and event.relatedTarget (the previous one):

javascript
document.querySelectorAll('[data-bs-toggle="tab"]').forEach((el) => { el.addEventListener("shown.bs.tab", (event) => { history.replaceState(null, "", event.target.dataset.bsTarget); }); });

Common Mistakes

  • Two panes with .active. Both are visible until the first click; only the initial pane should have active and show.
  • fade without show on the active pane. The pane exists but has opacity: 0, so it looks empty.
  • Loading the CSS but not the JavaScript. Tabs render but clicking does nothing; import bootstrap.bundle.min.js or the Tab module.
  • Duplicate IDs. Each pane needs a unique id matched by exactly one trigger.
Quick Quiz
Question 1 of 3

Which attribute connects a tab trigger to its pane?

Key Takeaways

  • .nav with .nav-item and .nav-link is the base; .nav-tabs, .nav-pills and .nav-underline change the style.
  • Flex utilities plus .nav-fill / .nav-justified control alignment and width; flex-column makes vertical navs.
  • The tab plugin uses data-bs-toggle="tab|pill" triggers, .tab-content and .tab-pane panels, with one pane marked active show.
  • Include ARIA roles so the plugin provides keyboard support and aria-selected updates.
  • Tab.getOrCreateInstance(el).show() and the show/shown/hide/hidden.bs.tab events control tabs from JavaScript.

Next lesson: Dropdowns - toggleable menus with headers, dividers, forms, dark variants and Popper-driven placement.

Navs, Tabs and Pills - Bootstrap | CodeYourCraft | CodeYourCraft