Bootstrap Tabs: 16 Examples of Tabs, Pills and Vertical Navs

The tab plugin in Bootstrap does one job. It connects a trigger to a pane through data-bs-toggle="tab" and data-bs-target, so clicking the trigger shows the pane and nothing reloads.

Below are 16 Bootstrap tabs examples you can copy, followed by a guide to the markup, styles, JavaScript API, accessibility and keeping the active tab after reload.

By Bogdan Sandu ยท Updated October 29, 2026

Every example runs on Bootstrap 5.3.8 from the jsDelivr CDN. Add these tags to your page, then paste an example's HTML, CSS and JS.

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.2/font/bootstrap-icons.min.css" rel="stylesheet" integrity="sha384-nWN3KqaaVcbEsCjLSV+2jMjh62Iny/+mN3srM8TZeM8N6aj7eDl5wwYggjAJ4/sJ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

Everyday tabs and pills

Product pages, settings cards, pricing switches and notification feeds: the four tab patterns most sites need, built with nav-tabs, nav-pills, nav-underline and the Tab plugin.

Product detail tabs with specs and reviews

  • nav-tabs
  • tab-pane fade
  • table-sm
  • aria-label on tablist

Click a tab, or focus the active one and use the arrow keys.

The standard Bootstrap tab markup inside a horizontal card: buttons with data-bs-toggle="tab", aria-controls and aria-selected, fading panes with tabindex="0" and an aria-label on the tablist. The specs pane is a plain table-sm, and the only custom CSS sets the image crop and tighter link padding through --bs-nav-link-padding-x.

View code
<div class="card shadow-sm border-0 overflow-hidden btt-product">
  <div class="row g-0">
    <div class="col-sm-4">
      <img src="mountain-hiker.webp" class="btt-product-img" width="800" height="600" alt="Hiker with a large trekking backpack standing on a rocky ridge above a misty valley">
    </div>
    <div class="col-sm-8">
      <div class="card-body p-3 p-sm-4">
        <h3 class="h5 mb-1">Ridgeline 38L Trekking Pack</h3>
        <p class="mb-3"><span class="fw-semibold">$189</span> <span class="text-body-secondary small">Free returns for 60 days</span></p>
        <ul class="nav nav-tabs btt-product-tabs" id="btt-prod-tabs" role="tablist" aria-label="Product information">
          <li class="nav-item" role="presentation">
            <button class="nav-link active" id="btt-prod-desc-tab" data-bs-toggle="tab" data-bs-target="#btt-prod-desc" type="button" role="tab" aria-controls="btt-prod-desc" aria-selected="true">Details</button>
          </li>
          <li class="nav-item" role="presentation">
            <button class="nav-link" id="btt-prod-specs-tab" data-bs-toggle="tab" data-bs-target="#btt-prod-specs" type="button" role="tab" aria-controls="btt-prod-specs" aria-selected="false">Specs</button>
          </li>
          <li class="nav-item" role="presentation">
            <button class="nav-link" id="btt-prod-rev-tab" data-bs-toggle="tab" data-bs-target="#btt-prod-rev" type="button" role="tab" aria-controls="btt-prod-rev" aria-selected="false">Reviews <span class="badge rounded-pill text-bg-light border">128</span></button>
          </li>
        </ul>
        <div class="tab-content pt-3">
          <div class="tab-pane fade show active" id="btt-prod-desc" role="tabpanel" aria-labelledby="btt-prod-desc-tab" tabindex="0">
            <p class="mb-2">A light, top-loading pack for two to four day hikes. The ventilated back panel keeps air moving on long climbs, and the hip belt carries most of the load.</p>
            <p class="mb-0 small text-body-secondary">Recycled ripstop shell, rain cover in the lid pocket.</p>
          </div>
          <div class="tab-pane fade" id="btt-prod-specs" role="tabpanel" aria-labelledby="btt-prod-specs-tab" tabindex="0">
            <table class="table table-sm mb-0 small">
              <tbody>
                <tr><th scope="row" class="fw-medium">Volume</th><td>38 liters</td></tr>
                <tr><th scope="row" class="fw-medium">Weight</th><td>1.2 kg</td></tr>
                <tr><th scope="row" class="fw-medium">Torso length</th><td>41 to 51 cm, adjustable</td></tr>
                <tr><th scope="row" class="fw-medium">Material</th><td>210D recycled nylon</td></tr>
              </tbody>
            </table>
          </div>
          <div class="tab-pane fade" id="btt-prod-rev" role="tabpanel" aria-labelledby="btt-prod-rev-tab" tabindex="0">
            <p class="mb-2"><span class="fw-semibold">4.7 out of 5</span> <span class="text-body-secondary small">from 128 reviews</span></p>
            <blockquote class="mb-2 small">"Carried it across the Dolomites for a week. No sore shoulders." <span class="text-body-secondary">Marta K.</span></blockquote>
            <blockquote class="mb-0 small">"The lid pocket fits a map, snacks and a headlamp." <span class="text-body-secondary">Dev P.</span></blockquote>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Tabs inside a card header

  • card-header-tabs
  • list-group-flush
  • badge
  • tab-content on card-body

Click a tab, or focus the active one and use the arrow keys.

The tab list sits in .card-header with .card-header-tabs, which pulls the tabs down so the active tab merges with the card body. The card body doubles as .tab-content, and the panes reuse list groups, badges and a definition list instead of custom markup.

View code
<div class="card btt-deploy">
  <div class="card-header bg-body-tertiary">
    <ul class="nav nav-tabs card-header-tabs" id="btt-deploy-tabs" role="tablist" aria-label="Project settings">
      <li class="nav-item" role="presentation">
        <button class="nav-link active" id="btt-dep-general-tab" data-bs-toggle="tab" data-bs-target="#btt-dep-general" type="button" role="tab" aria-controls="btt-dep-general" aria-selected="true">General</button>
      </li>
      <li class="nav-item" role="presentation">
        <button class="nav-link" id="btt-dep-domains-tab" data-bs-toggle="tab" data-bs-target="#btt-dep-domains" type="button" role="tab" aria-controls="btt-dep-domains" aria-selected="false">Domains</button>
      </li>
      <li class="nav-item" role="presentation">
        <button class="nav-link" id="btt-dep-env-tab" data-bs-toggle="tab" data-bs-target="#btt-dep-env" type="button" role="tab" aria-controls="btt-dep-env" aria-selected="false">Variables</button>
      </li>
    </ul>
  </div>
  <div class="card-body tab-content">
    <div class="tab-pane fade show active" id="btt-dep-general" role="tabpanel" aria-labelledby="btt-dep-general-tab" tabindex="0">
      <dl class="row small mb-0">
        <dt class="col-5 fw-medium">Project</dt><dd class="col-7">harbor-web</dd>
        <dt class="col-5 fw-medium">Framework</dt><dd class="col-7">Static site</dd>
        <dt class="col-5 fw-medium">Branch</dt><dd class="col-7">main</dd>
        <dt class="col-5 fw-medium">Last deploy</dt><dd class="col-7 mb-0"><span class="badge text-bg-success">Ready</span> 4 min ago</dd>
      </dl>
    </div>
    <div class="tab-pane fade" id="btt-dep-domains" role="tabpanel" aria-labelledby="btt-dep-domains-tab" tabindex="0">
      <ul class="list-group list-group-flush small">
        <li class="list-group-item d-flex justify-content-between align-items-center gap-2 px-0"><span class="text-break">harbor.example.com</span><span class="badge text-bg-success">Valid</span></li>
        <li class="list-group-item d-flex justify-content-between align-items-center gap-2 px-0"><span class="text-break">www.harbor.example.com</span><span class="badge text-bg-success">Valid</span></li>
        <li class="list-group-item d-flex justify-content-between align-items-center gap-2 px-0"><span class="text-break">shop.harbor.example.com</span><span class="badge text-bg-warning">Pending DNS</span></li>
      </ul>
    </div>
    <div class="tab-pane fade" id="btt-dep-env" role="tabpanel" aria-labelledby="btt-dep-env-tab" tabindex="0">
      <ul class="list-group list-group-flush small font-monospace">
        <li class="list-group-item d-flex justify-content-between gap-2 px-0"><span>SITE_URL</span><span class="text-body-secondary">Production</span></li>
        <li class="list-group-item d-flex justify-content-between gap-2 px-0"><span>ANALYTICS_ID</span><span class="text-body-secondary">All</span></li>
        <li class="list-group-item d-flex justify-content-between gap-2 px-0"><span>MAP_TOKEN</span><span class="text-body-secondary">Hidden</span></li>
      </ul>
    </div>
  </div>
