Skip to content

Documentation

On this page

Tabs

Bootstrap’s JavaScript tabs switch layered panes from a tab list. There is still no native HTML control for that, so this page ships a small <pe-tabs> behavior instead of the Tab plugin.

Sizes are minified and gzip. A plugin figure includes the Bootstrap runtime that plugin imports. Shipping every plugin is bootstrap.bundle.min.js at 23.3 KB gzip, not the sum of these rows.
What runs it JavaScript
Bootstrap 5.3 Tab plugin 5.8 KB gzip
This repo pe-tabs. role=tab and aria-controls. Required behavior. 1.3 KB gzip
Markup this component accepts. The full inventory is /components.json.
Required <pe-tabs> · src/behaviors/tabs.js . Keeps the selection, the roving tabindex, and the .active / .show panes in sync.
Avoid
  • Do not wrap the tabs in <pe-focus-group>. <pe-tabs> already owns the arrow keys.
  • A .nav-tabs list of page links is a different thing and needs no tab roles.

How it works

Wrap the tab list and the panes in <pe-tabs> and load src/behaviors/tabs.js. The element reads role="tab" and opens the pane whose id is aria-controls. Do not add data-bs-toggle or data-bs-target. role="tablist" and role="tabpanel" stay in the markup you write.

The selected tab has aria-selected="true" and tabindex="0". The others have tabindex="-1". Left and Right move in a horizontal list. aria-orientation="vertical" on the tab list uses Up and Down instead. Home and End jump to the ends. Choosing a tab also selects it. One pane stays open.

Bootstrap’s CSS still shows the pane: .tab-pane.active is visible, and .fade also needs .show. The element keeps those classes in step with the selection. Without the module, the pane that already has .active stays on screen and the other tabs do nothing. Do not also wrap the tabs in <pe-focus-group>. This element already owns the arrow keys.

Tabs

Click or press Left and Right. The note field keeps its own arrow keys.

This is the home pane. Arrow keys in the field below stay in the field.

This is the profile pane.

This is the contact pane.

HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/tabs.js"></script>

<pe-tabs>
  <ul class="nav nav-tabs" role="tablist">
    <li class="nav-item" role="presentation">
      <button class="nav-link active" id="home-tab" type="button" role="tab" aria-controls="home-tab-pane" aria-selected="true">Home</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="profile-tab" type="button" role="tab" aria-controls="profile-tab-pane" aria-selected="false" tabindex="-1">Profile</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="contact-tab" type="button" role="tab" aria-controls="contact-tab-pane" aria-selected="false" tabindex="-1">Contact</button>
    </li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane active show" id="home-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">
      <p class="mt-3 mb-0">This is the home pane. Arrow keys in the field below stay in the field.</p>
      <input class="form-control mt-3" type="text" value="Edit me" aria-label="Note">
    </div>
    <div class="tab-pane" id="profile-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
      <p class="mt-3 mb-0">This is the profile pane.</p>
    </div>
    <div class="tab-pane" id="contact-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
      <p class="mt-3 mb-0">This is the contact pane.</p>
    </div>
  </div>
</pe-tabs>

Fade

A pane with the fade class fades out before the next one fades in.

The home pane fades.

The profile pane fades.

The contact pane fades.

HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/tabs.js"></script>

<pe-tabs>
  <ul class="nav nav-tabs" role="tablist">
    <li class="nav-item" role="presentation">
      <button class="nav-link active" id="fade-home-tab" type="button" role="tab" aria-controls="fade-home-tab-pane" aria-selected="true">Home</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="fade-profile-tab" type="button" role="tab" aria-controls="fade-profile-tab-pane" aria-selected="false" tabindex="-1">Profile</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="fade-contact-tab" type="button" role="tab" aria-controls="fade-contact-tab-pane" aria-selected="false" tabindex="-1">Contact</button>
    </li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane fade show active" id="fade-home-tab-pane" role="tabpanel" aria-labelledby="fade-home-tab" tabindex="0">
      <p class="mt-3 mb-0">The home pane fades.</p>
    </div>
    <div class="tab-pane fade" id="fade-profile-tab-pane" role="tabpanel" aria-labelledby="fade-profile-tab" tabindex="0">
      <p class="mt-3 mb-0">The profile pane fades.</p>
    </div>
    <div class="tab-pane fade" id="fade-contact-tab-pane" role="tabpanel" aria-labelledby="fade-contact-tab" tabindex="0">
      <p class="mt-3 mb-0">The contact pane fades.</p>
    </div>
  </div>
</pe-tabs>

Pills

nav-pills is only a class. The roles are the same.

Pills use the same widget. Only the nav class changes.

This is the profile pane.

This is the contact pane.

HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/tabs.js"></script>

