Toolbar
A compact row of tools acting on one object or region: a list's count and actions, formatting controls, a context bar or the controls under a figure.
import {
Toolbar,
ToolbarButton,
ToolbarCount,
ToolbarGroup,
ToolbarMore,
ToolbarSeparator,
} from '@fairgarden-private/design/components/Toolbar'
A Base UI Toolbar: one Tab stop enters it, arrow keys move within it, and it remembers the last focused item. ToolbarButton is a text Button by default, so its hover, press and focus come from the Button; disabled buttons stay focusable so their names can be read. Add a Toggle or a Menu trigger through ToolbarItem's render, and a Link with ToolbarLink. Groups sit --size-px-1 apart inside and divide with short vertical rules. The row takes the band's ground and has no face of its own. Avoid saturated fields, and never float a toolbar or stack two sticky bars.
List and figure toolbars
List and figure toolbars
list (the default) puts the mono count first and its actions at the end. figure spreads its groups under the figure. A toolbar reflows on its own width and never scrolls or wraps: below 768 px a text Button with an icon shows only its glyph, its label kept as the accessible name and shown in a tooltip, and lowPriority items leave the row for ToolbarMore, whose menu lists them again. From 768 px labels and low-priority items return and "More" goes. The root is an inline-size container named toolbar. Drag a frame's corner to watch the reflow.
list
figure
Zoom 100% · No action yet.
'use client'
import * as React from 'react'
import {
Menu,
MenuItem,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuTrigger,
} from '@fairgarden-private/design/components/Menu'
import {
Toolbar,
ToolbarBottomRule,
ToolbarButton,
ToolbarCount,
ToolbarGroup,
ToolbarItem,
ToolbarMore,
ToolbarSeparator,
} from '@fairgarden-private/design/components/Toolbar'
import styles from './kinds.module.css'
const sorts = {
recent: 'Recently Accessed',
name: 'Name',
size: 'Size',
} as const
type Sort = keyof typeof sorts
export function ToolbarKinds() {
const [sort, setSort] = React.useState<Sort>('recent')
const [zoom, setZoom] = React.useState(100)
const [figure, setFigure] = React.useState(3)
const [last, setLast] = React.useState('No action yet.')
const step = (by: number) => () => setFigure((value) => ((value - 1 + by + 12) % 12) + 1)
return (
<div className={styles.stack}>
<p className={styles.name}>list</p>
<div className={styles.frame}>
<Toolbar
aria-label="Survey plots"
caption={`54 items · Sorted by ${sorts[sort].toLowerCase()}`}
>
<ToolbarCount>Items (54)</ToolbarCount>
<ToolbarButton icon="add" onClick={() => setLast('Added a plot.')}>
Add Plot
</ToolbarButton>
<ToolbarButton icon="download" lowPriority onClick={() => setLast('Exported the list.')}>
Export List
</ToolbarButton>
<Menu>
<ToolbarItem
render={
<MenuTrigger variant="text" size="sm" icon="expand_more" iconPosition="end">
{sorts[sort]}
</MenuTrigger>
}
/>
<MenuPopup align="end">
<MenuRadioGroup value={sort} onValueChange={(value: Sort) => setSort(value)}>
{(Object.keys(sorts) as Sort[]).map((value) => (
<MenuRadioItem key={value} value={value}>
{sorts[value]}
</MenuRadioItem>
))}
</MenuRadioGroup>
</MenuPopup>
</Menu>
<ToolbarMore>
<MenuItem icon="download" onClick={() => setLast('Exported the list.')}>
Export list
</MenuItem>
</ToolbarMore>
<ToolbarBottomRule variant="hairline" />
</Toolbar>
</div>
<p className={styles.name}>figure</p>
<div className={styles.frame}>
<Toolbar kind="figure" aria-label="Figure controls">
<ToolbarGroup>
<ToolbarButton
iconOnly
icon="zoom_out"
onClick={() => setZoom((value) => Math.max(50, value - 25))}
>
Zoom Out
</ToolbarButton>
<ToolbarButton
iconOnly
icon="zoom_in"
onClick={() => setZoom((value) => Math.min(200, value + 25))}
>
Zoom In
</ToolbarButton>
<ToolbarButton iconOnly icon="recenter" onClick={() => setZoom(100)}>
Recenter
</ToolbarButton>
<ToolbarSeparator />
<ToolbarButton icon="download" onClick={() => setLast('Downloaded the figure.')}>
Download Figure
</ToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<ToolbarButton iconOnly icon="chevron_left" onClick={step(-1)}>
Previous Figure
</ToolbarButton>
<ToolbarCount>{figure} of 12</ToolbarCount>
<ToolbarButton iconOnly icon="chevron_right" onClick={step(1)}>
Next Figure
</ToolbarButton>
</ToolbarGroup>
</Toolbar>
</div>
<p className={styles.status} aria-live="polite">
Zoom {zoom}% · {last}
</p>
</div>
)
}
format is a start-aligned row of icon toggles and buttons in groups. context is the docked bar: pass docked for the top edge's one sticky bar, opaque on the band's ground with a rule on the content side; swap it with the header, never stack the two.
On grounds
On page grounds and a deep field
Items keep their own roles: labels in the text ink, glyphs in the glyph role, separators and the bottom rule in the rule role.
paper
tide
forest
'use client'
import * as React from 'react'
import { Ground } from '@fairgarden-private/design/components/Ground'
import {
Toolbar,
ToolbarBottomRule,
ToolbarButton,
ToolbarCount,
ToolbarGroup,
ToolbarSeparator,
} from '@fairgarden-private/design/components/Toolbar'
import {
isFieldPreset,
isPageGroundPreset,
presets,
type GroundPreset,
} from '@fairgarden-private/design/utils/scope'
import styles from './grounds.module.css'
/** The first preset that passes `test`, read from the preset table, never named here. */
function firstPreset(test: (preset: GroundPreset) => boolean): GroundPreset | undefined {
return (Object.keys(presets) as GroundPreset[]).find(test)
}
/** A sample surface: a field preset as an inset field, a page ground as a face. */
function Sample({
preset,
className,
children,
}: {
preset: GroundPreset
className: string
children: React.ReactNode
}) {
if (isFieldPreset(preset)) {
return (
<Ground kind="field" preset={preset} className={className}>
{children}
</Ground>
)
}
if (isPageGroundPreset(preset)) {
return (
<Ground kind="face" preset={preset} className={className}>
{children}
</Ground>
)
}
return null
}
/** A light page ground, a pastel and a deep field; toolbars avoid saturated fields. */
const grounds = [
firstPreset((preset) => presets[preset].tone === 'light-base'),
firstPreset((preset) => presets[preset].tone === 'tinted'),
firstPreset((preset) => isFieldPreset(preset) && presets[preset].mode === 'always-dark'),
].filter((preset): preset is GroundPreset => preset != null)
export function ToolbarGrounds() {
return (
<div className={styles.stack}>
{grounds.map((preset) => (
<Sample key={preset} preset={preset} className={styles.face}>
<p className={styles.name}>{preset}</p>
<Toolbar aria-label={`Sightings on ${preset}`}>
<ToolbarCount>Sightings (12)</ToolbarCount>
<ToolbarGroup>
<ToolbarButton icon="add">Log Sighting</ToolbarButton>
<ToolbarSeparator />
<ToolbarButton iconOnly icon="download">
Download
</ToolbarButton>
</ToolbarGroup>
<ToolbarBottomRule />
</Toolbar>
</Sample>
))}
</div>
)
}
In print the toolbar is hidden. Pass caption to print the state it carries as one line in its place, such as "54 items · Sorted by name".
API Reference
Toolbar
A Base UI Toolbar: one Tab stop enters it and arrow keys move within it.
Compose ToolbarGroup, ToolbarSeparator, ToolbarButton, ToolbarLink,
ToolbarCount and ToolbarMore; a Toggle or a Menu trigger joins through
Base UI’s render, e.g. <ToolbarItem render={<Toggle … />} />. The row
takes the band’s ground and has no face of its own. Avoid saturated
grounds; on a photo, use media buttons.
| Prop | Type | Description |
|---|---|---|
caption | | The state the toolbar carries, printed as one |
docked | | |
kind | | |
primary | | |
secondary | |
ToolbarButton
A Button (§9.2) in the toolbar’s roving focus. Defaults: variant="text",
size sm (md when iconOnly). A text Button with an icon is
icon-only below 768 px of the toolbar’s width, its label kept as the
accessible name and shown in a Tooltip; from 768 px the label returns.
An iconOnly Button always carries the Tooltip. Disabled buttons stay
focusable so their name can be read.
| Prop | Type | Description |
|---|---|---|
butted | | Butts the button against an adjacent field on its |
icon | | One optional functional glyph (§6.10), inline tier, FILL 0. |
iconOnly | |
|
iconPosition | | Which side of the label the glyph sits on (a |
lowPriority | | Moves the item into ToolbarMore below 768 px of the toolbar’s width;
list it there too, as a MenuItem. Default |
onMedia | | Only with |
onPress | | Called on click, after |
primary | | Primary Radix scale, from the primary roster: the outline edge, labels and focus ring. Never defaulted; omitted, it inherits the scope [D133]. |
secondary | | Secondary Radix scale: the |
size | | Fixed height: |
variant | |
|
children | | The label: verb plus object, authored in title case [D160]. |
ToolbarGroup and ToolbarSeparator
Groups related items, --size-px-1 apart (Base UI Toolbar.Group). disabled disables the whole group.
A vertical --border-size-1 rule in --role-rule, --size-px-4 tall
with --size-px-2 each side, announced to assistive technology. It takes
the axis opposite the toolbar’s and hides beside a pressed segment.
ToolbarItem and ToolbarLink
A roving-focus slot for a control that is not a ToolbarButton: a Toggle
(§9.4) or a Menu trigger (§9.7), passed as render. Disabled items stay
focusable by default.
A Link (§9.3) in the toolbar’s roving focus (Base UI Toolbar.Link).
Default kind="nav": no rest underline, Link’s hover and ring.
| Prop | Type | Description |
|---|---|---|
external | | Adds the arrow-open mark and “(external site)” for assistive technology.
Default |
index | | Adds the screen-only visited ✓ used in long indexes, such as reference
and archive lists. Default |
kind | | Which build: |
list | | With |
muted | | Rests in |
primary | | Primary Radix scale: the link text and focus ring. Never defaulted; omitted, it inherits the scope [D133]. |
secondary | | Secondary Radix scale: the accent underline. Never defaulted; omitted, it inherits the scope. |
className | | Extra class names, added after the module’s own. |
ToolbarCount, ToolbarMore and ToolbarBottomRule
The list’s count in mono type-data, --primary12, following LTA’s parentheses: “Items (54)".
The “…” overflow: an icon-only more_horiz text Button named “More”
that opens a Menu. It shows only below 768 px of the toolbar’s width,
while the lowPriority items are hidden, so list those items here.
| Prop | Type | Description |
|---|---|---|
label | | The trigger’s accessible name and tooltip. Default “More”. |
primary | | Primary Radix scale inside the popup’s |
secondary | | Secondary Radix scale inside the popup. It drives nothing but a destructive item’s fallback. |
container | | The element the portal renders into. Default: |
align | | Alignment to the trigger. Default |
alignOffset | | Offset along the alignment axis in px. |
side | | Side of the trigger. Default |
sideOffset | | Distance from the trigger in px. Default 8 ( |
collisionPadding | | Clearance from the viewport edge in px before the popup shifts or flips. Default 16. |
children | | MenuItems mirroring the toolbar’s |
keepMounted | | Keeps the portal mounted while closed. |
The optional --border-size-1 rule along the toolbar’s bottom edge; place it last. Decorative.
| Prop | Type | Description |
|---|---|---|
variant | |
|
Additional types
toolbarBottomRule
The optional bottom rule (§9.11): rule governs a list or table; hairline where whitespace also separates.
type toolbarBottomRule = toolbarBottomRuleToolbarBottomRuleProps
Props for ToolbarBottomRule: span props plus the rule’s role.
type ToolbarBottomRuleProps = Omit<
React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>,
'children'
> &
VariantProps<__type> & { variant?: 'rule' | 'hairline' | null }ToolbarButtonProps
ToolbarCountProps
Props for ToolbarCount: span props.
type ToolbarCountProps = React.ComponentPropsWithRef<'span'>ToolbarGroupProps
Props for ToolbarGroup: Base UI Toolbar.Group props.
type ToolbarGroupProps = ToolbarGroup.ToolbarGroupPropsToolbarItemProps
Props for ToolbarItem: Base UI Toolbar.Button props; pass the control as render.
type ToolbarItemProps = ToolbarButton.ToolbarButtonPropsToolbarLinkProps
Props for ToolbarLink: Link props.
type ToolbarLinkProps = {
/**
* Which build: `inline` (default) underlines in running text; `standalone`
* is the caps module link with a trailing ›; `nav` has no rest underline
* and marks the current page; `title` stretches over a card or list item;
* `noteref` and `backref` are the note call and return (§9.3).
*/
kind?: 'nav' | 'title' | 'inline' | 'standalone' | 'backref' | 'noteref' | null;
/**
* Adds the screen-only visited ✓ used in long indexes, such as reference
* and archive lists. Default `false` [D174, D175].
*/
index?: boolean | null;
/**
* Adds the arrow-open mark and "(external site)" for assistive technology.
* Default `false`.
*/
external?: boolean | null;
/**
* With `kind="nav"`: a list link (nav-panel, footer, drawer and breadcrumb
* lists), whose hover is `--role-link-hover` color only, plus the
* `--ds-stroke-1-5` `--role-accent` underline where that ink is
* `--primary12`. Without it, `nav` is bare navigation text (bar and
* utility items, page numbers, toolbar links), whose hover is the
* `--border-size-2` `--role-accent` underline (§9.3) [D181]. Default
* `false`.
*/
list?: boolean | null;
/**
* Rests in `--role-muted` instead of `--primary12`, as a breadcrumb's
* ancestors. Hover takes `--role-link-hover` only (the underline is added
* where that ink is `--primary12`); with `kind="nav"` it replaces the bar
* item's bare-text underline (§9.3, §9.8) [D181]. Default `false`.
*/
muted?: boolean | null;
/**
* Primary Radix scale: the link text and focus ring. Never defaulted;
* omitted, it inherits the scope [D133].
*/
primary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
/**
* Secondary Radix scale: the accent underline. Never defaulted; omitted,
* it inherits the scope.
*/
secondary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
/** Extra class names, added after the module's own. */
className?: string;
}ToolbarMoreProps
Props for ToolbarMore: the overflow menu’s items and popup options.
type ToolbarMoreProps = {
/**
* Primary Radix scale inside the popup's `white` scope. Omitted, the
* white preset's default: the popup never takes the trigger's scales [D133].
*/
primary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
/** Secondary Radix scale inside the popup. It drives nothing but a destructive item's fallback. */
secondary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
/** Side of the trigger. Default `bottom` for a menu, the end side for a submenu. */
side?: Side;
/** Alignment to the trigger. Default `start`. */
align?: Align;
/** Distance from the trigger in px. Default 8 (`--size-px-2`). */
sideOffset?: number | OffsetFunction;
/** Offset along the alignment axis in px. */
alignOffset?: number | OffsetFunction;
/** Clearance from the viewport edge in px before the popup shifts or flips. Default 16. */
collisionPadding?: Padding;
/** The element the portal renders into. Default: `document.body`. */
container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null;
/** Keeps the portal mounted while closed. */
keepMounted?: boolean;
/** MenuItems mirroring the toolbar's `lowPriority` items, lowest priority last. */
children?: React.ReactNode;
/** The trigger's accessible name and tooltip. Default "More". */
label?: string;
}ToolbarProps
Props for Toolbar: Base UI Toolbar.Root props plus the kind, docked and color axes.
type ToolbarProps = ToolbarRootProps &
ToolbarVariants & {
kind?: 'list' | 'format' | 'context' | 'figure' | null;
docked?: boolean | null;
primary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
secondary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
caption?: React.ReactNode;
}ToolbarSeparatorProps
Props for ToolbarSeparator: Base UI Toolbar.Separator props.
type ToolbarSeparatorProps = ToolbarSeparator.ToolbarSeparatorPropsToolbarVariants
type ToolbarVariants = {
primary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
secondary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
kind?: 'list' | 'format' | 'context' | 'figure' | null;
docked?: boolean | null;
}Specification: DESIGN-SYSTEM.md §9.11 (toolbar), §9.12 (separators) and §5.10.2 (reflow).