Tabs

Switches between panels of related content in one place.

StableSince 1.0.0
On this page

Import

import { Tabs } from '@envsemble/envui';

Usage

<Tabs
  aria-label="Project"
  defaultValue="summary"
  items={[
    { value: 'summary', label: 'Summary', content: <Summary /> },
    { value: 'activity', label: 'Activity', content: <Activity /> },
  ]}
/>

Each TabsItem has its value, the panel's content, and its label with an optional icon, or a button of your own, which becomes the tab.

  • disabled dims a tab and reports aria-disabled.
  • A button is a Button, an IconButton or a component that forwards native button props and its ref, never a link. Every tab is a text tab, whatever its button's variant.
  • aria-label names the tab list.
  • Leave the tabs uncontrolled, starting on defaultValue, the first enabled tab by default, or control them with value and onValueChange.

Activation and mounting

<Tabs aria-label="Reports" activation="manual" keepMounted items={reports} />

activation is automatic, the default, which selects a tab as the arrow keys reach it, or manual, where Enter or Space selects, for panels that load when chosen, such as a report from a server. Only the selected panel is mounted; keepMounted keeps the others mounted and hidden, so their state survives switching.

Size, orientation and fill

size is sm, md or lg, the default, whose tabs are as tall as medium buttons.

  • orientation is horizontal, the default, or vertical, a column moved through with ↑ and ↓, whose tabs are as tall as medium controls unless size is sm. The panels stay below the list either way: to put a vertical list beside them, lay the Tabs out as a row through style, as the VerticalList example does.
  • With fill, the selected panel takes the height left below the list. Give the Tabs a height, for example as a flex child.
  • A horizontal list lines its first label up with the content's edge, so the first tab's padding, where its focus ring shows, reaches into the space before it. Leave that room where the Tabs sit flush against an edge that clips.

A layout of your own

renderList places the tab list in a layout of your own, such as beside a heading: it receives the list and returns what to render in its place, with the list in it once; the panels stay below. listClassName and panelClassName add classes to the list and to every panel.

<Tabs
  aria-label="Orders"
  items={items}
  renderList={(list) => (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
      <Text render={<h3 />} variant="title">
        Orders
      </Text>
      {list}
    </div>
  )}
/>

Composition

The tabs, env-tabs, carry data-fill when they fill a region; the rail, env-tabs-list, data-orientation="vertical" in a column; each tab, env-tabs-tab, data-active while selected. The panels sit in env-tabs-panels, each an env-tabs-panel.

Customize

The rail has no fill unless its container color sets one.

Accessibility

You provide

  • An aria-label on every Tabs and every icon-only tab.
  • A heading inside a panel where it starts a section.
  • The selected tab in the URL, where people should link to it.

envUI provides

  • A tablist with tabs and tabpanels, aria-selected, aria-controls and aria-orientation.
  • One tab stop in the rail, and the arrow keys between tabs, in the text direction.
  • aria-disabled on disabled tabs, which the arrow keys reach but don't select.
  • A focusable panel after the rail.

Props

Tabs

Switches between panels of related content in one place: a rail of the buttons you supply, each a tab linked to its panel, with selection from the keyboard and panels that fade as they change. Every tab is a text tab, the selected one in the accent color with an indicator under it.

