Skip to content

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">.

toolbar/basic.tsx

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.

toolbar/groups.tsx

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.

toolbar/variants.tsx

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.

toolbar/sizes.tsx

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.

toolbar/orientation.tsx

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.

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/composition.tsx

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.

toolbar/input.tsx

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.

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.

toolbar/link.tsx

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.

toolbar/disabled.tsx

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.

toolbar/editor.tsx

Accessibility

Keyboard interactions
KeyBehaviour
TabMoves 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 ArrowDownMoves 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 ArrowUpMoves focus to the previous item, wrapping from the first to the last.
Home then EndNot 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 EnterActivates 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

Theming tokens for Toolbar
PropertyControlsDefault
--forte-toolbar-gapGap between items. size re-points it.var(--forte-space-2)
--forte-toolbar-paddingPadding inside the bar. size re-points it; variant="plain" drops it to 0.var(--forte-space-2)
--forte-toolbar-radiusCorner radius. variant="plain" drops it to 0.var(--forte-radius-surface)
--forte-toolbar-bgFill behind the bar. variant sets it.transparent
--forte-toolbar-border-widthBorder width.1px
--forte-toolbar-border-colorBorder colour. variant sets it.transparent
--forte-toolbar-shadowDrop 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

Theming tokens for Toolbar
PropertyControlsDefault
--forte-toolbar-group-gapGap 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

Theming tokens for Toolbar
PropertyControlsDefault
--forte-toolbar-separator-thicknessLine thickness.1px
--forte-toolbar-separator-colorLine colour.var(--forte-color-border)
--forte-toolbar-separator-insetHow far the rule stops short of the bar's inner edges.var(--forte-space-1)
--forte-toolbar-separator-min-lengthLength 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.

Theming tokens for Toolbar
PropertyControlsDefault
--forte-toolbar-link-heightHeight. size re-points it.var(--forte-control-h-md)
--forte-toolbar-link-padding-xInline padding.var(--forte-space-2)
--forte-toolbar-link-radiusCorner 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-sizeLabel size. size re-points it.var(--forte-font-size-2)
--forte-toolbar-link-colorLabel colour.var(--forte-color-primary-text)
--forte-toolbar-link-bg-hoverFill on hover.var(--forte-color-panel-hover)
--forte-toolbar-link-underline-colorUnderline colour.color-mix( in oklab, currentcolor 45%, transparent )
--forte-toolbar-link-underline-offsetDistance between the text baseline and its underline.0.15em
--forte-toolbar-link-durationColour 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

Props for ToolbarRoot
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
disabledbooleanfalseDisable every item in the toolbar. Items stay focusable — see the note on `Toolbar.Button`'s `focusableWhenDisabled`.
orientationToolbarOrientationhorizontalDirection 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.
sizeToolbarSizemdSize 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.
variantToolbarVariantpanelHow 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.
wrapbooleanfalseLet 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

Props for ToolbarGroup
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
disabledbooleanfalseDisable every item in the group. Composes with the root's `disabled` — either one being true disables the item.

Toolbar.Button

Props for ToolbarButton
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
focusableWhenDisabledbooleantrueKeep 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.
iconOnlybooleanfalseRender 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.
loadingbooleanfalseShow a busy indicator and block interaction. Ignored when `render` is set.
loadingLabelstringLoadingAnnounced to assistive technology while `loading` is true. Ignored when `render` is set.
renderReactElement<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.
sizeButtonSizethe root's `size`Size of the button. Inherited from `Toolbar.Root` when left unset. Ignored when `render` is set.
toneButtonToneneutralWhich semantic colour set the button draws from. `neutral` by default, for the same reason `variant` is `ghost`. Ignored when `render` is set.
variantButtonVariantghostHow 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.

Props for ToolbarLink
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
sizeToolbarSizethe root's `size`Size of the link, matching the controls beside it. Inherited from `Toolbar.Root` when left unset.

Toolbar.Input

Props for ToolbarInput
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
fullWidthbooleanfalseStretch the input to fill the space left over in the bar. Ignored when `render` is set.
renderReactElement<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.
sizeToolbarSizethe 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.
variantInputVariantoutlineHow much visual weight the control carries. Ignored when `render` is set.

Toolbar.Separator

Props for ToolbarSeparator
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
orientationToolbarOrientationWhich 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.