macbetter

Navigation

Toolbar

Put a window's most-used commands in its title bar, or float them over an iPhone screen.

macOS only
Launch checklist
Press Tab to reach the toolbar, then use the arrow keys to move between its buttons.
"use client"

import {
  Toolbar,
  ToolbarButton,
  ToolbarGroup,
  ToolbarSearchField,
  ToolbarSeparator,
  ToolbarSpacer,
  ToolbarTitle,
  ToolbarToggle,
} from "macbetter/toolbar"
import { Window, WindowBody, WindowTitlebar, WindowTrafficLights } from "macbetter/window"
import {
  ImageIcon,
  ListChecksIcon,
  PanelLeftIcon,
  ShareIcon,
  SquarePenIcon,
  TableIcon,
  TypeIcon,
} from "lucide-react"

export default function Example() {
  return (
    <Window aria-label="Notes" style={{ maxWidth: 720 }}>
      <WindowTitlebar>
        <WindowTrafficLights />
        <Toolbar aria-label="Note">
          <ToolbarToggle aria-label="Show Sidebar" defaultPressed>
            <PanelLeftIcon aria-hidden="true" />
          </ToolbarToggle>
          <ToolbarTitle subtitle="Edited 2 minutes ago">Launch checklist</ToolbarTitle>
          <ToolbarSpacer />
          <ToolbarGroup aria-label="Insert">
            <ToolbarButton aria-label="Format">
              <TypeIcon aria-hidden="true" />
            </ToolbarButton>
            <ToolbarButton aria-label="Checklist">
              <ListChecksIcon aria-hidden="true" />
            </ToolbarButton>
            <ToolbarButton aria-label="Table">
              <TableIcon aria-hidden="true" />
            </ToolbarButton>
            <ToolbarButton aria-label="Attach Photo">
              <ImageIcon aria-hidden="true" />
            </ToolbarButton>
          </ToolbarGroup>
          <ToolbarSeparator />
          <ToolbarButton aria-label="Share">
            <ShareIcon aria-hidden="true" />
          </ToolbarButton>
          <ToolbarButton aria-label="New Note">
            <SquarePenIcon aria-hidden="true" />
          </ToolbarButton>
          <ToolbarSearchField aria-label="Search notes" />
        </Toolbar>
      </WindowTitlebar>
      <WindowBody style={{ padding: "20px 28px", height: 160 }}>
        <div className="mb-text-title-2">Launch checklist</div>
        <div className="mb-text-body mb-label-secondary" style={{ marginTop: 8 }}>
          Press Tab to reach the toolbar, then use the arrow keys to move between its buttons.
        </div>
      </WindowBody>
    </Window>
  )
}

When to use it

Use a toolbar for the commands people reach for most in the current view: navigating back, switching views, sharing, creating something new, searching. Everything else belongs in a menu.

  • Prefer symbols, and name each one. A title or a short label is fine when a symbol would be unclear.
  • Group related buttons into a cluster, and push the rest to the trailing edge with a flexible space.
  • Put search at the trailing end on the Mac.
  • On iPhone, put the toolbar at the bottom, within reach of a thumb, with three to five items.

Usage

import {
  Toolbar,
  ToolbarButton,
  ToolbarGroup,
  ToolbarSearchField,
  ToolbarSeparator,
  ToolbarSpacer,
  ToolbarTitle,
  ToolbarToggle,
  ToolbarToggleGroup,
} from "macbetter/toolbar"

Unified toolbar

Put the toolbar in a window's title bar. ToolbarTitle shows the document's name with an optional subtitle, ToolbarGroup clusters related buttons in a capsule, ToolbarSpacer pushes what follows to the trailing edge, and ToolbarSearchField is a search field that joins the toolbar's arrow-key navigation.

Toggles and view switchers

ToolbarToggle stays pressed, like Show Sidebar. ToolbarToggleGroup holds a set of toggles where one is chosen, like the Finder's icon, list and column views. Both join the toolbar's navigation without adding tab stops.

Floating toolbar

variant="floating" is the iOS bottom toolbar: each button, cluster and search field becomes its own capsule of glass floating over the content.

iOS only
Inbox
Inès Moreau9:12
Brand review moved to Thursday
Kofi Mensah8:47
Q3 metrics are in
Wren TakahashiYesterday
Offsite photos
Hollis GrantYesterday
Contract signed
macbetterMonday
Your weekly data digest
Ana LimaSunday
Re: onboarding flow
import { DeviceFrame } from "macbetter/device-frame"
import { Toolbar, ToolbarButton, ToolbarSearchField } from "macbetter/toolbar"
import { ListFilterIcon, SquarePenIcon } from "lucide-react"

