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