Toolbar

Groups related controls into a single keyboard navigation region.

import { AlignLeft, AlignRight } from "lucide-react"
import {
  Toolbar,
  ToolbarButton,
  ToolbarToggle,
  ToolbarGroup,
  ToolbarLink,
  ToolbarSeparator,
} from "@/components/ui/toolbar"
import { ToggleGroup } from "@/components/ui/toggle-group"

export default function ToolbarDemo() {
  return (
    <Toolbar aria-label="Formatting">
      <ToggleGroup aria-label="Alignment" defaultValue={["left"]} spacing={0}>
        <ToolbarToggle value="left" aria-label="Align left">
          <AlignLeft />
        </ToolbarToggle>
        <ToolbarToggle value="right" aria-label="Align right">
          <AlignRight />
        </ToolbarToggle>
      </ToggleGroup>
      <ToolbarSeparator />
      <ToolbarGroup aria-label="Numerical format">
        <ToolbarButton aria-label="Format as currency">$</ToolbarButton>
        <ToolbarButton aria-label="Format as percent">%</ToolbarButton>
      </ToolbarGroup>
      <ToolbarSeparator />
      <ToolbarLink href="#usage">Edited 51m ago</ToolbarLink>
    </Toolbar>
  )
}

Installation

Install the following dependencies:

pnpm add @base-ui/react @stylexjs/stylex

Set up Installation, including states.ts, state-styles.ts, and interop.css for icons. Install Button, Input, Separator, and Toggle for shared styles and parts.

Copy and paste the following code into your project.

components/ui/toolbar.tsx
"use client"

import type { CSSProperties } from "react"
import { Toolbar as ToolbarPrimitive } from "@base-ui/react/toolbar"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"

import { buttonStyles, type ButtonSize, type ButtonVariant } from "@/components/ui/button"
import { inputStyles } from "@/components/ui/input"
import { Toggle, ToggleGroupContext, type ToggleProps } from "@/components/ui/toggle"
import { separatorStyles } from "@/components/ui/separator"
import { stateStyles } from "@/lib/state-styles"
import { states } from "@/theme/states"
import { color, radius, space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  root: {
    display: "flex",
    alignItems: "center",
    gap: space.s1,
    width: "fit-content",
    padding: space.s1,
    borderRadius: radius.xl,
  },
  vertical: { flexDirection: "column", alignItems: "stretch" },
  group: { display: "flex", alignItems: "center", gap: space.s1 },
  separator: { marginInline: space.s1, marginBlock: space.s1 },
  ghostOnWell: {
    backgroundColor: { default: null, ":hover": color.itemHighlightBg },
    "--carbon-menu-open-bg": color.itemHighlightBg,
  },
})
const toolbarStyles = {
  ...styles,
  root: [states.well, styles.root],
  button: buttonStyles.base,
  variants: buttonStyles.variants,
  sizes: buttonStyles.sizes,
  input: inputStyles.base,
}
type Overrides = { xstyle?: StyleXStyles; className?: string; style?: CSSProperties }
type ButtonOptions = { variant?: ButtonVariant; size?: ButtonSize }

function Toolbar({
  xstyle,
  className,
  style,
  children,
  ...props
}: Omit<ToolbarPrimitive.Root.Props, "className" | "style"> & Overrides) {
  return (
    <ToolbarPrimitive.Root
      data-slot="toolbar"
      {...props}
      {...stateStyles(
        (state) => [
          states.well,
          styles.root,
          state.orientation === "vertical" && styles.vertical,
          xstyle,
        ],
        { className, style },
      )}
    >
      <ToggleGroupContext value={{ onWell: true }}>{children}</ToggleGroupContext>
    </ToolbarPrimitive.Root>
  )
}
function ToolbarGroup({
  xstyle,
  className,
  style,
  ...props
}: Omit<ToolbarPrimitive.Group.Props, "className" | "style"> & Overrides) {
  return (
    <ToolbarPrimitive.Group
      data-slot="toolbar-group"
      {...props}
      {...stateStyles(
        (state) => [styles.group, state.orientation === "vertical" && styles.vertical, xstyle],
        { className, style },
      )}
    />
  )
}
function ToolbarButton({
  variant = "ghost",
  size = "default",
  xstyle,
  className,
  style,
  ...props
}: Omit<ToolbarPrimitive.Button.Props, "className" | "style"> & Overrides & ButtonOptions) {
  return (
    <ToolbarPrimitive.Button
      data-slot="toolbar-button"
      data-variant={variant}
      data-size={size}
      {...props}
      {...stateStyles(
        (state) => [
          buttonStyles.base,
          buttonStyles.variants[variant],
          variant === "ghost" && styles.ghostOnWell,
          buttonStyles.sizes[size],
          state.disabled && states.disabled,
          xstyle,
        ],
        { className, style, hook: "carbon-icons" },
      )}
    />
  )
}
function ToolbarToggle<Value extends string = string>({
  focusableWhenDisabled = true,
  ...props
}: ToggleProps<Value> & { focusableWhenDisabled?: boolean }) {
  return (
    <ToolbarPrimitive.Button
      disabled={props.disabled}
      nativeButton={props.nativeButton}
      focusableWhenDisabled={focusableWhenDisabled}
      render={<Toggle data-slot="toolbar-toggle" {...props} />}
    />
  )
}

