Toolbar
A container for a set of controls that belong together — formatting buttons, an
editor's tools, a row of page actions. Renders a <div role="toolbar">.
The point of the component is the keyboard contract, not the row. Tab moves
past the whole bar in one press and the arrow keys move between its controls,
which is what stops a nine-button formatting bar from costing nine presses to
skip. A <div> with display: flex gets you the same picture and none of that.
Import
import { Toolbar } from "@forte-ui/react";<Toolbar.Root aria-label="Formatting">
<Toolbar.Group>
<Toolbar.Button iconOnly aria-label="Undo">…</Toolbar.Button>
<Toolbar.Button iconOnly aria-label="Redo">…</Toolbar.Button>
</Toolbar.Group>
<Toolbar.Separator />
<Toolbar.Input aria-label="Search" placeholder="Find…" />
<Toolbar.Link href="/history">History</Toolbar.Link>
<Toolbar.Button variant="solid" tone="primary">Publish</Toolbar.Button>
</Toolbar.Root>Examples
Groups and separators
A Toolbar.Group is a visual and semantic cluster, not a keyboard one: the
arrow keys still run the length of the whole bar. What changes is the gap —
tighter inside a group than between them — so the eye groups the items before it
reads them. Reach for a Toolbar.Separator between groups that answer different
questions, and let the group gap carry the rest.
There is no prop for pushing a cluster to the far end, because there is nothing
for one to do that a margin does not: the bar is a flex container, so
className="ms-auto" or style={{ marginInlineStart: "auto" }} on the group
does it.
Variants
variant decides how much chrome the bar carries, and says nothing about
the controls inside: a plain bar can still hold solid buttons, and a panel
bar quiet ones.
plain also drops the padding to zero, and that is the point of it. With no
surface to sit on, the padding would push the first control away from whatever
the bar is aligned with — a heading, a text column, the editor below it — and a
row of buttons that does not line up with the thing it acts on reads as a
mistake.
Sizes
size on the root is the default size of every Toolbar.Button,
Toolbar.Input and Toolbar.Link inside it, so a bar stays one height without
repeating the prop per control. An item's own size still wins.
This is React context rather than CSS inheritance, and it has to be. The size
knobs are declared on each control's own root rule — the placement that lets you
re-skin a single control, because an element's own declaration beats an
inherited one — so a value set on the bar would be inherited and then
immediately overwritten. The data-size attribute the rules key off is resolved
in JavaScript and written onto each item.
Orientation
orientation="vertical" stacks the items and moves the arrow keys onto the
block axis. Separators flip with it: Base UI gives a rule the orientation
perpendicular to the bar's, so a vertical toolbar gets horizontal rules
without being told.
A ToggleGroup inside it needs telling too. Base UI's ToggleGroup skips its
own roving focus in a toolbar, but orientation still drives its layout —
left at the default it lays its toggles out in a row inside your column.
Menus, selects and toggles
Anything can join the bar's arrow-key order, and there are two directions to compose in. Which one to reach for depends on whether the other component has a look of its own to defend.
Toolbar.Button render={<Select.Trigger />} is the direction for a component
that must keep its own appearance — a select trigger has to read as a select.
The default Button styling steps aside when render is present, so the two
never fight over the cascade, and only the toolbar membership is added.
Menu.Trigger render={<Toolbar.Button />} is the direction for a trigger with
no look to defend: Menu.Trigger, Dialog.Trigger, Popover.Trigger and
Drawer.Trigger all step aside for a render of their own, so this hands them
the bar's quiet styling. The toolbar button reads the toolbar's context from
where it sits, so disabled on the root still reaches it either way.
Text inputs
Toolbar.Input is a text field that is part of the roving focus. The arrow keys
do the right thing without configuration: while the caret has somewhere left to
go inside the field, Left and Right move the caret; at either end they hand the
key back to the toolbar and focus moves on. Home and End are never taken — a
toolbar does not bind them — so they jump the caret as they would in any field.
That handling is the reason to reach for it rather than dropping a bare
Input into the bar — a bare one is not in the roving
order at all, so Tab is the only way past it, and you have just paid for the
tab stop the toolbar exists to save.
Links
Toolbar.Link is drawn as a link and not as a button on purpose: it navigates,
and the underline is the only cue that says so before it is clicked. Colour
alone would fail SC 1.4.1 against the buttons beside it, which share the bar's
text colour.
Use render for a router link — render={<Link href="/docs" />}.
Disabled
disabled on the root reaches every item; on a Toolbar.Group it reaches that
group's items and composes with the root's.
A link is the one thing disabled cannot reach: HTML has no disabled state for
an <a>, so it stays live and focusable inside a disabled toolbar. Render it
conditionally if it has to go away.
In an editor
Everything the component is for, in one bar.
Accessibility
| Key | Behaviour |
|---|---|
| Tab | Moves focus into the toolbar, landing on the last-focused item — or the first one — and out again in one press. The whole bar is a single tab stop. |
| ArrowRight then ArrowDown | Moves focus to the next item, wrapping from the last to the first. Only the arrows on the toolbar's own axis are bound. |
| ArrowLeft then ArrowUp | Moves focus to the previous item, wrapping from the first to the last. |
| Home then End | Not bound. Unlike Toggle Group and Tabs, a toolbar leaves these to the page — or to the caret, when the focused item is a text input. |
| Space then Enter | Activates the focused item. Moving focus never activates anything. |
Set loopFocus={false} to stop the arrows wrapping at the ends.
Each icon-only item still needs a name of its own — an icon is not an accessible
name — and iconOnly holds the 24×24 minimum hit target from SC 2.5.8 for you.
Theming
| Property | Controls | Default |
|---|---|---|
--forte-toolbar-gap | Gap between items. size re-points it. | var(--forte-space-2) |
--forte-toolbar-padding | Padding inside the bar. size re-points it; variant="plain" drops it to 0. | var(--forte-space-2) |
--forte-toolbar-radius | Corner radius. variant="plain" drops it to 0. | var(--forte-radius-surface) |
--forte-toolbar-bg | Fill behind the bar. variant sets it. | transparent |
--forte-toolbar-border-width | Border width. | 1px |
--forte-toolbar-border-color | Border colour. variant sets it. | transparent |
--forte-toolbar-shadow | Drop shadow. Unset by default — a toolbar is part of the page, not a surface floating over it. | none |
The bar always draws a 1px border, even when it has no colour. transparent is
not preserved under forced colours — it is replaced with a system colour — so
declaring the border up front is what lets a panel bar keep a visible edge in
high contrast, where its fill disappears, without the accessibility rules having
to add a border and shift every item in the bar.
Group
| Property | Controls | Default |
|---|---|---|
--forte-toolbar-group-gap | Gap between items inside the group. Tighter than the bar's own gap, which is what makes a cluster read as one. | var(--forte-space-1) |
Separator
| Property | Controls | Default |
|---|---|---|
--forte-toolbar-separator-thickness | Line thickness. | 1px |
--forte-toolbar-separator-color | Line colour. | var(--forte-color-border) |
--forte-toolbar-separator-inset | How far the rule stops short of the bar's inner edges. | var(--forte-space-1) |
--forte-toolbar-separator-min-length | Length floor for a rule with nothing to stretch against. | 1lh |
The rule is painted with a border, never a background-color. A background
is replaced with a system colour under forced colours, so a hairline drawn that
way vanishes for exactly the users who most need the grouping cue; border
colours are forced to CanvasText instead.
Link
| Property | Controls | Default |
|---|---|---|
--forte-toolbar-link-height | Height. size re-points it. | var(--forte-control-h-md) |
--forte-toolbar-link-padding-x | Inline padding. | var(--forte-space-2) |
--forte-toolbar-link-radius | Corner radius — for the hover fill and the focus ring, which both need to match the buttons beside it. | var(--forte-radius-control) |
--forte-toolbar-link-font-size | Label size. size re-points it. | var(--forte-font-size-2) |
--forte-toolbar-link-color | Label colour. | var(--forte-color-primary-text) |
--forte-toolbar-link-bg-hover | Fill on hover. | var(--forte-color-panel-hover) |
--forte-toolbar-link-underline-color | Underline colour. | color-mix( in oklab, currentcolor 45%, transparent ) |
--forte-toolbar-link-underline-offset | Distance between the text baseline and its underline. | 0.15em |
--forte-toolbar-link-duration | Colour transitions between states. | var(--forte-duration-fast) |
The controls inside the bar are themed through their own properties —
--forte-button-* on Button, --forte-input-* on
Input, --forte-toggle-* on Toggle.
API reference
Toolbar.Root
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
disabled | boolean | false | Disable every item in the toolbar. Items stay focusable — see the note on `Toolbar.Button`'s `focusableWhenDisabled`. |
orientation | ToolbarOrientation | horizontal | Direction the items are laid out in, and the axis the arrow keys move along. This is not layout only: Base UI binds the arrows to the named axis, so a vertical toolbar answers to Up and Down and leaves Left and Right to the page — and to any text input inside it. |
size | ToolbarSize | md | Size of the bar, and the default size of every `Toolbar.Button`, `Toolbar.Input` and `Toolbar.Link` inside it. An item's own `size` still wins. |
variant | ToolbarVariant | panel | How much chrome the bar itself carries. `plain` is a bare row with no surface — right when the toolbar sits on a panel that already has one. `panel` fills; `outline` draws a border on the page background. It says nothing about the controls inside: a `plain` bar can still hold solid buttons, and a `panel` bar quiet ones. |
wrap | boolean | false | Let the items wrap onto more than one line when the bar runs out of room, instead of overflowing it. Roving focus follows DOM order either way, so wrapping costs the keyboard nothing. Ignored while `orientation="vertical"`. |
Toolbar.Group
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
disabled | boolean | false | Disable every item in the group. Composes with the root's `disabled` — either one being true disables the item. |
Toolbar.Button
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
focusableWhenDisabled | boolean | true | Keep the item focusable while it is disabled, rather than removing it from the bar's roving focus. `true` by default, and that default is the ARIA authoring practice for a toolbar rather than a stylistic choice: a disabled control that vanishes from the arrow-key order silently changes what every other key press does, and the user has no way to find out *why* an action they expected is missing. Focusable-and-disabled keeps it announced, with its disabled state, in the position they expect it. |
iconOnly | boolean | false | Render as a square button sized for a single icon, holding the 24×24 minimum hit target from WCAG SC 2.5.8. Always pair with `aria-label` — an icon is not an accessible name. Ignored when `render` is set. |
loading | boolean | false | Show a busy indicator and block interaction. Ignored when `render` is set. |
loadingLabel | string | Loading | Announced to assistive technology while `loading` is true. Ignored when `render` is set. |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ToolbarButtonState> | Replaces the rendered button with another element or component. This is how a menu, a select, a dialog or a toggle joins the bar's keyboard order — `render={<Toggle iconOnly />}`, `render={<Select.Trigger />}`. The default `Button` styling steps aside when this is present, so the two never fight over the cascade; the rendered component keeps its own look. | |
size | ButtonSize | the root's `size` | Size of the button. Inherited from `Toolbar.Root` when left unset. Ignored when `render` is set. |
tone | ButtonTone | neutral | Which semantic colour set the button draws from. `neutral` by default, for the same reason `variant` is `ghost`. Ignored when `render` is set. |
variant | ButtonVariant | ghost | How much visual weight the button carries. Defaults to `ghost` rather than `Button`'s `solid`: a toolbar is a strip of peers, and a row of solid buttons has no hierarchy left to spend on the one that matters. Give the primary action `variant="solid"` explicitly. Ignored when `render` is set — the rendered component brings its own styling. |
Every other Button prop passes through unchanged, as do
Base UI's own nativeButton and render.
Toolbar.Link
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
size | ToolbarSize | the root's `size` | Size of the link, matching the controls beside it. Inherited from `Toolbar.Root` when left unset. |
Toolbar.Input
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
fullWidth | boolean | false | Stretch the input to fill the space left over in the bar. Ignored when `render` is set. |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ToolbarInputState> | Replaces the rendered input with another element or component. The default `Input` styling steps aside when this is present. | |
size | ToolbarSize | the root's `size` | Size of the control. Inherited from `Toolbar.Root` when left unset. This shadows the native `size` attribute, exactly as `Input`'s does. Ignored when `render` is set. |
variant | InputVariant | outline | How much visual weight the control carries. Ignored when `render` is set. |
Toolbar.Separator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
orientation | ToolbarOrientation | Which way the rule runs. Defaults to the *opposite* of the toolbar's orientation — a horizontal bar gets vertical rules — which is almost always what you want, so leave it unset. |