</div>

Pills as a monthly and yearly pricing switch

  • nav-pills
  • data-bs-toggle="pill"
  • --bs-nav-pills-*
  • prefers-reduced-motion

Click a tab, or focus the active one and use the arrow keys.

Two pills with data-bs-toggle="pill" swap a monthly and a yearly price grid, so the switch is a real tablist with keyboard support. The segmented look comes from --bs-nav-pills-link-active-bg, --bs-nav-pills-border-radius and a tinted track, and the plan cards rise in only when the reader allows motion.

View code
<div class="btt-pricing text-center">
  <h3 class="h4 mb-1">Plans for small studios</h3>
  <p class="text-body-secondary mb-3">Pay yearly and get two months free.</p>
  <ul class="nav nav-pills btt-billing mb-4" id="btt-bill-tabs" role="tablist" aria-label="Billing period">
    <li class="nav-item" role="presentation">
      <button class="nav-link active" id="btt-bill-m-tab" data-bs-toggle="pill" data-bs-target="#btt-bill-m" type="button" role="tab" aria-controls="btt-bill-m" aria-selected="true">Monthly</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="btt-bill-y-tab" data-bs-toggle="pill" data-bs-target="#btt-bill-y" type="button" role="tab" aria-controls="btt-bill-y" aria-selected="false">Yearly <span class="badge rounded-pill btt-save">2 months free</span></button>
    </li>
  </ul>
  <div class="tab-content text-start">
    <div class="tab-pane fade show active" id="btt-bill-m" role="tabpanel" aria-labelledby="btt-bill-m-tab">
      <div class="row g-3">
        <div class="col-sm-6"><div class="btt-plan h-100 p-4 bg-body rounded-4 border">
          <h4 class="h6 mb-2">Starter</h4>
          <p class="mb-1"><span class="fs-2 fw-bold">$12</span> <span class="text-body-secondary">per month</span></p>
          <p class="small text-body-secondary mb-3">3 projects, 1 editor</p>
          <a href="#" class="btn btn-outline-dark w-100">Choose Starter</a>
        </div></div>
        <div class="col-sm-6"><div class="btt-plan h-100 p-4 rounded-4 btt-plan-pro">
          <h4 class="h6 mb-2">Studio</h4>
          <p class="mb-1"><span class="fs-2 fw-bold">$29</span> <span class="btt-plan-muted">per month</span></p>
          <p class="small btt-plan-muted mb-3">Unlimited projects, 5 editors</p>
          <a href="#" class="btn btn-light w-100">Choose Studio</a>
        </div></div>
      </div>
    </div>
    <div class="tab-pane fade" id="btt-bill-y" role="tabpanel" aria-labelledby="btt-bill-y-tab">
      <div class="row g-3">
        <div class="col-sm-6"><div class="btt-plan h-100 p-4 bg-body rounded-4 border">
          <h4 class="h6 mb-2">Starter</h4>
          <p class="mb-1"><span class="fs-2 fw-bold">$120</span> <span class="text-body-secondary">per year</span></p>
          <p class="small text-body-secondary mb-3">Works out to $10 a month</p>
          <a href="#" class="btn btn-outline-dark w-100">Choose Starter</a>
        </div></div>
        <div class="col-sm-6"><div class="btt-plan h-100 p-4 rounded-4 btt-plan-pro">
          <h4 class="h6 mb-2">Studio</h4>
          <p class="mb-1"><span class="fs-2 fw-bold">$290</span> <span class="btt-plan-muted">per year</span></p>
          <p class="small btt-plan-muted mb-3">Works out to $24.17 a month</p>
          <a href="#" class="btn btn-light w-100">Choose Studio</a>
        </div></div>
      </div>
    </div>
  </div>
</div>

Underline tabs for a notification feed

  • nav-underline
  • --bs-nav-underline-*
  • badge
  • visually-hidden

Click a tab, or focus the active one and use the arrow keys.

The .nav-underline variant, added in Bootstrap 5.3.0, marks the active tab with a bottom border instead of a box. Its gap and active color come from --bs-nav-underline-gap and --bs-nav-underline-link-active-color, and the unread counts are badges with visually hidden text.

View code
<div class="card border-0 shadow-sm btt-feed">
  <div class="card-body p-3 p-sm-4">
    <h3 class="h5 mb-3">Notifications</h3>
    <ul class="nav nav-underline btt-feed-tabs border-bottom mb-3" id="btt-feed-tabs" role="tablist" aria-label="Notification filters">
      <li class="nav-item" role="presentation">
        <button class="nav-link active" id="btt-feed-all-tab" data-bs-toggle="tab" data-bs-target="#btt-feed-all" type="button" role="tab" aria-controls="btt-feed-all" aria-selected="true">All</button>
      </li>
      <li class="nav-item" role="presentation">
        <button class="nav-link" id="btt-feed-men-tab" data-bs-toggle="tab" data-bs-target="#btt-feed-men" type="button" role="tab" aria-controls="btt-feed-men" aria-selected="false">Mentions <span class="badge rounded-pill btt-feed-count">2<span class="visually-hidden"> unread</span></span></button>
      </li>
      <li class="nav-item" role="presentation">
        <button class="nav-link" id="btt-feed-fol-tab" data-bs-toggle="tab" data-bs-target="#btt-feed-fol" type="button" role="tab" aria-controls="btt-feed-fol" aria-selected="false">Following</button>
      </li>
    </ul>
    <div class="tab-content">
      <div class="tab-pane fade show active" id="btt-feed-all" role="tabpanel" aria-labelledby="btt-feed-all-tab" tabindex="0">
        <div class="d-flex gap-3 mb-3"><span class="btt-ava" style="--btt-ava: #7c3aed" aria-hidden="true">LN</span><p class="small mb-0"><strong>Lena Novak</strong> approved the homepage redesign.<br><span class="text-body-secondary">12 min ago</span></p></div>
        <div class="d-flex gap-3"><span class="btt-ava" style="--btt-ava: #0f766e" aria-hidden="true">RB</span><p class="small mb-0"><strong>Rafael Brito</strong> uploaded 6 files to Brand assets.<br><span class="text-body-secondary">1 hour ago</span></p></div>
      </div>
      <div class="tab-pane fade" id="btt-feed-men" role="tabpanel" aria-labelledby="btt-feed-men-tab" tabindex="0">
        <div class="d-flex gap-3 mb-3"><span class="btt-ava" style="--btt-ava: #b45309" aria-hidden="true">AO</span><p class="small mb-0"><strong>Amara Obi</strong> mentioned you in Checkout copy: "Can you check the button labels?"<br><span class="text-body-secondary">5 min ago</span></p></div>
        <div class="d-flex gap-3"><span class="btt-ava" style="--btt-ava: #7c3aed" aria-hidden="true">LN</span><p class="small mb-0"><strong>Lena Novak</strong> mentioned you in Sprint 14 notes.<br><span class="text-body-secondary">Yesterday</span></p></div>
      </div>
      <div class="tab-pane fade" id="btt-feed-fol" role="tabpanel" aria-labelledby="btt-feed-fol-tab" tabindex="0">
        <div class="d-flex gap-3"><span class="btt-ava" style="--btt-ava: #be123c" aria-hidden="true">JS</span><p class="small mb-0"><strong>Jonas Siebert</strong> published Pattern library v3.<br><span class="text-body-secondary">3 hours ago</span></p></div>
      </div>
    </div>
  </div>
</div>

Layouts: vertical, justified, scrollable and with icons

The same plugin with different nav layouts. Flex utilities turn a vertical pill list into a row on phones, nav-justified stretches tabs across a card and flex-nowrap makes a long tab row scroll sideways.

Vertical pills settings panel that stacks on mobile

  • flex-md-column
  • aria-orientation
  • form-switch
  • matchMedia

Click a tab. On a phone, swipe the tab row sideways.

The docs layout for vertical pills, made responsive: from md up the pill list is a flex-md-column sidebar, below md it becomes a single scrollable row. A short script keeps aria-orientation in sync with the layout, and the panes use regular Bootstrap form controls and switches.

