Tabs
Switches related panels in the same context with predictable selection and keyboard behavior.
Design
Functional preview
Running with the stable ds-tis/tabs JavaScript runtime.
Open the HTML/CSS/JS playground
Anatomy
Panel content goes here.
4.ds-tabs) — container with role="tablist".2 Tab (
.ds-tab) — individual tab button with role="tab".3 Active indicator — bottom border on the active tab.
4 Tab panel — content area with
role="tabpanel".
Default tabs
One tab is active, the others are in their default state. Each tab controls a corresponding panel.
This is the Overview panel content. It provides a summary of your account status and recent activity.
Settings panel for profile, notifications preferences, and account security options.
Activity panel listing recent logins, changes, and audit events.
With disabled tab
A disabled tab cannot be interacted with and is skipped during keyboard navigation.
General settings for your account: display name, language, and timezone preferences.
Multiple tabs
Tabs scroll horizontally when they overflow the container.
Dashboard panel showing project metrics, recent commits, and team activity overview.
Token mapping
| Property | Token | CSS variable |
|---|---|---|
| text (inactive) | component.tabs.label.color.default | --ds-tabs-label-color-default |
| text (hover) | component.tabs.label.color.hover | --ds-tabs-label-color-hover |
| text (active) | component.tabs.label.color.active | --ds-tabs-label-color-active |
| text (disabled) | component.tabs.label.color.disabled | --ds-tabs-label-color-disabled |
| active indicator | component.tabs.indicator.fill.active | --ds-tabs-indicator-fill-active |
| divider | semantic.overlay.default | --ds-overlay-default |
| list border | component.tabs.list.border-width.default | --ds-tabs-list-border-width-default |
| item padding | component.tabs.item.{padding-x,padding-top}.default | --ds-tabs-item-padding-*-default |
| gap to indicator | component.tabs.item.indicator-gap.default | --ds-tabs-item-indicator-gap-default |
| indicator height | component.tabs.indicator.height.default | --ds-tabs-indicator-height-default |
| label typography | component.tabs.label.{font-size,line-height,font-weight,letter-spacing}.default | --ds-tabs-label-*-default |
| panel padding | component.tabs.panel.padding-y.default | --ds-tabs-panel-padding-y-default |
| focus ring | component.focus-ring.{color.default,width} | --ds-focus-ring-color-default, --ds-focus-ring-width |
| focus ring radius | component.tabs.focus-ring.radius.default | --ds-tabs-focus-ring-radius-default |
Usage
When to use
Best practices
Content guidelines
| Rule | Example |
|---|---|
| Use nouns as labels | "Overview", "Settings", "Activity" — not "View overview" or "Go to settings" |
| Same hierarchy level | All tabs should represent parallel categories of the same content |
| No icons without text | Always include a text label — icons alone are ambiguous in tab context |
| Keep labels short (1-2 words) | "Pull Requests" — not "View all pull requests in this project" |
Related
Implementation
- Status
- Stable
- Distribution
- npm package
Installation
npm install ds-tispnpm add ds-tisyarn add ds-tisbun add ds-tisImports
import 'ds-tis/css'
import { initTabs } from 'ds-tis/tabs'
initTabs()Markup
<div class="ds-tabs" role="tablist" aria-label="Seções do projeto">
<button class="ds-tab ds-tab--active" role="tab" id="overview-tab" aria-selected="true" aria-controls="overview-panel">Visão geral</button>
<button class="ds-tab" role="tab" id="team-tab" aria-selected="false" aria-controls="team-panel" tabindex="-1">Equipe</button>
</div>
<div class="ds-tab-panel" role="tabpanel" id="overview-panel" aria-labelledby="overview-tab">Resumo do projeto.</div>
<div class="ds-tab-panel" role="tabpanel" id="team-panel" aria-labelledby="team-tab" hidden>Equipe do projeto.</div>Web implementation contract
CSS classes
| Class | Description |
|---|---|
ds-tabs | Tab list container |
ds-tab | Individual tab button |
ds-tab--active | Active / selected tab |
disabled (HTML attribute) | Disabled tab — use the native disabled attribute; there is no ds-tab--disabled class |
ds-tab-panel | Tab panel content area |
Required runtime
CSS defines anatomy and visual states, but selection, roving tabindex, arrow keys, and panel sync depend on the public ds-tis/tabs module (initTabs / destroyTabs). Event: ds-tabs-change. Call destroyTabs on unmount.
Accessibility
Keyboard interaction
| Key | Action |
|---|---|
Tab | Moves focus into the tab list, landing on the active tab |
Arrow Left / Arrow Right | Moves to previous / next tab (auto-activates) |
Home | Moves to the first tab |
End | Moves to the last tab |
Tab (from list) | Moves focus into the active panel content |
Disabled tabs are skipped during arrow key navigation. Only the active tab is in the Tab sequence (tabindex="0"); all others have tabindex="-1".
Accessibility
| WCAG criterion | Requirement | Status |
|---|---|---|
| 4.1.2 Name, Role, Value (A) | role="tablist", role="tab", role="tabpanel". aria-selected on tabs. aria-controls / aria-labelledby linking tabs to panels. | ✓ |
| 2.1.1 Keyboard (A) | Arrow keys navigate between tabs. Tab enters and exits the tab list. | ✓ |
| 2.4.11 Focus Appearance (AA) | Focus ring visible on the active tab when focused. | ✓ |
role="tablist" — on the tab list container.role="tab" — on each tab button.role="tabpanel" — on each content panel.aria-selected="true|false" — indicates the active tab.aria-controls — on each tab, pointing to the corresponding panel ID.aria-labelledby — on each panel, pointing to the corresponding tab ID.aria-disabled="true" — on disabled tabs.
Output responsibility
Initialize the public runtime after render and destroy it when the owning view unmounts.
Validation evidence
Covered by the stable Storybook, public runtime tests when applicable, keyboard scenarios, responsive checks, and Axe.