Components / Structure
Tabs
Switches between views of the same thing, such as a document's tree, raw and schema views, while staying on one page. If the options filter or change a value rather than swapping whole panels, use SegmentedControl.
When to useLink to "When to use"
- Two or more panels of related content where only one is visible at a time.
variant="line"(default): page or panel level navigation with an underline indicator.variant="soft": compact switchers inside cards, toolbars and side panels.activation="manual": when showing a panel is expensive (network, large renders), so arrowing past a tab doesn't load it.
Use something else when:
- the choice is a setting or filter: SegmentedControl or RadioGroup
- each option goes to a different page: Links in your navigation
- the sections should be readable together: Stack with headings
UsageLink to "Usage"
import { Tab, TabList, TabPanel, Tabs } from "@arshad-shah/cynosure";
import { code, list } from "@arshad-shah/cynosure/icons";
<Tabs defaultValue="tree" onChange={setView}>
<TabList label="Document views">
<Tab value="tree" icon={list}>Tree</Tab>
<Tab value="raw" icon={code}>Raw</Tab>
<Tab value="schema" disabled>Schema</Tab>
</TabList>
<TabPanel value="tree"><Tree /></TabPanel>
<TabPanel value="raw"><Raw /></TabPanel>
<TabPanel value="schema"><Schema /></TabPanel>
</Tabs>TabList needs a label, which names the group for screen readers. Every Tab needs a unique value. A TabPanel with the same value is optional: tabs that drive routed content outside Tabs simply don't point at a panel.
Many tabsLink to "Many tabs"
When tabs don't fit, the list scrolls sideways (with no visible scrollbar). Edges that hide tabs fade out, and the selected or focused tab is always scrolled clear of the fade. This also works right to left.
For document tabs, scrollable adds chevrons at each end, shown only while there are more tabs that way, and lets a vertical mouse wheel scroll the list sideways. The chevrons are for the pointer: keyboard users move with the arrows, which bring the focused tab into view. trailing puts content after the last tab, outside the tab list, such as a button that opens a new tab; it stays in view when the tabs overflow.
<TabList
label="Open files"
scrollable
trailing={<IconButton icon={plus} label="New query" variant="ghost" size="sm" onClick={newQuery} />}
>
{files.map((f) => <Tab key={f.id} value={f.id} onClose={() => closeFile(f.id)}>{f.name}</Tab>)}
</TabList>Long labelsLink to "Long labels"
A label longer than --cy-tabs-label-max-width (default 20em) is cut short with an ellipsis, and hovering or focusing the tab shows the whole text in a tooltip. Screen readers already hear the whole text as the tab's name, so the tooltip is hidden from them.
Changing tabsLink to "Changing tabs"
- Controlled: pass
valueandonChange. The Tab stop follows selections made from outside. - Closing tabs: when the selected tab is removed, the tab now in its place is selected (or the previous one if it was last), and
onChangereports it.
Document tabsLink to "Document tabs"
For open files or documents, a Tab with onClose gets a close button, and Delete closes the focused tab. A TabList with onReorder lets people drag tabs into a new order, or move the focused one with Ctrl+Shift (Cmd+Shift) and the arrows. You hold the list: remove the tab in onClose and render the order onReorder gives you.
<Tabs value={current} onChange={setCurrent}>
<TabList label="Open files" onReorder={setFiles}>
{files.map((f) => (
<Tab key={f} value={f} onClose={() => closeFile(f)}>
{f}
</Tab>
))}
</TabList>
</Tabs>A middle-click on a closable tab closes it too, as in browsers and editors.
Unsaved changesLink to "Unsaved changes"
dirty marks a document with unsaved changes: a dot sits where the close button is, and turns into the close button while the tab is hovered or focused. The close button's name says so ("Close b.ts, unsaved changes"), and screen readers hear "Unsaved changes" as the tab's description, after its name. Ask before discarding the changes in your onClose.
Commands on a tabLink to "Commands on a tab"
contextMenu gives a tab its own commands, such as Close others or Close to the right, with the items Menu takes; onCommand gets the chosen item's value. Right-click, a long press, Shift+F10 or the context menu key opens it. A command that closes the tab moves focus to the tab then selected, as Delete does.
<Tab
value={f}
onClose={() => closeFile(f)}
contextMenu={[
{ value: "close", label: "Close", shortcut: ["Delete"] },
{ value: "others", label: "Close others" },
{ value: "right", label: "Close to the right", disabled: isLast },
]}
onCommand={(command) => run(command, f)}
>
{f}
</Tab>PropsLink to "Props"
| Prop | What it does | Type | Default |
|---|---|---|---|
value | Selected tab (controlled). | string | |
defaultValue | Initially selected tab (uncontrolled). Defaults to the first enabled tab. | string | the first enabled tab |
onChange | Called with the tab's value when a different tab is selected. | ((value: string) => void) | |
activation | auto: arrow keys select as they move. manual: arrows move focus; Enter or Space selects. | "auto" | "manual" | |
size | Height and text size of the tabs. Default "md". | Size | "md" |
variant | line: underline indicator. soft: tinted pill. | "line" | "soft" | |
labels | Replace the built-in English text: the close button's name (saved and unsaved), the unsaved description, the scroll chevrons' names, the context menu's name and the announcement after a move. | Partial<TabsLabels> | |
className | Added to the root element, for layout. | string | |
style | Layout only. | CSSProperties | |
children | A TabList and its TabPanels. | ReactNode |
Generated from the type definitions in @arshad-shah/cynosure 0.5.0.
Tab: value, icon, disabled, onClose, dirty, contextMenu, onCommand, closeLabel, children. The close button is named labels.close with the tab's text (a label with markup, such as a badge, is named by the text it shows); give closeLabel to name it yourself, such as "Close query 3". TabList: label, onReorder, scrollable, trailing. Tabs also takes labels: close(name), closeUnsaved(name), unsaved, scrollBack, scrollForward, menu(name) and moved(name, position, total). TabPanel: value, keepMounted (keep the content mounted while hidden, preserving its state; by default only the selected panel renders its content).
Behaviour and accessibilityLink to "Behaviour and accessibility"
| Input | Result |
|---|---|
| Tab | Focuses the selected tab (one Tab stop), then the panel |
| Left arrow Right arrow arrows | Previous / next tab, wrapping, skipping disabled ones (flipped in RTL) |
| Home End | First / last tab |
| Enter Space, click, tap | Select (the only way to select with activation="manual") |
| Delete | Close the focused tab (with onClose) |
| Middle-click | Close the tab (with onClose), without the browser's autoscroll |
| Mouse wheel | Scroll a scrollable list sideways |
| Shift + F10, context menu key, right-click | Open the tab's contextMenu |
| Control or Command + Shift + Left / Right | Move the focused tab (with onReorder); announced |
- Roles:
tablist(named bylabel),tabwitharia-selectedandaria-controls, andtabpanellabelled by its tab. - Hidden panels stay in the DOM with
hidden, soaria-controlsalways resolves. - Panels are focusable (
tabIndex=0), so keyboard users can reach panels without interactive content. - Unsaved state is the tab's description (
aria-describedbyto hidden text), not part of its name, so the name stays the document's for voice control and the panel's label. - Forced colors: the selected tab is outlined with the system highlight colour.
TouchLink to "Touch"
Each tab has at least a 24px hit area, and 44px on coarse pointers, without changing its visible size, as do the chevrons of a scrollable list. A long press opens a tab's contextMenu. The list scrolls with a swipe; to drag a reorderable tab, hold it briefly first.
ThemingLink to "Theming"
Tokens: --cy-tabs-dirty (the unsaved dot, default muted ink), --cy-tabs-gap, --cy-tabs-indicator (selected colour, default accent), --cy-tabs-indicator-width, --cy-tabs-label-max-width (where a long label is cut short, default 20em) and --cy-tabs-radius.
createTheme({ components: { tabs: { indicator: "#2563eb", indicatorWidth: "3px" } } });RelatedLink to "Related"
These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.