View code
<div class="card border-0 shadow-sm btt-settings">
  <div class="card-body p-3 p-md-4 d-flex flex-column flex-md-row gap-3 gap-md-4">
    <div class="nav nav-pills flex-nowrap flex-md-column btt-settings-nav" id="btt-set-tabs" role="tablist" aria-label="Account settings" aria-orientation="vertical">
      <button class="nav-link active" id="btt-set-profile-tab" data-bs-toggle="pill" data-bs-target="#btt-set-profile" type="button" role="tab" aria-controls="btt-set-profile" aria-selected="true"><i class="bi bi-person" aria-hidden="true"></i> Profile</button>
      <button class="nav-link" id="btt-set-notif-tab" data-bs-toggle="pill" data-bs-target="#btt-set-notif" type="button" role="tab" aria-controls="btt-set-notif" aria-selected="false"><i class="bi bi-bell" aria-hidden="true"></i> Notifications</button>
      <button class="nav-link" id="btt-set-sec-tab" data-bs-toggle="pill" data-bs-target="#btt-set-sec" type="button" role="tab" aria-controls="btt-set-sec" aria-selected="false"><i class="bi bi-shield-lock" aria-hidden="true"></i> Security</button>
      <button class="nav-link" id="btt-set-bill-tab" data-bs-toggle="pill" data-bs-target="#btt-set-bill" type="button" role="tab" aria-controls="btt-set-bill" aria-selected="false"><i class="bi bi-credit-card" aria-hidden="true"></i> Billing</button>
    </div>
    <div class="tab-content flex-grow-1 btt-settings-panes">
      <div class="tab-pane fade show active" id="btt-set-profile" role="tabpanel" aria-labelledby="btt-set-profile-tab">
        <h3 class="h6 mb-3">Public profile</h3>
        <div class="mb-3">
          <label for="btt-set-name" class="form-label small fw-medium">Display name</label>
          <input type="text" class="form-control" id="btt-set-name" value="Ines Moreau">
        </div>
        <div>
          <label for="btt-set-bio" class="form-label small fw-medium">Short bio</label>
          <textarea class="form-control" id="btt-set-bio" rows="2">Product designer in Lyon.</textarea>
        </div>
      </div>
      <div class="tab-pane fade" id="btt-set-notif" role="tabpanel" aria-labelledby="btt-set-notif-tab">
        <h3 class="h6 mb-3">Email me about</h3>
        <div class="form-check form-switch mb-2">
          <input class="form-check-input" type="checkbox" role="switch" id="btt-set-n1" checked>
          <label class="form-check-label" for="btt-set-n1">Comments on my files</label>
        </div>
        <div class="form-check form-switch mb-2">
          <input class="form-check-input" type="checkbox" role="switch" id="btt-set-n2" checked>
          <label class="form-check-label" for="btt-set-n2">Weekly summary</label>
        </div>
        <div class="form-check form-switch">
          <input class="form-check-input" type="checkbox" role="switch" id="btt-set-n3">
          <label class="form-check-label" for="btt-set-n3">Product news</label>
        </div>
      </div>
      <div class="tab-pane fade" id="btt-set-sec" role="tabpanel" aria-labelledby="btt-set-sec-tab">
        <h3 class="h6 mb-2">Two-step verification</h3>
        <p class="small text-body-secondary mb-3">Ask for a code from an authenticator app when you sign in on a new device.</p>
        <p class="mb-3"><span class="badge text-bg-success">On</span> <span class="small">since March 2026</span></p>
        <button type="button" class="btn btn-outline-secondary btn-sm">Show recovery codes</button>
      </div>
      <div class="tab-pane fade" id="btt-set-bill" role="tabpanel" aria-labelledby="btt-set-bill-tab">
        <h3 class="h6 mb-2">Team plan</h3>
        <p class="small text-body-secondary mb-3">Renews on November 14 for $48. Card ending in 4242.</p>
        <button type="button" class="btn btn-primary btn-sm">Change plan</button>
      </div>
    </div>
  </div>
</div>

Justified tabs in a travel search card

  • nav-justified
  • Bootstrap Icons
  • form-floating
  • row g-2

Click a tab. On a phone, swipe the tab row sideways.

nav-justified gives the three tabs equal widths across the card, and each pane holds a short search form built from form-floating fields in a grid row. Icons are decorative, so they carry aria-hidden="true", and the tab color comes from --bs-nav-tabs-link-active-color.

View code
<div class="card border-0 shadow-sm overflow-hidden btt-trip">
  <img src="canal-houses.webp" class="btt-trip-img" width="800" height="600" alt="Canal lined with old gabled houses and small moored boats on a sunny day">
  <div class="card-body p-3 p-sm-4">
    <ul class="nav nav-tabs nav-justified btt-trip-tabs mb-3" id="btt-trip-tabs" role="tablist" aria-label="Search type">
      <li class="nav-item" role="presentation">
        <button class="nav-link active" id="btt-trip-stay-tab" data-bs-toggle="tab" data-bs-target="#btt-trip-stay" type="button" role="tab" aria-controls="btt-trip-stay" aria-selected="true"><i class="bi bi-house-door" aria-hidden="true"></i> Stays</button>
      </li>
      <li class="nav-item" role="presentation">
        <button class="nav-link" id="btt-trip-fly-tab" data-bs-toggle="tab" data-bs-target="#btt-trip-fly" type="button" role="tab" aria-controls="btt-trip-fly" aria-selected="false"><i class="bi bi-airplane" aria-hidden="true"></i> Flights</button>
      </li>
      <li class="nav-item" role="presentation">
        <button class="nav-link" id="btt-trip-car-tab" data-bs-toggle="tab" data-bs-target="#btt-trip-car" type="button" role="tab" aria-controls="btt-trip-car" aria-selected="false"><i class="bi bi-car-front" aria-hidden="true"></i> Cars</button>
      </li>
    </ul>
    <div class="tab-content">
      <div class="tab-pane fade show active" id="btt-trip-stay" role="tabpanel" aria-labelledby="btt-trip-stay-tab">
        <form class="row g-2">
          <div class="col-12 form-floating"><input type="text" class="form-control" id="btt-trip-city" placeholder="City" value="Bruges"><label for="btt-trip-city">Destination</label></div>
          <div class="col-6 form-floating"><input type="date" class="form-control" id="btt-trip-in" value="2026-12-04"><label for="btt-trip-in">Check in</label></div>
          <div class="col-6 form-floating"><input type="date" class="form-control" id="btt-trip-out" value="2026-12-07"><label for="btt-trip-out">Check out</label></div>
          <div class="col-12"><button type="submit" class="btn btt-trip-go w-100">Search stays</button></div>
        </form>
      </div>
      <div class="tab-pane fade" id="btt-trip-fly" role="tabpanel" aria-labelledby="btt-trip-fly-tab">
        <form class="row g-2">
          <div class="col-6 form-floating"><input type="text" class="form-control" id="btt-trip-from" placeholder="From" value="Lisbon"><label for="btt-trip-from">From</label></div>
          <div class="col-6 form-floating"><input type="text" class="form-control" id="btt-trip-to" placeholder="To" value="Brussels"><label for="btt-trip-to">To</label></div>
          <div class="col-12 form-floating"><input type="date" class="form-control" id="btt-trip-dep" value="2026-12-04"><label for="btt-trip-dep">Departure</label></div>
          <div class="col-12"><button type="submit" class="btn btt-trip-go w-100">Search flights</button></div>
        </form>
      </div>
      <div class="tab-pane fade" id="btt-trip-car" role="tabpanel" aria-labelledby="btt-trip-car-tab">
        <form class="row g-2">
          <div class="col-12 form-floating"><input type="text" class="form-control" id="btt-trip-pick" placeholder="Pick-up" value="Bruges station"><label for="btt-trip-pick">Pick-up location</label></div>
          <div class="col-12 form-floating">
            <select class="form-select" id="btt-trip-size"><option>Compact</option><option selected>Estate</option><option>Van</option></select>
            <label for="btt-trip-size">Car size</label>
          </div>
          <div class="col-12"><button type="submit" class="btn btt-trip-go w-100">Search cars</button></div>
        </form>
      </div>
    </div>
  </div>
</div>

Scrollable tab row for many categories

  • flex-nowrap
  • overflow-x-auto
  • text-nowrap
  • shown.bs.tab

Click a tab. On a phone, swipe the tab row sideways.

Six categories do not fit a phone, so flex-nowrap and overflow-x-auto turn the underline nav into a sideways scroller with scroll snapping. A shown.bs.tab listener scrolls the row so the newly active tab, which the arrow keys can also reach, is never hidden off screen.