function ToolbarLink({
  variant = "link",
  size = "default",
  xstyle,
  className,
  style,
  ...props
}: Omit<ToolbarPrimitive.Link.Props, "className" | "style"> & Overrides & ButtonOptions) {
  return (
    <ToolbarPrimitive.Link
      data-slot="toolbar-link"
      data-variant={variant}
      data-size={size}
      {...props}
      {...stateStyles(
        () => [buttonStyles.base, buttonStyles.variants[variant], buttonStyles.sizes[size], xstyle],
        { className, style, hook: "carbon-icons" },
      )}
    />
  )
}
function ToolbarInput({
  xstyle,
  className,
  style,
  ...props
}: Omit<ToolbarPrimitive.Input.Props, "className" | "style"> & Overrides) {
  return (
    <ToolbarPrimitive.Input
      data-slot="toolbar-input"
      {...props}
      {...stateStyles((state) => [inputStyles.base, state.disabled && states.disabled, xstyle], {
        className,
        style,
      })}
    />
  )
}
function ToolbarSeparator({
  xstyle,
  className,
  style,
  ...props
}: Omit<ToolbarPrimitive.Separator.Props, "className" | "style"> & Overrides) {
  return (
    <ToolbarPrimitive.Separator
      data-slot="toolbar-separator"
      {...props}
      {...stateStyles(
        (state) => [
          separatorStyles.base,
          separatorStyles.orientations[state.orientation],
          styles.separator,
          xstyle,
        ],
        { className, style },
      )}
    />
  )
}
export {
  Toolbar,
  ToolbarGroup,
  ToolbarButton,
  ToolbarToggle,
  ToolbarLink,
  ToolbarInput,
  ToolbarSeparator,
  toolbarStyles,
}

Update the import paths to match your project setup.

Usage

import { Toolbar, ToolbarButton, ToolbarSeparator } from "@/components/ui/toolbar"
<Toolbar aria-label="Document actions">
  <ToolbarButton>Undo</ToolbarButton>
  <ToolbarSeparator />
  <ToolbarButton>Redo</ToolbarButton>
</Toolbar>

Give the toolbar an aria-label. In a horizontal toolbar, place at most one ToolbarInput last so arrow navigation does not conflict with moving the caret.

Use ToolbarToggle inside Toggle Group for mutually exclusive or multiple selections.

Using with Menu

Pass DropdownMenuTrigger to the button's render prop.

import { Toolbar, ToolbarButton } from "@/components/ui/toolbar"
import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
} from "@/components/ui/dropdown-menu"
export default function ToolbarMenu() {
  return (
    <Toolbar aria-label="Document">
      <ToolbarButton>Save</ToolbarButton>
      <DropdownMenu>
        <ToolbarButton render={<DropdownMenuTrigger />}>More</ToolbarButton>
        <DropdownMenuContent>
          <DropdownMenuItem>Print</DropdownMenuItem>
          <DropdownMenuItem>Export</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </Toolbar>
  )
}

Using with Tooltip

Pass ToolbarButton to TooltipTrigger's render prop.

import { Bold } from "lucide-react"
import { Toolbar, ToolbarButton } from "@/components/ui/toolbar"
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@/components/ui/tooltip"
export default function ToolbarTooltip() {
  return (
    <TooltipProvider>
      <Toolbar aria-label="Text">
        <Tooltip>
          <TooltipTrigger render={<ToolbarButton aria-label="Bold" size="icon" />}>
            <Bold />
          </TooltipTrigger>
          <TooltipContent>Bold</TooltipContent>
        </Tooltip>
      </Toolbar>
    </TooltipProvider>
  )
}