const messages = [
  { from: "Inès Moreau", subject: "Brand review moved to Thursday", time: "9:12" },
  { from: "Kofi Mensah", subject: "Q3 metrics are in", time: "8:47" },
  { from: "Wren Takahashi", subject: "Offsite photos", time: "Yesterday" },
  { from: "Hollis Grant", subject: "Contract signed", time: "Yesterday" },
  { from: "macbetter", subject: "Your weekly data digest", time: "Monday" },
  { from: "Ana Lima", subject: "Re: onboarding flow", time: "Sunday" },
]

export default function Example() {
  return (
    <DeviceFrame scale={0.78} role="group" aria-label="Mail on iPhone">
      <div style={{ position: "relative", overflow: "hidden" }}>
        <div style={{ height: "100%", overflowY: "auto", padding: "64px 0 110px" }}>
          <div className="mb-text-large-title" style={{ padding: "0 16px 8px" }}>
            Inbox
          </div>
          {messages.map((message) => (
            <div
              key={message.subject}
              style={{ padding: "10px 16px 10px 0", marginLeft: 16, borderBottom: "0.5px solid var(--mb-separator)" }}
            >
              <div style={{ display: "flex", justifyContent: "space-between", gap: 8 }}>
                <span className="mb-text-headline">{message.from}</span>
                <span className="mb-text-subheadline mb-label-secondary">{message.time}</span>
              </div>
              <div className="mb-text-subheadline">{message.subject}</div>
            </div>
          ))}
        </div>
        <Toolbar
          aria-label="Mailbox"
          variant="floating"
          style={{ position: "absolute", insetInline: 16, bottom: 24 }}
        >
          <ToolbarButton aria-label="Filter">
            <ListFilterIcon aria-hidden="true" />
          </ToolbarButton>
          <ToolbarSearchField aria-label="Search mail" />
          <ToolbarButton aria-label="New Message">
            <SquarePenIcon aria-hidden="true" />
          </ToolbarButton>
        </Toolbar>
      </div>
    </DeviceFrame>
  )
}

Platform behavior

  • macOS: 28 point buttons with a soft fill under the pointer and a darker one when pressed. Symbols are secondary gray and darken on hover. Clusters sit in a recessed capsule, and a chosen toggle in a cluster is raised.
  • Narrow windows: the search field gives up its width first. Below about 100 points it folds into a magnifying glass, which opens back into a field when focused. Only after that does a long title truncate.
  • Right to left: the toolbar mirrors. Mark directional symbols, like Back and Forward chevrons, with data-mirror and they point the other way.
  • iOS and iPadOS: 44 point targets with no hover. Buttons give slightly under the finger. Plain toolbar buttons take the accent color; floating capsules keep the label color, so the glass carries the emphasis.

Accessibility

  • The toolbar is one tab stop. Tab enters on the first item, or the one focused last; the arrow keys move between items, Home and End jump to the ends, and Tab leaves. This comes from ui-sdk's Toolbar.
  • Name the toolbar with aria-label, and give every icon-only button its own.
  • Separators and the flexible space are hidden from assistive technology; groups are named with aria-label.
  • The search field keeps the arrow keys while its caret moves, and moves on at either end.

API

Toolbar

PropTypeDefaultDescription
variant"unified" | "floating""unified"Unified sits in a title bar. Floating turns each item into a capsule of glass.
orientation"horizontal" | "vertical""horizontal"Which arrow keys move between items.
loopboolean—Whether the arrow keys wrap from the last item to the first.
disabledboolean—Disables every item. They stay focusable.

ToolbarButton and ToolbarLink take ui-sdk's props: disabled and focusableWhenDisabled. ToolbarToggle takes the Toggle's pressed, defaultPressed, onPressedChange and, in a group, value.

ToolbarGroup, ToolbarToggleGroup

ToolbarGroup takes disabled. ToolbarToggleGroup takes the Toggle Group's value, defaultValue, onValueChange and multiple. Name both with aria-label.

ToolbarTitle

PropTypeDefaultDescription
subtitleReactNode—A second line, like when the document was edited.

ToolbarSearchField

PropTypeDefaultDescription
aria-labelstring—Names the field. Required.
placeholderstring"Search"Hint text.

It takes the rest of an input's props, like value and onChange.

Data attributes and CSS variables

NameOnDescription
data-variant.mb-toolbarunified or floating.
data-orientationevery partThe toolbar's orientation.
data-pressed.mb-toolbar-buttonA toggle that's on.
data-disableditems and groupsUnavailable, still focusable.
data-mirroran svg in .mb-toolbar-buttonSet it yourself on a directional symbol to flip it in right to left.
--mb-toolbar-item-heightinherited28 points on macOS, 44 on touch.
--mb-toolbar-icon-sizeinherited16 points on macOS, 22 on touch.
--mb-toolbar-floating-heightinheritedHeight of the floating capsules.