View code
<div class="btt-news w-100">
  <ul class="nav nav-underline flex-nowrap overflow-x-auto text-nowrap border-bottom btt-news-tabs" id="btt-news-tabs" role="tablist" aria-label="Story categories">
    <li class="nav-item" role="presentation"><button class="nav-link active" id="btt-news-top-tab" data-bs-toggle="tab" data-bs-target="#btt-news-top" type="button" role="tab" aria-controls="btt-news-top" aria-selected="true">Top stories</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" id="btt-news-design-tab" data-bs-toggle="tab" data-bs-target="#btt-news-design" type="button" role="tab" aria-controls="btt-news-design" aria-selected="false">Design</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" id="btt-news-dev-tab" data-bs-toggle="tab" data-bs-target="#btt-news-dev" type="button" role="tab" aria-controls="btt-news-dev" aria-selected="false">Development</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" id="btt-news-a11y-tab" data-bs-toggle="tab" data-bs-target="#btt-news-a11y" type="button" role="tab" aria-controls="btt-news-a11y" aria-selected="false">Accessibility</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" id="btt-news-perf-tab" data-bs-toggle="tab" data-bs-target="#btt-news-perf" type="button" role="tab" aria-controls="btt-news-perf" aria-selected="false">Performance</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" id="btt-news-jobs-tab" data-bs-toggle="tab" data-bs-target="#btt-news-jobs" type="button" role="tab" aria-controls="btt-news-jobs" aria-selected="false">Careers</button></li>
  </ul>
  <div class="tab-content pt-3">
    <div class="tab-pane fade show active" id="btt-news-top" role="tabpanel" aria-labelledby="btt-news-top-tab" tabindex="0">
      <h3 class="h5 mb-1">Why our team moved every form to native validation</h3>
      <p class="text-body-secondary small mb-0">Field notes, 6 min read</p>
    </div>
    <div class="tab-pane fade" id="btt-news-design" role="tabpanel" aria-labelledby="btt-news-design-tab" tabindex="0">
      <h3 class="h5 mb-1">Spacing scales that survive a redesign</h3>
      <p class="text-body-secondary small mb-0">Design, 4 min read</p>
    </div>
    <div class="tab-pane fade" id="btt-news-dev" role="tabpanel" aria-labelledby="btt-news-dev-tab" tabindex="0">
      <h3 class="h5 mb-1">Container queries in a component library, one year in</h3>
      <p class="text-body-secondary small mb-0">Development, 8 min read</p>
    </div>
    <div class="tab-pane fade" id="btt-news-a11y" role="tabpanel" aria-labelledby="btt-news-a11y-tab" tabindex="0">
      <h3 class="h5 mb-1">Testing tab interfaces with a screen reader in 15 minutes</h3>
      <p class="text-body-secondary small mb-0">Accessibility, 5 min read</p>
    </div>
    <div class="tab-pane fade" id="btt-news-perf" role="tabpanel" aria-labelledby="btt-news-perf-tab" tabindex="0">
      <h3 class="h5 mb-1">Cutting 300 KB of JavaScript from a marketing site</h3>
      <p class="text-body-secondary small mb-0">Performance, 7 min read</p>
    </div>
    <div class="tab-pane fade" id="btt-news-jobs" role="tabpanel" aria-labelledby="btt-news-jobs-tab" tabindex="0">
      <h3 class="h5 mb-1">What hiring managers look for in a front-end portfolio</h3>
      <p class="text-body-secondary small mb-0">Careers, 4 min read</p>
    </div>
  </div>
</div>

Tabs with icons and count badges

  • nav-tabs
  • Bootstrap Icons
  • badge rounded-pill
  • list-group

Click a tab. On a phone, swipe the tab row sideways.

An issue tracker header: each tab pairs a Bootstrap Icon with a label and a rounded-pill count. The badge color follows the active state through a short scoped rule, and screen readers hear the count with visually hidden context.

View code
<div class="btt-issues w-100">
  <ul class="nav nav-tabs btt-issue-tabs" id="btt-issue-tabs" role="tablist" aria-label="Issues by status">
    <li class="nav-item" role="presentation">
      <button class="nav-link active" id="btt-iss-open-tab" data-bs-toggle="tab" data-bs-target="#btt-iss-open" type="button" role="tab" aria-controls="btt-iss-open" aria-selected="true"><i class="bi bi-record-circle d-none d-sm-inline-block" aria-hidden="true"></i> Open <span class="badge rounded-pill">24<span class="visually-hidden"> issues</span></span></button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="btt-iss-rev-tab" data-bs-toggle="tab" data-bs-target="#btt-iss-rev" type="button" role="tab" aria-controls="btt-iss-rev" aria-selected="false"><i class="bi bi-eye d-none d-sm-inline-block" aria-hidden="true"></i> Review <span class="badge rounded-pill">5<span class="visually-hidden"> issues</span></span></button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="btt-iss-done-tab" data-bs-toggle="tab" data-bs-target="#btt-iss-done" type="button" role="tab" aria-controls="btt-iss-done" aria-selected="false"><i class="bi bi-check2-circle d-none d-sm-inline-block" aria-hidden="true"></i> Closed</button>
    </li>
  </ul>
  <div class="tab-content border border-top-0 rounded-bottom bg-body">
    <div class="tab-pane fade show active" id="btt-iss-open" role="tabpanel" aria-labelledby="btt-iss-open-tab">
      <ul class="list-group list-group-flush rounded-bottom">
        <li class="list-group-item"><a href="#" class="fw-medium">Date picker ignores the min attribute</a><div class="small text-body-secondary">#482 opened 2 hours ago <span class="badge text-bg-danger ms-1">bug</span></div></li>
        <li class="list-group-item"><a href="#" class="fw-medium">Add dark mode to the email templates</a><div class="small text-body-secondary">#479 opened yesterday <span class="badge text-bg-info ms-1">feature</span></div></li>
      </ul>
    </div>
    <div class="tab-pane fade" id="btt-iss-rev" role="tabpanel" aria-labelledby="btt-iss-rev-tab">
      <ul class="list-group list-group-flush rounded-bottom">
        <li class="list-group-item"><a href="#" class="fw-medium">Lazy load avatars in the member list</a><div class="small text-body-secondary">#471 waiting on 2 reviewers</div></li>
        <li class="list-group-item"><a href="#" class="fw-medium">Fix focus ring on icon buttons</a><div class="small text-body-secondary">#468 changes requested</div></li>
      </ul>
    </div>
    <div class="tab-pane fade" id="btt-iss-done" role="tabpanel" aria-labelledby="btt-iss-done-tab">
      <ul class="list-group list-group-flush rounded-bottom">
        <li class="list-group-item"><a href="#" class="fw-medium">Footer links overlap on 320px screens</a><div class="small text-body-secondary">#455 closed 3 days ago</div></li>
      </ul>
    </div>
  </div>
</div>

Themed with CSS variables and color modes

Bootstrap 5.3 drives navs with --bs-nav-*, --bs-nav-tabs-*, --bs-nav-pills-* and --bs-nav-underline-* variables, so a few lines of CSS give tabs a completely different look without Sass.

Dark-theme analytics tabs

  • data-bs-theme="dark"
  • nav-pills
  • --bs-nav-pills-*
  • inline SVG

Click a tab to see the themed active state.

The card sets data-bs-theme="dark", so the nav, borders and body colors switch to the dark palette without extra rules. Only the pill accent changes through --bs-nav-pills-link-active-bg and --bs-nav-pills-link-active-color, and each pane shows a small inline SVG bar chart with a text summary.

