Skip to content
Cynosurev0.5.0
SearchStorybookGitHubnpmColour mode: System. Switch to Light

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.

Arrows select skipping disabled

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"

tsx
Copy
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.

Overflow scrolls the active tab into view

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.

tsx
Copy
<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>
Scrollable shows chevrons toward more tabs

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.

Long labels truncate with the full text in atooltip

Changing tabsLink to "Changing tabs"

  • Controlled: pass value and onChange. 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 onChange reports it.
Removing the selected tab selects the next one

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.

tsx
Copy
<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>
Reorder with the keyboard

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.

Unsaved tab shows adot that becomes the close button

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.

tsx
Copy
<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>
Right click opens the tabs menu

PropsLink to "Props"

Every prop of Tabs
PropWhat it doesTypeDefault
value
Selected tab (controlled).string
defaultValue
Initially selected tab (uncontrolled). Defaults to the first enabled tab.stringthe 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"

Behaviour and accessibility, table 1
InputResult
TabFocuses the selected tab (one Tab stop), then the panel
Left arrow Right arrow arrowsPrevious / next tab, wrapping, skipping disabled ones (flipped in RTL)
Home EndFirst / last tab
Enter Space, click, tapSelect (the only way to select with activation="manual")
DeleteClose the focused tab (with onClose)
Middle-clickClose the tab (with onClose), without the browser's autoscroll
Mouse wheelScroll a scrollable list sideways
Shift + F10, context menu key, right-clickOpen the tab's contextMenu
Control or Command + Shift + Left / RightMove the focused tab (with onReorder); announced
  • Roles: tablist (named by label), tab with aria-selected and aria-controls, and tabpanel labelled by its tab.
  • Hidden panels stay in the DOM with hidden, so aria-controls always resolves.
  • Panels are focusable (tabIndex=0), so keyboard users can reach panels without interactive content.
  • Unsaved state is the tab's description (aria-describedby to 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.

ts
Copy
createTheme({ components: { tabs: { indicator: "#2563eb", indicatorWidth: "3px" } } });

SegmentedControl · Toolbar · Card

These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.

Getting started
Theming
Icons
Accessibility
Code editors and highlighters
Using Cynosure without React
Build-tool adapters
Components
Accordion
AppShell
AspectRatio
Breadcrumbs
Calendar
Card
Container
Disclosure
Divider
DocumentViewport
Grid
Inline
PageHeader
PanZoomCanvas
ScrollArea
SideNav
SortableList
SplitPane
Stack
StatusBar
Tabs
TitleBar
Toolbar
Tree
VirtualList
ZoomControls
Announcer
Avatar
Badge
CodeBlock
DescriptionList
EmptyState
ErrorBoundary
Field
Heading
IconTile
Image
Kbd
List
LoadingDots
Notice
ProgressBar
Prose
Skeleton
Stat
Steps
Table
Text
Button
Checkbox
Checklist
ColorField
ColorSwatches
Combobox
CompareSlider
CopyButton
DateField
FileDrop
IconButton
KeyValueEditor
Link
NumberField
Pagination
PathField
RadioGroup
SearchField
SegmentedControl
Select
SelectionFrame
SignaturePad
Slider
Switch
TagInput
TextArea
TextField
ThemeToggle
TimeField
ButtonBase
ChoiceBase
DialogBase
FileInputBase
HiddenValue
Icon
InputBase
LinkBase
LiveRegion
TextAreaBase
ChatMessage
Choice
DataGrid
DeviceFrame
HighlightedText
HotkeyField
ListRow
OptionList
OverlayFrame
QRCode
RowActions
Section
SiteFooter
SiteHeader
SkipLink
SortButton
VisuallyHidden
CommandPalette
ContextMenu
Dialog
FloatingToolbar
HoverCard
Menu
Menubar
Popover
ProgressOverlay
Sheet
Toast
Tooltip
Changelog