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.
| 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 |
| Required | <pe-tabs> · src/behaviors/tabs.js . Keeps the selection, the roving tabindex, and the .active / .show panes in sync. |
|---|---|
| Avoid |
|
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.
<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.
<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.
<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.
<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.
<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
| 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.