Tabs
Base UI Tabs for 2–6 peer panels on one page: a caps tab list with a current bar, and one panel per tab.
import {
Tabs,
TabsList,
TabsPanel,
TabsTab,
} from '@fairgarden-private/design/components/Tabs'
Tab labels are caps wayfinding: author them in sentence case and type-label sets the caps. The current tab takes a --ds-stroke-3 bar and weight 700; hover adds an accent underline under the label, told from the bar by its color and position, and a filled current cell lightens to --role-select-hover. Tabs never navigate between pages. In print the list is hidden and every panel prints in order under its label, so panels stay mounted. The root is an inline-size container, so give it a width in shrink-to-fit layouts.
Underline and segmented
Underline and segmented
underline is the default: tabs on a full-width baseline. Up to three tabs share the row as equal cells with wrapping labels; four to six hug their labels and scroll sideways in a Scroll Area, with a visible scrollbar and a › button; from 768 px of container width every row hugs its labels. segmented is a full-bleed bar of equal cells, and filled gives its current cell the selection fill. A disabled tab draws a dotted underline, but prefer removing it.
312 acres of wet meadow and oak savanna, protected since 1998.
Four marked trails, open dawn to dusk. Dogs on leash.
Volunteers pull invasive buckthorn on the first Saturday of each month.
Four to six tabs hug their labels; the row scrolls when it runs out of room.
Land bought outright and held in trust.
Prairie seeding, wetland berms and prescribed burns.
Annual visits to every protected parcel.
Unavailable.
Filled segmented cells belong on paper and white only.
Long labels wrap to two lines instead of truncating.
Guided walks every weekend from April to October.
Unfilled, the bar and weight alone mark the current cell.
Seven days of events.
The whole month at a glance.
import {
Tabs,
TabsList,
TabsPanel,
TabsTab,
} from '@fairgarden-private/design/components/Tabs'
import styles from './variants.module.css'
/**
* Underline tabs (default), a longer underline row that scrolls with a ›
* button, and the segmented bar, filled and bare.
*/
export function TabsVariants() {
return (
<div className={styles.stack}>
<Tabs defaultValue="overview">
<TabsList>
<TabsTab value="overview">Overview</TabsTab>
<TabsTab value="access">Trails and access</TabsTab>
<TabsTab value="stewardship">Stewardship</TabsTab>
</TabsList>
<TabsPanel value="overview">
<p className={styles.copy}>312 acres of wet meadow and oak savanna, protected since 1998.</p>
</TabsPanel>
<TabsPanel value="access">
<p className={styles.copy}>Four marked trails, open dawn to dusk. Dogs on leash.</p>
</TabsPanel>
<TabsPanel value="stewardship">
<p className={styles.copy}>Volunteers pull invasive buckthorn on the first Saturday of each month.</p>
</TabsPanel>
</Tabs>
<Tabs defaultValue="easements">
<TabsList>
<TabsTab value="easements">Easements</TabsTab>
<TabsTab value="acquisitions">Acquisitions</TabsTab>
<TabsTab value="restoration">Restoration</TabsTab>
<TabsTab value="monitoring">Monitoring</TabsTab>
<TabsTab value="archive" disabled>
Archive
</TabsTab>
</TabsList>
<TabsPanel value="easements">
<p className={styles.copy}>Four to six tabs hug their labels; the row scrolls when it runs out of room.</p>
</TabsPanel>
<TabsPanel value="acquisitions">
<p className={styles.copy}>Land bought outright and held in trust.</p>
</TabsPanel>
<TabsPanel value="restoration">
<p className={styles.copy}>Prairie seeding, wetland berms and prescribed burns.</p>
</TabsPanel>
<TabsPanel value="monitoring">
<p className={styles.copy}>Annual visits to every protected parcel.</p>
</TabsPanel>
<TabsPanel value="archive">
<p className={styles.copy}>Unavailable.</p>
</TabsPanel>
</Tabs>
<Tabs variant="segmented" filled defaultValue="land">
<TabsList>
<TabsTab value="land">Protect land</TabsTab>
<TabsTab value="water">Protect water and wetland habitat</TabsTab>
<TabsTab value="people">Connect people</TabsTab>
</TabsList>
<TabsPanel value="land">
<p className={styles.copy}>Filled segmented cells belong on paper and white only.</p>
</TabsPanel>
<TabsPanel value="water">
<p className={styles.copy}>Long labels wrap to two lines instead of truncating.</p>
</TabsPanel>
<TabsPanel value="people">
<p className={styles.copy}>Guided walks every weekend from April to October.</p>
</TabsPanel>
</Tabs>
<Tabs variant="segmented" defaultValue="day">
<TabsList>
<TabsTab value="day">Day</TabsTab>
<TabsTab value="week">Week</TabsTab>
<TabsTab value="month">Month</TabsTab>
</TabsList>
<TabsPanel value="day">
<p className={styles.copy}>Unfilled, the bar and weight alone mark the current cell.</p>
</TabsPanel>
<TabsPanel value="week">
<p className={styles.copy}>Seven days of events.</p>
</TabsPanel>
<TabsPanel value="month">
<p className={styles.copy}>The whole month at a glance.</p>
</TabsPanel>
</Tabs>
</div>
)
}
Primary and secondary
Primary and secondary
primary recolors labels, bar and baseline. secondary drives only the filled segmented cell; keep it equal to the brand strip's scale so strip and cell join.
Primary plum: labels, bar and baseline.
Sedges, milkweed and bur oak.
Monarchs and native bees.
Secondary indigo: the filled cell and its edge.
Workdays run year round.
Every gift stays local.
import {
Tabs,
TabsList,
TabsPanel,
TabsTab,
} from '@fairgarden-private/design/components/Tabs'
import styles from './color.module.css'
/** `primary` drives labels, bar and baseline; `secondary` only the filled segmented cell. */
export function TabsColor() {
return (
<div className={styles.stack}>
<Tabs primary="plum" defaultValue="birds">
<TabsList>
<TabsTab value="birds">Birds</TabsTab>
<TabsTab value="plants">Plants</TabsTab>
<TabsTab value="insects">Insects</TabsTab>
</TabsList>
<TabsPanel value="birds">
<p className={styles.copy}>Primary plum: labels, bar and baseline.</p>
</TabsPanel>
<TabsPanel value="plants">
<p className={styles.copy}>Sedges, milkweed and bur oak.</p>
</TabsPanel>
<TabsPanel value="insects">
<p className={styles.copy}>Monarchs and native bees.</p>
</TabsPanel>
</Tabs>
<Tabs variant="segmented" filled secondary="indigo" defaultValue="visit">
<TabsList>
<TabsTab value="visit">Visit</TabsTab>
<TabsTab value="volunteer">Volunteer</TabsTab>
<TabsTab value="give">Give</TabsTab>
</TabsList>
<TabsPanel value="visit">
<p className={styles.copy}>Secondary indigo: the filled cell and its edge.</p>
</TabsPanel>
<TabsPanel value="volunteer">
<p className={styles.copy}>Workdays run year round.</p>
</TabsPanel>
<TabsPanel value="give">
<p className={styles.copy}>Every gift stays local.</p>
</TabsPanel>
</Tabs>
</div>
)
}
On grounds
On paper and forest
Pass filled on paper and white only: deep and saturated grounds take no cell fill, and the bar and weight carry the current tab.
paper
Trailheads and parking.
Every preserve, A to Z.
Filled on a light ground.
Upcoming.
forest
Trailheads and parking.
Every preserve, A to Z.
No cell fill on a deep ground.
Upcoming.
import {
Tabs,
TabsList,
TabsPanel,
TabsTab,
} from '@fairgarden-private/design/components/Tabs'
import { PresetGround } from '@/components/PresetGround'
import styles from './grounds.module.css'
const presets = ['paper', 'forest'] as const
/** The segmented cell fills on paper only; on forest the bar and weight carry "current". */
export function TabsGrounds() {
return (
<div className={styles.row}>
{presets.map((preset) => (
<PresetGround key={preset} preset={preset} className={styles.face}>
<p className={styles.name}>{preset}</p>
<Tabs defaultValue="map">
<TabsList>
<TabsTab value="map">Map</TabsTab>
<TabsTab value="list">List</TabsTab>
</TabsList>
<TabsPanel value="map">
<p className={styles.copy}>Trailheads and parking.</p>
</TabsPanel>
<TabsPanel value="list">
<p className={styles.copy}>Every preserve, A to Z.</p>
</TabsPanel>
</Tabs>
{preset === 'paper' ? (
<Tabs variant="segmented" filled defaultValue="now">
<TabsList>
<TabsTab value="now">Now</TabsTab>
<TabsTab value="later">Later</TabsTab>
</TabsList>
<TabsPanel value="now">
<p className={styles.copy}>Filled on a light ground.</p>
</TabsPanel>
<TabsPanel value="later">
<p className={styles.copy}>Upcoming.</p>
</TabsPanel>
</Tabs>
) : (
<Tabs variant="segmented" defaultValue="now">
<TabsList>
<TabsTab value="now">Now</TabsTab>
<TabsTab value="later">Later</TabsTab>
</TabsList>
<TabsPanel value="now">
<p className={styles.copy}>No cell fill on a deep ground.</p>
</TabsPanel>
<TabsPanel value="later">
<p className={styles.copy}>Upcoming.</p>
</TabsPanel>
</Tabs>
)}
</PresetGround>
))}
</div>
)
}
API Reference
Tabs
Tabs for 2–6 peer panels on one page (§9.5): TabsList holding TabsTabs,
then one TabsPanel per tab. Labels are caps wayfinding (type-label),
authored in sentence case [D165]. Never use tabs for page-to-page
navigation or sequential steps. In print the list hides and every panel
prints in order under its label. The root is an inline-size container, so
give it a width in shrink-to-fit contexts.
| Prop | Type | Description |
|---|---|---|
filled | | Only with |
primary | | Primary Radix scale: labels, bar, baseline and focus ring. Never defaulted; omitted, it inherits the scope [D133]. |
secondary | | Secondary Radix scale: only the segmented current cell under |
variant | |
|
TabsList and TabsTab
The tab list and its current-tab indicator. When 4–6 tabs overflow their
row, the row scrolls sideways in a wide Scroll Area (the last tab
clipped, a visible scrollbar, hard overflow edges) and a --size-px-7 ›
button at its end scrolls it on. More than 6 tabs belong in navigation
or a Select.
| Prop | Type | Description |
|---|---|---|
overflowLabel | | Accessible name of the overflow › button that scrolls the row. Default “More tabs”. |
One tab: an optional icon and a caps type-label label, at least
--ds-size-hit tall. Inactive labels are --role-muted at weight 600,
the current label --primary12 at 700 with the --ds-stroke-3 bar.
Prefer removing a tab to disabling it.
| Prop | Type | Description |
|---|---|---|
icon | | An optional leading icon, Material Symbols Rounded at the inline tier, FILL 0. Never icon-only. |
TabsPanel
One panel, --size-px-5 below the list, with no border. Panels stay in
the DOM while hidden (keepMounted, default true) so print can show
every panel in order, each under its label in type-subhead.
| Prop | Type | Description |
|---|---|---|
label | | The heading this panel prints under. Omitted, it takes the matching tab’s label once the page has hydrated. |
Additional types
TabsListProps
Props for TabsList: Base UI Tabs List props plus the overflow button’s label.
type TabsListProps = TabsListProps & { overflowLabel?: string }TabsPanelProps
Props for TabsPanel: Base UI Tab Panel props plus the printed label.
type TabsPanelProps = TabsPanelProps & { label?: React.ReactNode }TabsProps
Props for Tabs: Base UI Tabs Root props (without orientation) plus the variant and color axes.
type TabsProps = (
{ variant?: 'underline'; filled?: undefined } | { variant: 'segmented'; filled?: boolean }
) & {
/**
* Primary Radix scale: labels, bar, baseline and focus ring. Never
* defaulted; omitted, it inherits the scope [D133].
*/
primary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
/**
* Secondary Radix scale: only the segmented current cell under `filled`.
* Keep it equal to the brand strip's scale so strip and cell join.
*/
secondary?:
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
}TabsTabProps
Props for TabsTab: Base UI Tab props plus an optional leading icon.
type TabsTabProps = TabsTabProps & {
icon?:
| 'menu'
| 'search'
| 'circle'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'chevron_right'
| 'chevron_left'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail';
}Specification: DESIGN-SYSTEM.md §9.5 (tabs) and §5.10.2 (reflow).