A row of tabs that switches between views of one thing, one panel showing at a time, such as a stop's departures, its lines and its disruptions in the detail panel.
When to use
Use Tabs to switch between views of one thing, one panel showing at a time, such as a stop's departures, its lines and its disruptions.
| Use instead | When |
|---|---|
| SegmentedControl | The choice changes how one view shows its content, such as departures by mode |
| FilterChip | Filters narrow one view, each turned on and off on its own |
| Accordion | Headed sections stack down the page, each opening its content under its heading |
| LinkButton | The control goes to another screen or page |
Kongens Nytorv
Metro M1, M2 and M3.
Props
| Name | Type | Default | Description |
|---|---|---|---|
value | string | null | — | The selected tab's value, when the caller controls it. null selects no tab. |
defaultValue | string | null | The first enabled tab | The tab selected at first, when Tabs keeps the value itself. Left out, the first enabled tab is selected, and onChange reports it with details.reason 'initial'. |
onChange | (event: Event, value: string | null, details: Tabs.Root.ChangeEventDetails) => void | — | Called when the selected tab changes: by a click, Enter or Space, or an arrow key with activateOnFocus on (details.reason 'none'); and by Tabs itself, at mount when defaultValue is left out ('initial'), or when the selected tab is disabled or removed ('disabled', 'missing'). event is details.event. |
children | ReactNode | — | One TabsList and a TabsPanel for each of its tabs. |
className | string | — | Reaches the root. className is merged after pn-Tabs. |
style | CSSProperties | — | Reaches the root. className is merged after pn-Tabs. |
ref | Ref<HTMLDivElement> | — | Reaches the root. className is merged after pn-Tabs. |
valuestring | null, default —- The selected tab's
value, when the caller controls it.nullselects no tab. defaultValuestring | null, default The first enabled tab- The tab selected at first, when Tabs keeps the value itself. Left out, the first enabled tab is selected, and
onChangereports it withdetails.reason'initial'. onChange(event: Event, value: string | null, details: Tabs.Root.ChangeEventDetails) => void, default —- Called when the selected tab changes: by a click, Enter or Space, or an arrow key with
activateOnFocuson (details.reason'none'); and by Tabs itself, at mount whendefaultValueis left out ('initial'), or when the selected tab is disabled or removed ('disabled','missing').eventisdetails.event. childrenReactNode, default —- One
TabsListand aTabsPanelfor each of its tabs. classNamestring, default —- Reaches the root.
classNameis merged afterpn-Tabs. styleCSSProperties, default —- Reaches the root.
classNameis merged afterpn-Tabs. refRef<HTMLDivElement>, default —- Reaches the root.
classNameis merged afterpn-Tabs.
Use it
import { Tabs } from '@mylder/perron'Ask your agent
get_reference({ library: "base-ui", component: "Tabs" })Keyboard and ARIA
From the Base UI reference for Tabs and the installed TabsRoot, TabsList, TabsTab, TabsPanel, TabsIndicator and the composite root they build on. "From" says who does it.
| Key | Does | From |
|---|---|---|
Tab | Moves focus into the tab list, to the selected tab; from a tab, on to the open panel, which is focusable (tabIndex 0); the tab list is one tab stop | Base UI |
Shift+Tab | Moves focus back: from the panel to the selected tab, and from the list out of it | Base UI |
ArrowRight | Moves focus to the next tab, wrapping from the last to the first while loopFocus is on, without selecting it unless activateOnFocus; the previous tab under an rtl DirectionProvider, which Perron doesn't set up | Base UI |
ArrowLeft | Moves focus to the previous tab, wrapping from the first to the last while loopFocus is on, without selecting it unless activateOnFocus; the next tab under an rtl DirectionProvider | Base UI |
Home, End | Move focus to the first and the last tab | Base UI |
Enter, Space | Select the focused tab and show its panel; nothing on a disabled tab | The browser's click; Base UI for a non-button render |
ArrowUp and ArrowDown do nothing: the list runs horizontally, and the component adds no keys of its own.
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
root | none: a <div> | none | none |
list | tablist, from Base UI | The caller's aria-label or aria-labelledby | none (aria-orientation only for vertical lists) |
tab | tab, from Base UI, on a native <button> | Its text content: the label's children, then the count, as "Disruptions 2" | aria-selected; disabled by aria-disabled |
count | none: text inside the tab | Part of the tab's name | none |
indicator | presentation, from Base UI | none | none |
panel | tabpanel, from Base UI | Its tab, through aria-labelledby, which Base UI sets | none |
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: which tab is selected, which the underline shows, is
aria-selected; a disabled tab's tertiary colour isaria-disabled; the count is text in the tab's name, heard after the label; the underline itself ispresentation. - Every landmark and named region has an accessible name: each panel is named by its tab through Base UI's
aria-labelledby, and the tab list by the caller'saria-labeloraria-labelledby, which every story sets. - Every default label can be overridden: none here; Tabs owns no strings.
- Every shape the type accepts renders, in every variant: a tab's
childrenand a panel's render a string, an element and a list alike;countrenders0and larger numbers alike. - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: a hidden panel is unmounted, or withkeepMountedkepthiddenandinertby Base UI; Tabs keeps no copy for animation. - Native semantics first: native
<button>s fromTabs.Tab, and Base UI'stablist,tabandtabpanelroles witharia-selected,aria-controlsandaria-labelledby, with ARIA from the caller only for the list's name. - Focus moves in reading order and returns on close: Tab goes from the selected tab to its panel, and the arrow keys move through the tabs in document order; Tabs opens and closes nothing, and focus stays on the tab it selects.
- Every target is at least
size.target-minsquare: the smallest target is a tab with a one-word label,size.button(36 px) tall and its label plusspace.300on each side wide. - Loops stop when the system asks for less motion: none here; Tabs has no loop and no transition.
Owned strings
No owned strings: every word comes from the caller, as a tab's children, a panel's content, or the list's aria-label.
Do and don't
For a caller using the component, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | should | Use Tabs to switch between views of one thing, such as a stop's departures, lines and disruptions | Use Tabs to filter or sort one view, such as departures by mode: a segmented control or filter chips change a view, tabs change which one | review |
| DD-2 | must | Name the tab list with aria-label or aria-labelledby, such as the stop's name, so a screen reader tells it from the next one | Leave the list unnamed beside another set of tabs | review |
| DD-3 | should | Keep each label to a word or two, in sentence case: "Departures", "Planned works" | Write a sentence on a tab, or put a count in the label's text: give it as count, so every count has one shape | review |
| DD-4 | must | Give every Tab a TabsPanel with the same value | Leave a tab without a panel: its aria-controls points at nothing, and selecting it shows nothing | review |
| DD-5 | should | Show a tab whose view is empty, and say so in its panel with an EmptyState or a DataStatePanel | Disable a tab because its view has no data: a disabled tab can't say why, and the empty view is what Mylder should tell | review |
| DD-6 | should | Use keepMounted on a panel that holds state the caller would lose, such as a scrolled list | Keep every panel mounted by default: hidden panels still render, and update with the live data | review |
| DD-7 | must | Link to another screen with a LinkButton | Use tabs as navigation between pages: a tab selects a panel on this screen, and a link is what goes elsewhere | review |
| DD-8 | should | Let the tabs size to their labels and wrap on a narrow screen | Set the list to scroll sideways or give the tabs fixed widths: only the map scrolls sideways, and a translated label outgrows a fixed width | review |