Tabs from @prepared911/ui/tabs implements the WAI-ARIA Tabs patternopens in a new tab. Trigger-to-panel association, orientation, roving focus, and keyboard behavior are built in; your job is to name the tab list and to keep focus behavior inside panels sensible.
What the component handles
Role assignment.TabsList → tablist, TabsTrigger → tab with aria-selected and aria-controls, TabsContent → tabpanel with aria-labelledby.
Orientation.aria-orientation on the tab list reflects horizontal or vertical layout and drives arrow-key direction.
Focus flow. Focus moves into the active trigger, then Tab moves into the focused panel.
Activation model. Default automatic activation on arrow keys; activation={TabsActivation.Manual} keeps arrows on focus until Enter or Space.
Scrollable tab lists. With listOverflow={TabsListOverflow.Scroll}, arrow-key roving focus is unchanged. When focus moves to a tab outside the visible scrollport, the list scrolls that tab into view (scrollIntoView). Scroll padding and clip margin reserve space so focus rings stay visible at the edges of the scrollport (WCAG 2.4.11).
Fill and panel scroll. With fill on panel Tabs, the root grows in a flex parent and each TabsContent panel scrolls inside itself.
What you must provide
Tab list label. On TabsList, set label (LocalizedMessage, rendered as aria-label) or aria-labelledby. The panel-mode Tabs root has no role; the accessible name belongs on the tablist, not on a wrapper div.
Descriptive trigger text. Keep triggers concrete ("General", "Privacy"), not generic ("More"). Icon-only triggers need aria-label.
Panel content that doesn't trap. If a panel holds complex interactive content (forms, editors), ensure its own focus management doesn't conflict with the tab pattern.
Testing
Locate tabs only through roles and names:
getByRole("tablist", { name: "…" })
getByRole("tab", { name: "…" })
getByRole("tabpanel", { name: "…" }) (the panel name comes from the tab that labels it)
Do not wrap Tabs, TabsList, or TabsContent in extra elements just to attach data-test-id or to query structure. Do not disable testing-library/no-node-access to reach into a design-system tab; if a part is not findable by role, treat that as a design-system defect.
Keyboard behavior
Key
Action
Tab
Moves focus into the active trigger, then into the panel.
Arrow Left / Right
Moves between triggers in horizontal layouts.
Arrow Up / Down
Moves between triggers in vertical layouts.
Enter / Space
Activates the focused trigger (manual mode selects).
Home / End
Jumps to the first / last trigger.
Semantics and roles
Element
Role
Key attributes
TabsList
tablist
aria-orientation, accessible name
TabsTrigger
tab
aria-selected, aria-controls
TabsContent
tabpanel
aria-labelledby
Provide a tab list label:
Screen reader announcements
Typical sequence when navigating:
Focus enters the trigger list → "Account settings, tab list".
First trigger → "General, tab, selected, 1 of 2".
Arrow right → "Privacy, tab, 2 of 2".
Enter → "Privacy, tab, selected, 2 of 2"; panel content updates.
Focus management
Focus stays on the active trigger after activation; Tab moves into the panel.
When a panel contains interactive elements, Tab reaches them directly from the trigger.
Respect focus restoration when tabs are controlled by external state (e.g., query parameters).
On a scrollable tab list (TabsListOverflow.Scroll), roving focus does not trap inside the scrollport: Home, End, and arrow keys still move among all tabs, and the list scrolls to keep the focused tab visible.
Known caveats
Lazy panels. If panels mount lazily, ensure the panel's content is rendered before screen readers try to read it. A momentarily empty tabpanel can confuse assistive tech.
Destructive actions inside panels. Confirm in a Dialog; never rely on a tab change to "cancel" a destructive flow.