PropTypeDescription
itemsrequired
readonly TabsItem[]
The tabs in order, each with its value, its label or button, and its panel content.
aria-labelrequired
string
The tab list's accessible name, such as Project views.
value
stringnull
The selected tab's value, for controlled tabs; update it from onValueChange.
defaultValueDefault the first enabled item's value
stringnull
The tab that uncontrolled tabs start on.
onValueChange
(value: string | null) => void
Receives the value of the tab people select, by pressing it or from the keyboard.
activationDefault 'automatic'
'automatic''manual'
automatic selects a tab as the arrow keys move focus to it; manual only moves focus, and Enter or Space selects, for panels that take a moment to show.
orientationDefault 'horizontal'
'horizontal''vertical'
horizontal puts the tabs in a row, moved through with the left and right arrow keys; vertical in a column, with the up and down arrow keys. The panels stay below the list either way.
keepMountedDefault false
boolean
Keep the panels of unselected tabs mounted and hidden, so their state, such as a draft or a scroll position, survives switching; otherwise a panel unmounts once it has faded out.
renderList
(list: ReactElement) => ReactNode
Places the tab list in a layout of your own, such as beside a heading: it receives the list and returns what to render in its place, with the list in it once. The panels stay below.
listClassName
string
A class for the tab list, the rail that holds the tabs.
panelClassName
string
A class for every panel.
fillDefault false
boolean
The selected panel takes the height left below the list, for content that fills a region, such as a canvas; give the tabs a height, for example as a flex child.
sizeDefault 'lg'
'sm''md''lg'
The rail's height, as on every control: sm, md or lg in the current scale. The tabs inside are a little shorter. lg gives tabs as tall as md buttons; sm suits a toolbar or a bar, beside a SegmentedControl or buttons of size sm.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Part names

Each part's class and state attributes are public and versioned, for an application's CSS to style.

env-tabs
The tabs.
env-tabs-indicator
The indicator that moves to the selected tab, hidden until it has measured it.
env-tabs-list
The list of tabs.
env-tabs-panel
One panel.
env-tabs-panels
The panels.
env-tabs-tab
One tab: a text tab, the selected one with an indicator under it.
data-active
Any value, or present on env-tabs-tab
data-ending-style
Any value, or present on env-tabs-panel
data-fill
Present
data-orientation
vertical on env-tabs-list
data-placed
Present on env-tabs-indicator
data-size
lg · md · sm on env-tabs-list
data-starting-style
Any value, or present on env-tabs-panel
data-variant
text on env-tabs-tab

Color properties

Each sets a color of one instance, through style or CSS; its hover, focus and press follow it. They are public and versioned.

--env-tabs-container
The rail behind the tabs, transparent by default.
--env-tabs-content
The labels of the tabs not selected, and the color of their state layers.
--env-tabs-selected-content
The selected tab's label.
--env-tabs-indicator
The indicator under the selected tab.

Changelog

1.0.0

The arrow keys follow the text direction, from EnvUIProvider's dir or the direction at the tabs; only the tab list takes it, so a panel keeps the direction around it. The tab and panel classes are env-tabs-tab and env-tabs-panel (were env-tab and env-tab-panel).

1.0.0-prerelease.6

A tab's label changes color under the pointer in --env-motion-duration-fast, as every color under the pointer does, rather than the medium duration.

Colors under the pointer and while pressed change with --env-motion-ease-enter, rather than easing in slowly, so hover and press show from the first frame.

The new panel fades in with --env-motion-ease-enter from half its opacity, rather than from nothing, so switching tabs never shows an empty panel between the two; under reduced motion it shows at once.

One indicator, env-tabs-indicator, slides and resizes to the selected tab, and the outgoing panel goes at once while the new one fades in, so two panels never show over each other.

A vertical rail's tabs are as tall as a medium control, like other navigation rows; a small rail keeps its smaller tabs.

The labels of the tabs not selected are a step stronger than the muted text, so a disabled tab stands apart from them.

Under the pointer a tab shows no fill and no bar: only its label takes the text color, so the indicator alone marks the selected tab; pressed, it also lays a faint state layer, a step past hover.

1.0.0-prerelease.5

An item takes a label and an icon, as SegmentedControl's do; button is for a control of your own.

1.0.0-prerelease.2

Tabs look like Material's: text tabs on no fill, whatever their buttons' variants, the selected one in the accent's text color with an indicator under it, so they read differently from SegmentedControl. --env-tabs-selected-content and --env-tabs-indicator set those colors, and the rail has no fill unless --env-tabs-container sets one. The API stays.