Tabs
Organize related panels with automatic activation, roving focus, and synchronized ARIA state.
Tabs
Tabs use automatic activation and a roving tabindex. Horizontal lists use Left/Right; vertical lists use Up/Down. Both support Home and End.
<div class="bs-tabs" role="tablist" aria-label="Account" data-bs-tabs>
<button class="bs-tab" id="profile-tab" type="button" role="tab"
aria-controls="profile-panel" aria-selected="true">Profile</button>
<button class="bs-tab" id="security-tab" type="button" role="tab"
aria-controls="security-panel">Security</button>
</div>
<div class="bs-tab-panel" id="profile-panel" role="tabpanel"
aria-labelledby="profile-tab">Profile settings</div>
<div class="bs-tab-panel" id="security-panel" role="tabpanel"
aria-labelledby="security-tab" hidden>Security settings</div>
<section x-data="bsTabs">
<div class="bs-tabs" role="tablist" aria-label="Account" x-bind="tablist">
<button class="bs-tab" id="profile-tab" type="button" role="tab"
x-bind="tab" aria-controls="profile-panel" aria-selected="true">Profile</button>
<button class="bs-tab" id="security-tab" type="button" role="tab"
x-bind="tab" aria-controls="security-panel">Security</button>
</div>
<div class="bs-tab-panel" id="profile-panel" role="tabpanel"
aria-labelledby="profile-tab" x-bind="panel">Profile settings</div>
<div class="bs-tab-panel" id="security-panel" role="tabpanel"
aria-labelledby="security-tab" x-bind="panel" hidden>Security settings</div>
</section>
import { useTabs } from "@boobstrap/react";
function Account() {
const tabs = useTabs({ defaultSelectedId: "profile-tab" });
return (
<>
<div className="bs-tabs" aria-label="Account" {...tabs.getTablistProps()}>
<button className="bs-tab" {...tabs.getTabProps({
id: "profile-tab", controls: "profile-panel"
})}>Profile</button>
<button className="bs-tab" {...tabs.getTabProps({
id: "security-tab", controls: "security-panel"
})}>Security</button>
</div>
<div className="bs-tab-panel" id="profile-panel"
{...tabs.getPanelProps({ tabId: "profile-tab" })}>
Profile settings
</div>
<div className="bs-tab-panel" id="security-panel"
{...tabs.getPanelProps({ tabId: "security-tab" })}>
Security settings
</div>
</>
);
}
<script setup>
import { useTabs } from "@boobstrap/vue";
const tabs = useTabs({ defaultSelectedId: "profile-tab" });
</script>
<template>
<div class="bs-tabs" aria-label="Account" v-bind="tabs.getTablistProps()">
<button class="bs-tab" v-bind="tabs.getTabProps({
id: 'profile-tab', controls: 'profile-panel'
})">Profile</button>
<button class="bs-tab" v-bind="tabs.getTabProps({
id: 'security-tab', controls: 'security-panel'
})">Security</button>
</div>
<div class="bs-tab-panel" id="profile-panel"
v-bind="tabs.getPanelProps({ tabId: 'profile-tab' })">
Profile settings
</div>
<div class="bs-tab-panel" id="security-panel"
v-bind="tabs.getPanelProps({ tabId: 'security-tab' })">
Security settings
</div>
</template>
Structure
Each tab's aria-controls and panel's aria-labelledby form a two-way relationship.
State
The selected tab gets aria-selected="true"; every inactive panel uses hidden.
Accessibility
Use tabs only when panels are peer views. Preserve meaningful headings inside complex panels.
Pill tabs
Use the pills modifier for a compact view switcher. It changes presentation only; the same tab roles, relationships, and controller behavior still apply.
<div class="bs-tabs bs-tabs-pills" role="tablist" aria-label="Report period" data-bs-tabs>
<button class="bs-tab" id="pill-week-tab" type="button" role="tab" aria-controls="pill-week-panel" aria-selected="true">Week</button>
<button class="bs-tab" id="pill-month-tab" type="button" role="tab" aria-controls="pill-month-panel">Month</button>
</div>
<div class="bs-tab-panel" id="pill-week-panel" role="tabpanel" aria-labelledby="pill-week-tab">7-day report</div>
<div class="bs-tab-panel" id="pill-month-panel" role="tabpanel" aria-labelledby="pill-month-tab" hidden>30-day report</div>Contained tabs and panels
Pair contained tabs with contained panels when the selector and content should read as one bordered surface.
<section aria-label="Deployment details">
<div class="bs-tabs bs-tabs-contained" role="tablist" aria-label="Deployment detail" data-bs-tabs>
<button class="bs-tab" id="contained-summary-tab" type="button" role="tab" aria-controls="contained-summary-panel" aria-selected="true">Summary</button>
<button class="bs-tab" id="contained-log-tab" type="button" role="tab" aria-controls="contained-log-panel">Build log</button>
</div>
<div class="bs-tab-panel bs-tab-panel-contained" id="contained-summary-panel" role="tabpanel" aria-labelledby="contained-summary-tab">Deployment completed successfully.</div>
<div class="bs-tab-panel bs-tab-panel-contained" id="contained-log-panel" role="tabpanel" aria-labelledby="contained-log-tab" hidden>Build finished in 42 seconds.</div>
</section>Initialize and observe tabs
The tabs controller synchronizes selection, panel visibility, roving focus, and change events.
import { initBoobstrap } from "@boobstrap/boobstrap/js";
const boobstrap = initBoobstrap(document);
const tablist = document.querySelector("[data-bs-tabs]");
tablist.addEventListener("bs:tabs:changed", (event) => {
console.log("Selected tab", event.detail.tab.id);
});
// Later: boobstrap.destroy();Tab presentation classes
Presentation modifiers do not change tab semantics or behavior. Apply them to the base tablist and panel classes while keeping every ARIA relationship intact.
| Class | Purpose |
|---|---|
.bs-tabs-pills | Turns the base tablist into a compact pill-style view switcher. |
.bs-tabs-contained | Connects the tablist visually to a bordered content surface. |
.bs-tab-panel-contained | Adds the matching border, surface, radius, and padding to each controlled panel. |
JavaScript API
Import Tabs from @boobstrap/boobstrap/js/tabs.
| Contract | Purpose |
|---|---|
activate(tab) | Select a tab element and synchronize all controlled panels. |
bs:tabs:change | Cancelable before-event with previous and next tab/panel detail. |
bs:tabs:changed | Bubbling event after the completed transition. |