<pe-tabs>
  <ul class="nav nav-pills" role="tablist">
    <li class="nav-item" role="presentation">
      <button class="nav-link active" id="pills-home-tab" type="button" role="tab" aria-controls="pills-home-tab-pane" aria-selected="true">Home</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="pills-profile-tab" type="button" role="tab" aria-controls="pills-profile-tab-pane" aria-selected="false" tabindex="-1">Profile</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="pills-contact-tab" type="button" role="tab" aria-controls="pills-contact-tab-pane" aria-selected="false" tabindex="-1">Contact</button>
    </li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane active show" id="pills-home-tab-pane" role="tabpanel" aria-labelledby="pills-home-tab" tabindex="0">
      <p class="mt-3 mb-0">Pills use the same widget. Only the nav class changes.</p>
    </div>
    <div class="tab-pane" id="pills-profile-tab-pane" role="tabpanel" aria-labelledby="pills-profile-tab" tabindex="0">
      <p class="mt-3 mb-0">This is the profile pane.</p>
    </div>
    <div class="tab-pane" id="pills-contact-tab-pane" role="tabpanel" aria-labelledby="pills-contact-tab" tabindex="0">
      <p class="mt-3 mb-0">This is the contact pane.</p>
    </div>
  </div>
</pe-tabs>

Vertical

aria-orientation=vertical on the tab list. Up and Down select. Left and Right do not.

Up and Down move between these tabs. Left and Right do not.

This is the profile pane.

This is the messages pane.

HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/tabs.js"></script>

<pe-tabs>
  <div class="d-flex align-items-start">
    <div class="nav flex-column nav-pills me-3" role="tablist" aria-orientation="vertical">
      <button class="nav-link active" id="vert-home-tab" type="button" role="tab" aria-controls="vert-home-tab-pane" aria-selected="true">Home</button>
      <button class="nav-link" id="vert-profile-tab" type="button" role="tab" aria-controls="vert-profile-tab-pane" aria-selected="false" tabindex="-1">Profile</button>
      <button class="nav-link" id="vert-contact-tab" type="button" role="tab" aria-controls="vert-contact-tab-pane" aria-selected="false" tabindex="-1">Messages</button>
    </div>
    <div class="tab-content">
      <div class="tab-pane active show" id="vert-home-tab-pane" role="tabpanel" aria-labelledby="vert-home-tab" tabindex="0">
        <p class="mb-0">Up and Down move between these tabs. Left and Right do not.</p>
      </div>
      <div class="tab-pane" id="vert-profile-tab-pane" role="tabpanel" aria-labelledby="vert-profile-tab" tabindex="0">
        <p class="mb-0">This is the profile pane.</p>
      </div>
      <div class="tab-pane" id="vert-contact-tab-pane" role="tabpanel" aria-labelledby="vert-contact-tab" tabindex="0">
        <p class="mb-0">This is the messages pane.</p>
      </div>
    </div>
  </div>
</pe-tabs>

Disabled

A tab with the disabled class and aria-disabled=true is skipped.

Arrow keys skip the disabled tab.

This pane is not reachable.

This is the contact pane.

HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/tabs.js"></script>

<pe-tabs>
  <ul class="nav nav-tabs" role="tablist">
    <li class="nav-item" role="presentation">
      <button class="nav-link active" id="dis-home-tab" type="button" role="tab" aria-controls="dis-home-tab-pane" aria-selected="true">Home</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link disabled" id="dis-profile-tab" type="button" role="tab" aria-controls="dis-profile-tab-pane" aria-selected="false" aria-disabled="true" tabindex="-1">Disabled</button>
    </li>
    <li class="nav-item" role="presentation">
      <button class="nav-link" id="dis-contact-tab" type="button" role="tab" aria-controls="dis-contact-tab-pane" aria-selected="false" tabindex="-1">Contact</button>
    </li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane active show" id="dis-home-tab-pane" role="tabpanel" aria-labelledby="dis-home-tab" tabindex="0">
      <p class="mt-3 mb-0">Arrow keys skip the disabled tab.</p>
    </div>
    <div class="tab-pane" id="dis-profile-tab-pane" role="tabpanel" aria-labelledby="dis-profile-tab" tabindex="0">
      <p class="mt-3 mb-0">This pane is not reachable.</p>
    </div>
    <div class="tab-pane" id="dis-contact-tab-pane" role="tabpanel" aria-labelledby="dis-contact-tab" tabindex="0">
      <p class="mt-3 mb-0">This is the contact pane.</p>
    </div>
  </div>
</pe-tabs>

Accessibility

This follows the WAI-ARIA tabs pattern: role="tablist", role="tab", role="tabpanel", aria-controls, and aria-selected. The selected tab is tabindex="0"; the others are -1.

Left and Right move in a horizontal list. aria-orientation="vertical" uses Up and Down. Home and End jump to the ends. Choosing a tab also selects it.

Browser support

Three columns, not one baseline. The same row is on the overview.
No script Polyfill No fallback
The .active pane stays visible. Other tabs do not switch. None. pe-tabs is required to switch panes. It is not a polyfill. No native tab control yet. Dropdowns inside a tab list are not supported.

Why this is not native HTML

Open UI’s tabs draft is not in browsers. Until it is, the WAI-ARIA tabs pattern needs script: roving tabindex, arrow keys, and aria-selected.

  • <details name> is a disclosure, not a tab. Every summary can still collapse, including the last one, and the trigger has to wrap the panel.
  • A hidden checkbox or radio can show a pane, but the control is announced as a form field, and the focus ring sits on the hidden input.

A .nav-tabs list used as page links is a different thing. Those links do not need tab roles or this element.