View code
<div class="card btt-dash" data-bs-theme="dark">
  <div class="card-body p-3 p-sm-4">
    <div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
      <h3 class="h6 mb-0">Store overview</h3>
      <ul class="nav nav-pills btt-dash-tabs" id="btt-dash-tabs" role="tablist" aria-label="Metric">
        <li class="nav-item" role="presentation"><button class="nav-link active" id="btt-dash-rev-tab" data-bs-toggle="pill" data-bs-target="#btt-dash-rev" type="button" role="tab" aria-controls="btt-dash-rev" aria-selected="true">Revenue</button></li>
        <li class="nav-item" role="presentation"><button class="nav-link" id="btt-dash-ord-tab" data-bs-toggle="pill" data-bs-target="#btt-dash-ord" type="button" role="tab" aria-controls="btt-dash-ord" aria-selected="false">Orders</button></li>
        <li class="nav-item" role="presentation"><button class="nav-link" id="btt-dash-vis-tab" data-bs-toggle="pill" data-bs-target="#btt-dash-vis" type="button" role="tab" aria-controls="btt-dash-vis" aria-selected="false">Visitors</button></li>
      </ul>
    </div>
    <div class="tab-content">
      <div class="tab-pane fade show active" id="btt-dash-rev" role="tabpanel" aria-labelledby="btt-dash-rev-tab" tabindex="0">
        <p class="mb-1"><span class="fs-3 fw-bold">$18,420</span> <span class="badge btt-up">+12%</span></p>
        <p class="small text-body-secondary mb-3">Last 7 days, compared with the week before</p>
        <svg class="btt-bars" viewBox="0 0 280 80" role="img" aria-label="Daily revenue bars, highest on Saturday"><rect x="4" y="38" width="28" height="42" rx="4"/><rect x="44" y="30" width="28" height="50" rx="4"/><rect x="84" y="44" width="28" height="36" rx="4"/><rect x="124" y="24" width="28" height="56" rx="4"/><rect x="164" y="18" width="28" height="62" rx="4"/><rect x="204" y="6" width="28" height="74" rx="4"/><rect x="244" y="28" width="28" height="52" rx="4"/></svg>
      </div>
      <div class="tab-pane fade" id="btt-dash-ord" role="tabpanel" aria-labelledby="btt-dash-ord-tab" tabindex="0">
        <p class="mb-1"><span class="fs-3 fw-bold">312</span> <span class="badge btt-up">+4%</span></p>
        <p class="small text-body-secondary mb-3">Average order value $59.04</p>
        <svg class="btt-bars" viewBox="0 0 280 80" role="img" aria-label="Daily order bars, fairly even across the week"><rect x="4" y="34" width="28" height="46" rx="4"/><rect x="44" y="30" width="28" height="50" rx="4"/><rect x="84" y="36" width="28" height="44" rx="4"/><rect x="124" y="28" width="28" height="52" rx="4"/><rect x="164" y="26" width="28" height="54" rx="4"/><rect x="204" y="20" width="28" height="60" rx="4"/><rect x="244" y="32" width="28" height="48" rx="4"/></svg>
      </div>
      <div class="tab-pane fade" id="btt-dash-vis" role="tabpanel" aria-labelledby="btt-dash-vis-tab" tabindex="0">
        <p class="mb-1"><span class="fs-3 fw-bold">9,870</span> <span class="badge btt-down">-3%</span></p>
        <p class="small text-body-secondary mb-3">Conversion rate 3.2%</p>
        <svg class="btt-bars" viewBox="0 0 280 80" role="img" aria-label="Daily visitor bars, dipping midweek"><rect x="4" y="20" width="28" height="60" rx="4"/><rect x="44" y="26" width="28" height="54" rx="4"/><rect x="84" y="40" width="28" height="40" rx="4"/><rect x="124" y="42" width="28" height="38" rx="4"/><rect x="164" y="30" width="28" height="50" rx="4"/><rect x="204" y="14" width="28" height="66" rx="4"/><rect x="244" y="22" width="28" height="58" rx="4"/></svg>
      </div>
    </div>
  </div>
</div>

Folder tabs themed with --bs-nav-tabs-* variables

  • --bs-nav-tabs-*
  • --bs-nav-link-*
  • nav-tabs
  • list-unstyled

Click a tab to see the themed active state.

Every color, border and radius here comes from the nav-tabs variables: --bs-nav-tabs-link-active-bg matches the paper card, --bs-nav-tabs-border-color draws the warm rule and --bs-nav-tabs-border-radius rounds the folder tops. The variables sit on the same element as .nav-tabs, which is where Bootstrap defines them.

View code
<div class="btt-recipe w-100">
  <ul class="nav nav-tabs btt-folder" id="btt-recipe-tabs" role="tablist" aria-label="Recipe sections">
    <li class="nav-item" role="presentation"><button class="nav-link active" id="btt-rec-ing-tab" data-bs-toggle="tab" data-bs-target="#btt-rec-ing" type="button" role="tab" aria-controls="btt-rec-ing" aria-selected="true">Ingredients</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" id="btt-rec-met-tab" data-bs-toggle="tab" data-bs-target="#btt-rec-met" type="button" role="tab" aria-controls="btt-rec-met" aria-selected="false">Method</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" id="btt-rec-not-tab" data-bs-toggle="tab" data-bs-target="#btt-rec-not" type="button" role="tab" aria-controls="btt-rec-not" aria-selected="false">Notes</button></li>
  </ul>
  <div class="tab-content btt-folder-body">
    <div class="tab-pane fade show active" id="btt-rec-ing" role="tabpanel" aria-labelledby="btt-rec-ing-tab" tabindex="0">
      <h3 class="h5 mb-3">Brown butter oat cookies</h3>
      <ul class="list-unstyled mb-0 btt-recipe-list">
        <li><span>Butter</span><span>170 g</span></li>
        <li><span>Dark brown sugar</span><span>150 g</span></li>
        <li><span>Rolled oats</span><span>120 g</span></li>
        <li><span>Plain flour</span><span>140 g</span></li>
      </ul>
    </div>
    <div class="tab-pane fade" id="btt-rec-met" role="tabpanel" aria-labelledby="btt-rec-met-tab" tabindex="0">
      <h3 class="h5 mb-3">Method</h3>
      <ol class="mb-0 ps-3">
        <li class="mb-1">Brown the butter until it smells nutty, then cool for ten minutes.</li>
        <li class="mb-1">Beat in the sugar and egg, then fold in oats and flour.</li>
        <li>Bake at 180&deg;C for 11 minutes.</li>
      </ol>
    </div>
    <div class="tab-pane fade" id="btt-rec-not" role="tabpanel" aria-labelledby="btt-rec-not-tab" tabindex="0">
      <h3 class="h5 mb-3">Notes</h3>
      <p class="mb-0">Chill the dough overnight for thicker cookies. Flaky salt on top before baking is worth it.</p>
    </div>
  </div>
</div>

Code snippet tabs for HTML, CSS and JS

  • nav-underline
  • --bs-nav-underline-*
  • data-bs-theme="dark"
  • font-monospace

Click a tab to see the themed active state.

A docs-style code viewer: a dark nav-underline row switches between three pre blocks. The underline color and gap come from --bs-nav-underline-link-active-color and --bs-nav-underline-gap, and data-bs-theme="dark" on the wrapper handles the rest of the colors.

View code
<div class="btt-code w-100" data-bs-theme="dark">
  <div class="btt-code-head d-flex align-items-center justify-content-between gap-3 px-3">
    <ul class="nav nav-underline btt-code-tabs" id="btt-code-tabs" role="tablist" aria-label="Snippet language">
      <li class="nav-item" role="presentation"><button class="nav-link active" id="btt-code-html-tab" data-bs-toggle="tab" data-bs-target="#btt-code-html" type="button" role="tab" aria-controls="btt-code-html" aria-selected="true">HTML</button></li>
      <li class="nav-item" role="presentation"><button class="nav-link" id="btt-code-css-tab" data-bs-toggle="tab" data-bs-target="#btt-code-css" type="button" role="tab" aria-controls="btt-code-css" aria-selected="false">CSS</button></li>
      <li class="nav-item" role="presentation"><button class="nav-link" id="btt-code-js-tab" data-bs-toggle="tab" data-bs-target="#btt-code-js" type="button" role="tab" aria-controls="btt-code-js" aria-selected="false">JS</button></li>
    </ul>
    <span class="small btt-code-file d-none d-sm-inline">toast.html</span>
  </div>
  <div class="tab-content">
    <div class="tab-pane fade show active" id="btt-code-html" role="tabpanel" aria-labelledby="btt-code-html-tab" tabindex="0">
<pre class="mb-0"><code>&lt;div class="toast" role="status"&gt;
  &lt;div class="toast-body"&gt;Saved.&lt;/div&gt;
&lt;/div&gt;</code></pre>
    </div>
    <div class="tab-pane fade" id="btt-code-css" role="tabpanel" aria-labelledby="btt-code-css-tab" tabindex="0">
<pre class="mb-0"><code>.toast {
  --bs-toast-bg: #1e293b;
  --bs-toast-color: #f8fafc;
}</code></pre>
    </div>
    <div class="tab-pane fade" id="btt-code-js" role="tabpanel" aria-labelledby="btt-code-js-tab" tabindex="0">
<pre class="mb-0"><code>const el = document.querySelector('.toast');
bootstrap.Toast.getOrCreateInstance(el).show();</code></pre>
    </div>
  </div>
</div>

JavaScript API and events

Tabs that react to show.bs.tab, shown.bs.tab and hide.bs.tab, or get switched with bootstrap.Tab.getOrCreateInstance(). Each script uses textContent and the plugin API only.

Help center reacting to shown.bs.tab

  • shown.bs.tab
  • event.relatedTarget
  • breadcrumb
  • textContent

A shown.bs.tab listener on the tab list reads event.target and event.relatedTarget, then updates the breadcrumb and a status line with textContent. That is the hook to use for analytics, lazy loading or syncing other UI, because show() returns before the pane is visible.

