Skip to the content

Kongens Nytorv

Metro M1, M2 and M3.

Props

NameTypeDefaultDescription
valuestring | null—The selected tab's value, when the caller controls it. null selects no tab.
defaultValuestring | nullThe first enabled tabThe 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.
childrenReactNode—One TabsList and a TabsPanel for each of its tabs.
classNamestring—Reaches the root. className is merged after pn-Tabs.
styleCSSProperties—Reaches the root. className is merged after pn-Tabs.
refRef<HTMLDivElement>—Reaches the root. className is merged after pn-Tabs.
valuestring | null, default —
The selected tab's value, when the caller controls it. null selects 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 onChange reports it with details.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 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.
childrenReactNode, default —
One TabsList and a TabsPanel for each of its tabs.
classNamestring, default —
Reaches the root. className is merged after pn-Tabs.
styleCSSProperties, default —
Reaches the root. className is merged after pn-Tabs.
refRef<HTMLDivElement>, default —
Reaches the root. className is merged after pn-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.

KeyDoesFrom
TabMoves 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 stopBase UI
Shift+TabMoves focus back: from the panel to the selected tab, and from the list out of itBase UI
ArrowRightMoves 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 upBase UI
ArrowLeftMoves 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 DirectionProviderBase UI
Home, EndMove focus to the first and the last tabBase UI
Enter, SpaceSelect the focused tab and show its panel; nothing on a disabled tabThe 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.

ElementRoleAccessible name fromStates announced
rootnone: a <div>nonenone
listtablist, from Base UIThe caller's aria-label or aria-labelledbynone (aria-orientation only for vertical lists)
tabtab, 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
countnone: text inside the tabPart of the tab's namenone
indicatorpresentation, from Base UInonenone
paneltabpanel, from Base UIIts tab, through aria-labelledby, which Base UI setsnone

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 is aria-disabled; the count is text in the tab's name, heard after the label; the underline itself is presentation.
  • 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's aria-label or aria-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 children and a panel's render a string, an element and a list alike; count renders 0 and larger numbers alike.
  • A hidden copy kept for animation is aria-hidden, and a collapsed region is inert: a hidden panel is unmounted, or with keepMounted kept hidden and inert by Base UI; Tabs keeps no copy for animation.
  • Native semantics first: native <button>s from Tabs.Tab, and Base UI's tablist, tab and tabpanel roles with aria-selected, aria-controls and aria-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-min square: the smallest target is a tab with a one-word label, size.button (36 px) tall and its label plus space.300 on 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.

RuleLevelDoDon'tChecked by
DD-1shouldUse Tabs to switch between views of one thing, such as a stop's departures, lines and disruptionsUse Tabs to filter or sort one view, such as departures by mode: a segmented control or filter chips change a view, tabs change which onereview
DD-2mustName the tab list with aria-label or aria-labelledby, such as the stop's name, so a screen reader tells it from the next oneLeave the list unnamed beside another set of tabsreview
DD-3shouldKeep 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 shapereview
DD-4mustGive every Tab a TabsPanel with the same valueLeave a tab without a panel: its aria-controls points at nothing, and selecting it shows nothingreview
DD-5shouldShow a tab whose view is empty, and say so in its panel with an EmptyState or a DataStatePanelDisable a tab because its view has no data: a disabled tab can't say why, and the empty view is what Mylder should tellreview
DD-6shouldUse keepMounted on a panel that holds state the caller would lose, such as a scrolled listKeep every panel mounted by default: hidden panels still render, and update with the live datareview
DD-7mustLink to another screen with a LinkButtonUse tabs as navigation between pages: a tab selects a panel on this screen, and a link is what goes elsewherereview
DD-8shouldLet the tabs size to their labels and wrap on a narrow screenSet the list to scroll sideways or give the tabs fixed widths: only the map scrolls sideways, and a translated label outgrows a fixed widthreview