Tab Bar
TabBar is a headless tab list container that provides the ARIA tablist role and built-in keyboard navigation for tab interfaces. It manages arrow key navigation between tab elements, allowing users to cycle through tabs with Left/Right arrow keys, and jump to the first or last tab with Home/End keys.
Use TabBar when building a tabbed interface where users switch between different views or content panels. The component wraps tab buttons in a <div> with role="tablist" and handles keyboard interactions automatically. Consumer provides tab buttons as children (each with role="tab" and aria-selected) and is responsible for managing selection state and associating tab panels.
Example
Show demo markup
<div class="tab-bar" role="tablist"><button class="tab-bar-button" role="tab" aria-selected="true">Tab 1</button><button class="tab-bar-button" role="tab">Tab 2</button></div>
Usage example
<script lang="ts">
import TabBar from "./TabBar.svelte";
import TabBarButton from "../TabBarButton/TabBarButton.svelte";
let active = $state<"general" | "advanced">("general");
</script>
<TabBar label="Settings">
<TabBarButton
selected={active === "general"}
controls="panel-general"
onclick={() => (active = "general")}
>General</TabBarButton>
<TabBarButton
selected={active === "advanced"}
controls="panel-advanced"
onclick={() => (active = "advanced")}
>Advanced</TabBarButton>
</TabBar>
<section id="panel-general" role="tabpanel" hidden={active !== "general"}>General panel</section>
<section id="panel-advanced" role="tabpanel" hidden={active !== "advanced"}>Advanced panel</section>
Show Svelte source
// In your Svelte component:
import TabBar from "lily-design-system-svelte-headless/components/TabBar/TabBar.svelte";
<TabBar>
<!-- list children -->
</TabBar>
Usage
<TabBar label="Medication information">
<TabBarButton selected controls="panel-about">About this medicine</TabBarButton>
<TabBarButton controls="panel-dosage">Dosage</TabBarButton>
<TabBarButton controls="panel-side-effects">Side effects</TabBarButton>
<TabBarButton controls="panel-interactions">Interactions</TabBarButton>
</TabBar>
Props
label: string (required) -- accessible label for the tab list, applied via aria-label
children: slot (required) -- tab button elements to render inside the tablist
...restProps: unknown -- additional attributes spread onto the <div> element
Keyboard Interactions
- Arrow Right: Moves focus to the next tab; wraps from the last tab to the first
- Arrow Left: Moves focus to the previous tab; wraps from the first tab to the last
- Home: Moves focus to the first tab
- End: Moves focus to the last tab
- Tab: Moves focus into and out of the tab list (only one tab in the list is in the tab order at a time)
ARIA
role="tablist" -- identifies the container as a list of tabs
aria-label={label} -- provides an accessible name for the tab list so screen readers announce its purpose
- Child tabs should have
role="tab" and aria-selected attributes (managed by consumer)
- Associated tab panels should have
role="tabpanel" with aria-labelledby pointing to the corresponding tab (managed by consumer)
When to Use
- Use to let users navigate between related sections of content without leaving the page
- Use when content naturally divides into distinct categories that users will access one at a time
- Use to reduce page length by showing only the relevant section
- Use when there are 2-6 sections -- enough to justify tabs, few enough to remain scannable
When Not to Use
- Do not use when users need to compare information across sections -- show all content on one page or use a Table
- Do not use with only one section -- show the content directly without tabs
- Do not use for sequential steps or progress -- use TaskList or a multi-page form with BackLink
- Do not use for navigation between different pages -- use NavigationMenu or BreadcrumbNav instead
Headless
This headless component renders a <div> with role="tablist", aria-label, and built-in arrow key navigation (Left/Right to cycle tabs, Home/End to jump to first/last). The consumer provides tab button children, manages selection state, associates tab panels, and applies all visual styling.
Styles
The consumer provides all CSS styling. The component renders with a .tab-bar class for targeting. No default styles are included — this is a fully headless component.
Testing
- Verify the component renders a
<div> element with class tab-bar
- Verify role="tablist"` -- identifies the container as a list of tabs
- Verify aria-label={label}` -- provides an accessible name for the tab list so screen readers announce its purpose
- Verify Child tabs should have
role="tab" and aria-selected attributes (managed by consumer)
- Verify Associated tab panels should have
role="tabpanel" with aria-labelledby pointing to the corresponding tab (managed by consumer)
- Verify keyboard interactions work correctly
- Verify pass-through attributes are applied
Advice
- Designers: Clearly indicate the active tab with a visual treatment such as an underline, background color, or border. Keep tab labels short and descriptive.
- Developers: Pair each TabBarButton with a corresponding
role="tabpanel" element linked via aria-controls and aria-labelledby. Manage the aria-selected state on child tab buttons from the consumer.
Implementation Notes
- Renders a
<div> element with role="tablist" for ARIA semantics
- Uses two-way
this binding to capture a reference to the tablist element for DOM queries
- Queries all child elements with
[role='tab'] to build the list of navigable tabs
- Implements keyboard navigation via an
onkeydown handler:
- ArrowRight moves focus to the next tab (wraps to first)
- ArrowLeft moves focus to the previous tab (wraps to last)
- Home moves focus to the first tab
- End moves focus to the last tab
Uses reactive state for the internal DOM reference (tablistRef)
Consumer is responsible for managing aria-selected state on tab buttons and displaying corresponding tab panels
Spreads restProps onto the div for consumer customization
Composition
TabBar uses the Bar/BarButton composition pattern. Place TabBarButton components as children inside TabBar. The TabBar provides the role="tablist" container with keyboard navigation, while each TabBarButton provides role="tab" with selection state and panel association.
Related components
tab-bar-button — one tab button in a tab group
References
- WAI-ARIA Tabs Pattern: https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
- WAI-ARIA Authoring Practices - Tabs Example: https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/
Canonical contract
Generated from this component's canonical metadata — the machine-checked source the implementations are held to.
Metadata
- Component: tab-bar
- PascalCase: TabBar
- Description: a group of tabs for switching between content panels
- Status: stable — exercised in composed page flows under e2e and axe, beyond the per-component checks
- HTML tag: <div>
- CSS class: .tab-bar
- Interactive: yes
ARIA
role="tablist" -- identifies the container as a list of tabs aria-label={label} -- provides an accessible name for the tab list so screen readers announce its purpose - Child tabs should have
role="tab" and aria-selected attributes (managed by consumer) - Associated tab panels should have
role="tabpanel" with aria-labelledby pointing to the corresponding tab (managed by consumer)
Keyboard
- Arrow Right: Moves focus to the next tab; wraps from the last tab to the first
- Arrow Left: Moves focus to the previous tab; wraps from the first tab to the last
- Home: Moves focus to the first tab
- End: Moves focus to the last tab
- Tab: Moves focus into and out of the tab list (only one tab in the list is in the tab order at a time)
Props
label: string (required) -- accessible label for the tab list, applied via aria-label children: slot (required) -- tab button elements to render inside the tablist ...restProps: unknown -- additional attributes spread onto the <div> element