View code
<div class="card border-0 shadow-sm btt-help">
  <div class="card-body p-3 p-sm-4">
    <nav aria-label="breadcrumb">
      <ol class="breadcrumb small mb-3">
        <li class="breadcrumb-item"><a href="#">Help center</a></li>
        <li class="breadcrumb-item active" aria-current="page" id="btt-help-crumb">Getting started</li>
      </ol>
    </nav>
    <ul class="nav nav-pills nav-fill btt-help-tabs mb-3" id="btt-help-tabs" role="tablist" aria-label="Help topics">
      <li class="nav-item" role="presentation"><button class="nav-link active" id="btt-help-start-tab" data-bs-toggle="pill" data-bs-target="#btt-help-start" type="button" role="tab" aria-controls="btt-help-start" aria-selected="true">Getting started</button></li>
      <li class="nav-item" role="presentation"><button class="nav-link" id="btt-help-bill-tab" data-bs-toggle="pill" data-bs-target="#btt-help-bill" type="button" role="tab" aria-controls="btt-help-bill" aria-selected="false">Billing</button></li>
      <li class="nav-item" role="presentation"><button class="nav-link" id="btt-help-team-tab" data-bs-toggle="pill" data-bs-target="#btt-help-team" type="button" role="tab" aria-controls="btt-help-team" aria-selected="false">Teams</button></li>
    </ul>
    <div class="tab-content">
      <div class="tab-pane fade show active" id="btt-help-start" role="tabpanel" aria-labelledby="btt-help-start-tab" tabindex="0">
        <p class="mb-0">Create a workspace, invite one teammate and publish your first board. Most teams are set up in under ten minutes.</p>
      </div>
      <div class="tab-pane fade" id="btt-help-bill" role="tabpanel" aria-labelledby="btt-help-bill-tab" tabindex="0">
        <p class="mb-0">Invoices go out on the first of the month. Change your card or download past invoices from Settings, then Billing.</p>
      </div>
      <div class="tab-pane fade" id="btt-help-team" role="tabpanel" aria-labelledby="btt-help-team-tab" tabindex="0">
        <p class="mb-0">Owners can add members, set roles and transfer ownership. Guests see only the boards they are invited to.</p>
      </div>
    </div>
    <p class="small text-body-secondary border-top pt-3 mt-3 mb-0" id="btt-help-status" aria-live="polite">Pick a topic to see the event details.</p>
  </div>
</div>

Checkout steps driven by the Tab API

  • bootstrap.Tab.getOrCreateInstance
  • disabled
  • progress
  • shown.bs.tab

Next and Back buttons call bootstrap.Tab.getOrCreateInstance(tab).show(), and later steps stay disabled until the reader reaches them, so the arrow keys skip them too. A shown.bs.tab listener updates the progress bar and its aria-valuenow.

View code
<div class="card border-0 shadow-sm btt-wizard">
  <div class="card-body p-3 p-sm-4">
    <div class="progress mb-3" role="progressbar" aria-label="Checkout progress" aria-valuenow="33" aria-valuemin="0" aria-valuemax="100" id="btt-wiz-progress" style="height: 6px">
      <div class="progress-bar btt-wiz-bar" style="width: 33%"></div>
    </div>
    <ul class="nav nav-pills nav-justified btt-wiz-tabs mb-3" id="btt-wiz-tabs" role="tablist" aria-label="Checkout steps">
      <li class="nav-item" role="presentation"><button class="nav-link active" id="btt-wiz-ship-tab" data-bs-toggle="pill" data-bs-target="#btt-wiz-ship" type="button" role="tab" aria-controls="btt-wiz-ship" aria-selected="true">Shipping</button></li>
      <li class="nav-item" role="presentation"><button class="nav-link" id="btt-wiz-pay-tab" data-bs-toggle="pill" data-bs-target="#btt-wiz-pay" type="button" role="tab" aria-controls="btt-wiz-pay" aria-selected="false" disabled>Payment</button></li>
      <li class="nav-item" role="presentation"><button class="nav-link" id="btt-wiz-rev-tab" data-bs-toggle="pill" data-bs-target="#btt-wiz-rev" type="button" role="tab" aria-controls="btt-wiz-rev" aria-selected="false" disabled>Review</button></li>
    </ul>
    <div class="tab-content">
      <div class="tab-pane fade show active" id="btt-wiz-ship" role="tabpanel" aria-labelledby="btt-wiz-ship-tab">
        <div class="form-check border rounded-3 p-3 ps-5 mb-2">
          <input class="form-check-input" type="radio" name="btt-wiz-speed" id="btt-wiz-std" checked>
          <label class="form-check-label d-flex justify-content-between" for="btt-wiz-std"><span>Standard, 3 to 5 days</span><span class="fw-semibold">Free</span></label>
        </div>
        <div class="form-check border rounded-3 p-3 ps-5">
          <input class="form-check-input" type="radio" name="btt-wiz-speed" id="btt-wiz-exp">
          <label class="form-check-label d-flex justify-content-between" for="btt-wiz-exp"><span>Express, next day</span><span class="fw-semibold">$9</span></label>
        </div>
      </div>
      <div class="tab-pane fade" id="btt-wiz-pay" role="tabpanel" aria-labelledby="btt-wiz-pay-tab">
        <div class="form-check border rounded-3 p-3 ps-5 mb-2">
          <input class="form-check-input" type="radio" name="btt-wiz-method" id="btt-wiz-card" checked>
          <label class="form-check-label" for="btt-wiz-card">Saved card ending in 4242</label>
        </div>
        <div class="form-check border rounded-3 p-3 ps-5">
          <input class="form-check-input" type="radio" name="btt-wiz-method" id="btt-wiz-invoice">
          <label class="form-check-label" for="btt-wiz-invoice">Pay by invoice</label>
        </div>
      </div>
      <div class="tab-pane fade" id="btt-wiz-rev" role="tabpanel" aria-labelledby="btt-wiz-rev-tab">
        <ul class="list-group mb-0">
          <li class="list-group-item d-flex justify-content-between"><span>Linen throw, sand</span><span>$64</span></li>
          <li class="list-group-item d-flex justify-content-between"><span>Shipping</span><span>Free</span></li>
          <li class="list-group-item d-flex justify-content-between fw-semibold"><span>Total</span><span>$64</span></li>
        </ul>
      </div>
    </div>
    <div class="d-flex justify-content-between mt-3">
      <button type="button" class="btn btn-outline-secondary" id="btt-wiz-back" disabled>Back</button>
      <button type="button" class="btn btt-wiz-next" id="btt-wiz-next">Continue</button>
    </div>
  </div>
</div>

Unsaved changes guard with hide.bs.tab

  • hide.bs.tab
  • event.preventDefault()
  • alert
  • nav-tabs

Edit the headline, then try to switch tabs.

hide.bs.tab fires on the current tab before anything changes, and calling event.preventDefault() there cancels the switch. The script tracks edits in the first pane, blocks leaving it while changes are unsaved and shows a Bootstrap alert with Save and Discard buttons.

View code
<div class="card border-0 shadow-sm btt-guard">
  <div class="card-body p-3 p-sm-4">
    <ul class="nav nav-tabs mb-3" id="btt-guard-tabs" role="tablist" aria-label="Post editor">
      <li class="nav-item" role="presentation"><button class="nav-link active" id="btt-guard-edit-tab" data-bs-toggle="tab" data-bs-target="#btt-guard-edit" type="button" role="tab" aria-controls="btt-guard-edit" aria-selected="true">Edit</button></li>
      <li class="nav-item" role="presentation"><button class="nav-link" id="btt-guard-seo-tab" data-bs-toggle="tab" data-bs-target="#btt-guard-seo" type="button" role="tab" aria-controls="btt-guard-seo" aria-selected="false">Search preview</button></li>
    </ul>
    <div class="tab-content">
      <div class="tab-pane show active" id="btt-guard-edit" role="tabpanel" aria-labelledby="btt-guard-edit-tab">
        <label for="btt-guard-title" class="form-label small fw-medium">Headline</label>
        <input type="text" class="form-control mb-3" id="btt-guard-title" value="Five ways to speed up a slow checkout">
        <div class="alert alert-warning d-none mb-0 py-2" id="btt-guard-alert" role="alert">
          <p class="small mb-2">You have unsaved changes. Save or discard them before switching tabs.</p>
          <button type="button" class="btn btn-sm btn-dark" id="btt-guard-save">Save</button>
          <button type="button" class="btn btn-sm btn-link text-dark" id="btt-guard-discard">Discard</button>
        </div>
      </div>
      <div class="tab-pane" id="btt-guard-seo" role="tabpanel" aria-labelledby="btt-guard-seo-tab" tabindex="0">
        <p class="small text-success mb-1">blog.example.com</p>
        <p class="h6 mb-1 btt-guard-serp" id="btt-guard-preview">Five ways to speed up a slow checkout</p>
        <p class="small text-body-secondary mb-0">Shorter forms, fewer redirects and a guest option. A practical list for store owners.</p>
      </div>
    </div>
  </div>
