Navigation
Toolbar
Put a window's most-used commands in its title bar, or float them over an iPhone screen.
"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.
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-mirrorand 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
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
loop | boolean | — | Whether the arrow keys wrap from the last item to the first. |
disabled | boolean | — | Disables every item. They stay focusable. |
ToolbarButton, ToolbarLink, ToolbarToggle
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
| Prop | Type | Default | Description |
|---|---|---|---|
subtitle | ReactNode | — | A second line, like when the document was edited. |
ToolbarSearchField
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | — | Names the field. Required. |
placeholder | string | "Search" | Hint text. |
It takes the rest of an input's props, like value and onChange.
Data attributes and CSS variables
| Name | On | Description |
|---|---|---|
data-variant | .mb-toolbar | unified or floating. |
data-orientation | every part | The toolbar's orientation. |
data-pressed | .mb-toolbar-button | A toggle that's on. |
data-disabled | items and groups | Unavailable, still focusable. |
data-mirror | an svg in .mb-toolbar-button | Set it yourself on a directional symbol to flip it in right to left. |
--mb-toolbar-item-height | inherited | 28 points on macOS, 44 on touch. |
--mb-toolbar-icon-size | inherited | 16 points on macOS, 22 on touch. |
--mb-toolbar-floating-height | inherited | Height of the floating capsules. |