Using with NumberField

Use NumberFieldToolbarInput from Number Field as the last control.

import * as stylex from "@stylexjs/stylex"
import { Toolbar, ToolbarButton, ToolbarSeparator } from "@/components/ui/toolbar"
import { NumberField, NumberFieldGroup, NumberFieldToolbarInput } from "@/components/ui/number-field"
const styles = stylex.create({ number: { width: "5rem" } })
export default function ToolbarNumberField() {
  return (
    <Toolbar aria-label="Text size">
      <ToolbarButton>Reset</ToolbarButton>
      <ToolbarSeparator />
      <NumberField defaultValue={16} min={8} max={96} xstyle={styles.number}>
        <NumberFieldGroup>
          <NumberFieldToolbarInput aria-label="Font size" />
        </NumberFieldGroup>
      </NumberField>
    </Toolbar>
  )
}

Orientation

Use orientation to switch between horizontal and vertical navigation.

import * as stylex from "@stylexjs/stylex"
import { Toolbar, ToolbarButton, ToolbarSeparator, ToolbarInput } from "@/components/ui/toolbar"
import { VStack } from "@/components/ui/stack"
const styles = stylex.create({ input: { width: "10rem" } })

export default function ToolbarOrientation() {
  return (
    <VStack gap={4}>
      {(["horizontal", "vertical"] as const).map((orientation) => (
        <Toolbar key={orientation} orientation={orientation} aria-label={`${orientation} toolbar`}>
          <ToolbarButton>Undo</ToolbarButton>
          <ToolbarSeparator />
          <ToolbarButton>Redo</ToolbarButton>
          <ToolbarInput
            aria-label="Document name"
            placeholder="Document name"
            xstyle={styles.input}
          />
        </Toolbar>
      ))}
    </VStack>
  )
}

Disabled

Use disabled on the toolbar, a group, or an individual control; set focusableWhenDisabled={false} to skip a disabled control during navigation.

import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarInput } from "@/components/ui/toolbar"
import { VStack } from "@/components/ui/stack"
export default function ToolbarDisabled() {
  return (
    <VStack gap={4}>
      <Toolbar aria-label="Partially disabled">
        <ToolbarButton>Undo</ToolbarButton>
        <ToolbarButton disabled>Redo</ToolbarButton>
        <ToolbarButton disabled focusableWhenDisabled={false}>
          Delete
        </ToolbarButton>
        <ToolbarGroup disabled>
          <ToolbarButton>Copy</ToolbarButton>
        </ToolbarGroup>
      </Toolbar>
      <Toolbar disabled aria-label="Disabled toolbar">
        <ToolbarButton>Save</ToolbarButton>
        <ToolbarInput aria-label="Name" defaultValue="Untitled" />
      </Toolbar>
    </VStack>
  )
}

API reference

Parts accept Base UI Toolbar props, including render. Styled parts accept xstyle, className, and inline style. ToolbarToggle accepts Toggle props and focusableWhenDisabled (default true).

PropTypeDefault
variant"default" | "outline" | "secondary" | "ghost" | "destructive" | "link""ghost" (button), "link" (link)
size"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg""default"

toolbarStyles

KeyWhat it is
rootThe well, inset and nested corner
verticalVertical arrangement
groupGroup layout
separatorSeparator spacing
ghostOnWellRow-highlight hover and open fill for ghost buttons
buttonButton base and focus ring
variants, sizesButton appearance maps
inputInput base and focus ring

Data attributes

AttributeOnEffect
data-slot="toolbar"rootToolbar anatomy
data-slot="toolbar-group"groupGroup anatomy
data-slot="toolbar-button"buttonAction anatomy
data-slot="toolbar-toggle"toggleSelection anatomy
data-slot="toolbar-link"linkLink anatomy
data-slot="toolbar-input"inputInput anatomy
data-slot="toolbar-separator"separatorSeparator anatomy
data-orientationpartsHorizontal or vertical
data-disabledroot, group, controlsDisabled state
data-focusablebutton, inputDisabled control remains focusable
data-pressedtoggleSelected state
data-variant, data-sizebutton, link, toggleAppearance