</div>

What are Bootstrap tabs and how do they work?

The markup comes in two parts. A nav list holds the tab triggers, and a tab-content container holds one tab-pane per trigger.

The Tab plugin, shipped with the Bootstrap framework, links each trigger to its pane and moves the active and show classes around when a trigger is used.

That switching logic lives in JavaScript, not in the stylesheet. Adding nav-tabs to a list only changes how it looks, and Bootstrap’s plugins have no special fallback when scripting is off (Bootstrap 5.3 documentation).

A navbar styled with nav-tabs is a different animal. It links to other pages and must not carry the tablist, tab and tabpanel roles, while a real tab interface swaps panes on the same page and does carry them.

The docs draw that line themselves. Version 5.0.0 also renamed the docs page from “Navs” to “Navs & tabs” (Bootstrap migration guide).

Everything below follows Bootstrap 5.3.8, the release the official docs list as current in October 2026.

How to create Bootstrap tabs step by step

Start with the plugin, since nothing else works without it.

  1. Load one build of the plugin, such as bootstrap.bundle.min.js from jsDelivr, or install the bootstrap package from npm. The docs warn against including both bootstrap.js and bootstrap.min.js.
  2. Build a ul with the classes nav and nav-tabs and role=”tablist”, with one li.nav-item per tab.
  3. Put a button.nav-link inside each li, with data-bs-toggle=”tab” and a data-bs-target that points at the pane id.
  4. Add a div.tab-content after the nav, holding one div.tab-pane per trigger. Each pane gets the id its trigger targets.
  5. Put active on the first button and active on the first pane.
  6. Add role, aria-controls and aria-selected to the triggers, and role and aria-labelledby to the panes.
<ul class="nav nav-tabs" id="myTab" role="tablist">
  <li class="nav-item" role="presentation">
  <button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#home-pane" type="button" role="tab" aria-controls="home-pane" aria-selected="true">Home</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile-pane" type="button" role="tab" aria-controls="profile-pane" aria-selected="false">Profile</button>
</li>
</ul>
<div class="tab-content">
  <div class="tab-pane active" id="home-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">...</div>
  <div class="tab-pane" id="profile-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">...</div>
</div>

The role and aria attributes in step 6 are what the ARIA tabs pattern asks for. That’s why the markup looks so much heavier than a plain nav. It is wordy, no way around it.

Anchors with an href also work as triggers, as older tutorials show. The docs still recommend buttons, because a tab triggers an in-page change and does not navigate anywhere.

This markup targets Bootstrap 5, and Bootstrap 4 has reached end of life according to the banner on its own docs.

Among sites running Bootstrap, 21.4% used version 4 and 10.7% used version 5 (W3Techs, 28 October 2026). Old data-toggle and data-target snippets are still easy to find for that reason.

Adding the fade effect

Add fade to every tab-pane. The first pane also needs show, or its content stays invisible after load.

<div class="tab-pane fade show active" id="home-pane" ...>...</div>
<div class="tab-pane fade" id="profile-pane" ...>...</div>

Skip fade and the panes switch instantly.

Which style fits: tabs, pills, underline, vertical, fill or justified?

Classic tab headers come from nav-tabs. If the active tab should look like a button, use nav-pills instead, and nav-underline gives a lighter look. Stack the triggers with flex-column, or stretch the row across the full width with nav-fill or nav-justified.

StyleClassResult
Tabsnav-tabsClassic headers, bordered active tab
Pillsnav-pillsRounded background on the active link
Underlinenav-underlineSimple bottom border under the active link
Fillnav-fillProportional widths, all space used
Justifiednav-justifiedEqual widths, all space used

Pills use data-bs-toggle=”pill” on the triggers. The rest of the markup stays the same.

nav-underline was added in Bootstrap 5.3.0, so earlier 5.x releases do not have it (Bootstrap 5.3 documentation).

Vertical tabs

Put flex-column on the nav, and place the nav beside the tab-content. The wrapper gets d-flex align-items-start. The trigger list gets nav flex-column nav-pills me-3, plus aria-orientation=”vertical”, and tab-content sits as the second child of the wrapper.

That is the layout from the 5.3 docs. Col-3 and col-9 columns from the grid system work as well, and they give the pane a fixed share of the width.

Fill versus justified

With nav-fill, items share the available space in proportion to their content, so widths differ. nav-justified gives every item the same width, which works when the labels are close in length.

If one label is far longer than the rest, go with nav-fill, because equal widths would squeeze it.

How to control Bootstrap tabs with JavaScript

Call getOrCreateInstance on the trigger element, then call show(). The call starts the transition and returns at once, so listen for shown.bs.tab when code has to run after the pane is visible.

Like every other Bootstrap plugin, the tab plugin exposes the same programmatic API, with a constructor, getInstance, getOrCreateInstance and dispose.

Methods

const trigger = document.querySelector('#profile-tab');
  bootstrap.Tab.getOrCreateInstance(trigger).show();

Wire that to a click on any button and it jumps to the Profile tab from anywhere on the page.

The show method selects the tab and its pane and hides the previous pair. getInstance returns the existing instance, or null when none exists. getOrCreateInstance returns the instance or creates one, and dispose destroys it.

Because getInstance returns null when nothing was initialized, getOrCreateInstance is the safer call from an external button.

Every method is asynchronous, and a call made during a running transition is ignored (Bootstrap 5.3 documentation).

Events and their order

Each switch fires its events in the same sequence. hide.bs.tab goes first, on the current active tab, then show.bs.tab on the tab about to be shown. After the switch, hidden.bs.tab fires on the previous tab and shown.bs.tab on the new one.

If no tab was active, hide.bs.tab and hidden.bs.tab do not fire.

In shown.bs.tab, event.target is the newly active tab and event.relatedTarget is the previous one.

The show and hide events can be cancelled with preventDefault(), which stops the switch before it starts, per the Bootstrap 5.3 JavaScript docs.

document.querySelectorAll('[data-bs-toggle="tab"]').forEach(el => {
  el.addEventListener('shown.bs.tab', event => {
    console.log(event.target, event.relatedTarget);
  });
});

How to open a specific tab and keep it after reload

Bootstrap keeps no tab state, so a reload returns to the tab marked active in the markup. Store the active tab in the URL hash for shareable links, or in localStorage for a per-browser memory, and call show() on load to restore it.

The gap is old. An early twbs/bootstrap issue titled “Linking to specific tab” (number 2415) asked for it, and the current docs list no state option for the plugin.

Joomla hit the same wall. After its move to Bootstrap 5, a reload opened the first tab, and a November 2022 pull request (number 39283) fixed the tab-state script, borrowing a solution from twbs/bootstrap issue 34767 (Joomla issue tracker).

const hash = window.location.hash;
const trigger = hash && document.querySelector('[data-bs-target="' + hash + '"]');
if (trigger) {
  bootstrap.Tab.getOrCreateInstance(trigger).show();
}

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

The localStorage version follows the same pattern. Save event.target.dataset.bsTarget under a key in the shown.bs.tab handler, and read it back before calling show().

The hash route carries the state in the link, so it can be shared or bookmarked, and other pages can deep-link to a tab, for example page.html#profile-pane. The downsides are small. Setting location.hash adds a history entry per click, and replaceState avoids that at the cost of back-button tab stepping. An unknown hash also needs a fallback to the default tab.

localStorage suits returning visitors, who land on their last tab without any change to the URL. The memory is per browser and per device though, so a link cannot carry it, and storage access can fail in private or restricted modes, so wrap reads and writes in try/catch.

Use the hash when people link to or share a tab. Use localStorage for pages users revisit, such as dashboards, and let the hash win when both are present.

What do Bootstrap tabs need for accessibility and keyboard use?

Keyboard use runs on a roving tabindex. Only the active tab sits in the Tab sequence, and the arrow keys, Home and End move between tabs (Bootstrap 5.3 documentation).

Tabs are easy to get wrong for web accessibility, because the semantics live in attributes that browsers do not infer from the markup.

Tab moves focus into the list, onto the active tab only. Left and right arrows activate the previous or next tab, and Home and End jump to the first and last. Every inactive tab gets tabindex=”-1″ when the plugin initializes.

Koha, the open-source library system, logged bug 38015 after moving to Bootstrap 5 because only the active tab took Tab focus. One commenter on the thread argued that this is the better behavior, since the arrow keys switch tabs and Tab skips the whole list (Koha bug tracker).

Bootstrap and the W3C pattern part ways in a couple of places. Bootstrap sends up and left to the previous tab and down and right to the next, in horizontal and vertical lists alike.

The W3C ARIA Authoring Practices Guide is stricter. A horizontal list ignores Up and Down so those keys keep their normal page scrolling, and Up and Down apply only when aria-orientation is vertical.

So in a horizontal Bootstrap tab list, pressing Down on a focused tab switches tabs instead of scrolling the page. Home and End are optional in the W3C pattern, and Bootstrap implements both.

Bootstrap activates a tab as the arrow keys move focus. The W3C guide recommends that behavior only when panels appear without noticeable latency, which typically means content loaded in advance, so lazy-loaded panes make arrow-key use feel slow.

Label the tab list. The W3C pattern wants aria-label or aria-labelledby on the tablist, and the Bootstrap docs example carries neither.

<ul class="nav nav-tabs" id="myTab" role="tablist" aria-label="Account sections">

Panes keep tabindex=”0″ unless the first element inside is already focusable, such as a link or an input. In that case the attribute adds a redundant stop and can go.

How to style Bootstrap tabs with CSS variables and Sass

Override the local CSS variables on the nav for per-component changes, and the Sass variables before compiling for site-wide changes. The nav CSS variables arrived in Bootstrap 5.2.0, and the nav-underline ones in 5.3.0 (Bootstrap 5.3 documentation).

CSS variables are set at runtime on the element, need no recompile and stay scoped to one tab set. Sass variables get compiled into the stylesheet, so they have to be set before the variables import to replace the defaults.

.nav-tabs.settings-tabs {
  --bs-nav-link-color: #495057;
  --bs-nav-tabs-link-active-color: #0b5ed7;
  --bs-nav-tabs-link-active-bg: #f8f9fa;
  --bs-nav-tabs-border-color: #dee2e6;
}

Use two classes on the same element. The variables are defined on .nav-tabs itself, so a value set on a parent wrapper never reaches them.

Each component carries its own set. .nav covers link padding, font size, font weight, color, hover color and disabled color. .nav-tabs adds border width, border color, border radius and hover border color, plus active color, background and border color. .nav-pills has border radius, active color and active background, and .nav-underline has gap, border width and active color.

The Sass side starts with two spacing variables, $nav-link-padding-y (.5rem by default) and $nav-link-padding-x (1rem).

@import "functions";
  $nav-link-padding-y: .75rem;
  $nav-link-padding-x: 1.25rem;
  @import "variables";
  // then maps, mixins and the rest of the Bootstrap imports

Default tab colors reference body variables such as –bs-body-bg and –bs-emphasis-color, so they follow data-bs-theme in Bootstrap 5.3 without extra rules.

Hard-coded replacements lose that, so check the color contrast of active and inactive tabs in both light and dark mode.

What changed in Bootstrap tabs between version 4 and version 5?

Bootstrap 5 adds the bs namespace to data attributes (data-toggle becomes data-bs-toggle, data-target becomes data-bs-target), drops the jQuery requirement, and replaces the jQuery call with the bootstrap.Tab class. The four event names stay the same.

ItemBootstrap 4.6Bootstrap 5.3
Toggle attributedata-toggle=”tab”data-bs-toggle=”tab”
Target attributedata-target (or href on links)data-bs-target (or href on links)
Activation call$(‘#myTab button’).tab(‘show’)bootstrap.Tab.getOrCreateInstance(el).show()
Event listenerjQuery .on(‘shown.bs.tab’)addEventListener(‘shown.bs.tab’)

Plenty carries over untouched. hide.bs.tab, show.bs.tab, hidden.bs.tab and shown.bs.tab fire in the same order, triggers are buttons, and panes use fade show active (Bootstrap 4.6 and 5.3 documentation).

Bootstrap 5 does not need jQuery, but when it detects jQuery on the page it registers its plugins there. Old $(‘#myTab button’).tab(‘show’) calls therefore keep working on pages that still load jQuery (Bootstrap 5.3 documentation).

Version 5 has been the default since May 5, 2021. Bootstrap 3 reached end of life on July 24, 2019 and Bootstrap 4 on January 1, 2023, and neither gets features or security fixes (Bootstrap release schedule in the twbs/release repository).

For tabs, the minimal migration is to rename data-toggle to data-bs-toggle and data-target to data-bs-target. After that, either swap .tab(‘show’) for bootstrap.Tab.getOrCreateInstance(el).show(), or keep jQuery loaded. The panes can stay as they are, since 4.6 already used fade show active.

When do Bootstrap tabs fail or fit poorly?

Tabs break in a few well-documented ways, and they are a poor fit for core content that every reader needs.

Dropdowns are the first one. The plugin does not support dropdown menus inside a tab list, because the active tab’s trigger can end up hidden in a closed menu, and no standard ARIA pattern describes that construct (Bootstrap docs).

Keep any Bootstrap dropdown beside the tab list instead, or flatten its items into plain tabs.

An element takes one set of data attributes, so a tab button cannot also trigger a tooltip through data-bs-toggle. Put the tooltip on a wrapper.

Bootstrap’s JavaScript is not fully compatible with React, Vue and Angular, which assume full knowledge of the DOM. The docs point to React Bootstrap and ng-bootstrap instead.

Tabs inside accordions had their own problem. Bootstrap 5.1.0 shipped a regression where the active tab’s content vanished after the surrounding accordion was reopened (twbs/bootstrap issue 34698). Pull request 34835 fixed it, and the fix shipped in Bootstrap 5.1.1 in September 2021.

When tabs refuse to work, check that only one Bootstrap JavaScript build is loaded and that each data-bs-target matches a pane id and is a valid selector, with special characters escaped. Look for leftover data-toggle attributes from Bootstrap 4 snippets too. And if jQuery UI or Prototype is on the page, that can cause trouble, since Bootstrap does not officially support either.

The CDC’s web content guidance advises against tabs and accordions for most content. Readers overlook important content in them and cannot scan it easily. The modules also work poorly in small viewports and do not print well.

The California Courts design system adds further limits: use a table when readers must compare content across sections, and use separate pages when the content inside a tab is complex and needs further navigation.

When several sections should stay open at once, a Bootstrap accordion fits better, because omitting data-bs-parent keeps each item open independently (Bootstrap 5.3 documentation).

Sources disagree on one practice. CDC guidance advises against linking to content inside a tab or accordion section, while the hash technique does exactly that.

Keep hash links for application screens and dashboards, where people expect to land on a specific tab.

Use tabs for parallel views of one subject that readers open one at a time.

Common Questions About Bootstrap Tabs

How do you disable a tab in Bootstrap?

Add the disabled attribute to the button trigger, as the official 5.3 example does. For an anchor trigger, use the disabled class plus aria-disabled=”true”.

A disabled button also drops out of the keyboard sequence.

Can Bootstrap tabs be nested?

Yes. Put a second nav and tab-content inside a tab-pane, and give every trigger and pane its own unique id so the data-bs-target selectors never collide.

An early bug removed the active class from inner tabs whenever the outer tabs switched (twbs/bootstrap issue 310), so test the inner state after switching outer tabs.

How do you lazy load content inside a tab?

Fetch the content in a shown.bs.tab handler the first time a tab opens, and skip the request on later visits.

The tab activates before the request finishes, so put a loading state in the pane.

How can tabs sit inside a card?

Place the nav inside the card header with the classes nav, nav-tabs and card-header-tabs, then put the tab-content in the card body.

Everything else follows the standard Bootstrap card markup.

How do you make tabs stack on small screens?

Add flex-column flex-sm-row to the nav. The triggers stack on the smallest breakpoint and sit in a row from sm upward (Bootstrap 5.3 documentation).

Add flex-sm-fill to each link to stretch them across the row. It gives you a responsive design without custom media queries.

What to Check Before Bootstrap Tabs Go Live

Tabs are ready to ship once the checks pass, and the order matters.

Check the attribute syntax against the installed major version first, because a wrong data attribute makes every other test meaningless. Keyboard comes second, from a labeled tab list, since it exposes labeling and focus gaps. State goes last, because it depends on working switching and events, so a reload should land on the intended tab.

Passing the keyboard check with the plugin’s automatic activation means keeping every pane in the initial HTML, which costs page weight.

This position was verified against Bootstrap 5.3.8 on 28 October 2026. The team planned that release as the last patch before 5.4.0, so rerun all 3 checks when it ships (Bootstrap blog, August 2025).