Toolbar
PortedControls joined into one card, such as a filter bar above a table or text formatting. The toolbar is one Tab stop; arrow keys move between its controls and wrap around. Give it an aria-label. Put in ToolbarButtons, a ToolbarInput, Toggles and ToggleGroups, and menu triggers (<DropdownMenuTrigger render={<ToolbarButton>Status</ToolbarButton>} />). For two or three buttons that act as one, use ButtonGroup.
import { FunnelSimpleIcon, GearSixIcon, MagnifyingGlassIcon } from "@phosphor-icons/react";
import { Toolbar, ToolbarButton, ToolbarInput } from "@etched/monet/toolbar";
export function Preview() {
return (
<Toolbar aria-label="Runs" style={{ inlineSize: "100%", maxInlineSize: "24rem" }}>
<ToolbarInput icon={<MagnifyingGlassIcon />} aria-label="Search runs" placeholder="Search runs" />
<ToolbarButton shape="square" aria-label="Filter" icon={<FunnelSimpleIcon />} />
<ToolbarButton shape="square" aria-label="Settings" icon={<GearSixIcon />} />
</Toolbar>
);
}Installation
import { Toolbar, ToolbarButton, ToolbarInput, ToolbarSeparator } from "@etched/monet/toolbar";Examples
Filter bar above a table
A search field that takes the free width, two menus, a toggle, and after a separator a button that clears the filters. The toolbar is one Tab stop: arrow keys move between its controls. A disabled button stays in that order.
| Run | Status | Owner |
|---|---|---|
| Thermal soak 1284 | Passed | Sam Lee |
| Power cycle 1285 | Failed | Kai Ito |
| Memory stress 1286 | Running | Sam Lee |
import { ArchiveIcon, CaretDownIcon, FunnelSimpleIcon, MagnifyingGlassIcon, UserCircleIcon } from "@phosphor-icons/react";
import { useState } from "react";
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from "@etched/monet/dropdown";
import { Pill, type PillTone } from "@etched/monet/pill";
import { Table } from "@etched/monet/table";
import { Toggle } from "@etched/monet/toggle";
import { Toolbar, ToolbarButton, ToolbarInput, ToolbarSeparator } from "@etched/monet/toolbar";
interface Run {
id: string;
name: string;
status: "Passed" | "Failed" | "Running";
owner: string;
archived: boolean;
}
const runs: Run[] = [
{ id: "1284", name: "Thermal soak", status: "Passed", owner: "Sam Lee", archived: false },
{ id: "1285", name: "Power cycle", status: "Failed", owner: "Kai Ito", archived: false },
{ id: "1286", name: "Memory stress", status: "Running", owner: "Sam Lee", archived: false },
{ id: "1251", name: "Fan curve", status: "Passed", owner: "Ana Ruiz", archived: true },
];
const tones = new Map<Run["status"], PillTone>([
["Passed", "green"],
["Failed", "red"],
["Running", "blue"],
]);
const owners = ["Anyone", "Sam Lee", "Kai Ito", "Ana Ruiz"];
export function FilterBarAboveATable() {
const [query, setQuery] = useState("");
const [statuses, setStatuses] = useState<Run["status"][]>([]);
const [owner, setOwner] = useState("Anyone");
const [archived, setArchived] = useState(false);
const shown = runs.filter(
(run) =>
run.name.toLowerCase().includes(query.trim().toLowerCase()) &&
(!statuses.length || statuses.includes(run.status)) &&
(owner === "Anyone" || run.owner === owner) &&
(archived || !run.archived),
);
const filtered = Boolean(query || statuses.length || owner !== "Anyone" || archived);
const clear = () => {
setQuery("");
setStatuses([]);
setOwner("Anyone");
setArchived(false);
};
return (
<div style={{ display: "grid", gap: "var(--et-space-3)" }}>
<Toolbar aria-label="Filter runs">
<ToolbarInput icon={<MagnifyingGlassIcon />} aria-label="Search runs" placeholder="Search runs" value={query} onChange={(event) => setQuery(event.target.value)} />
<DropdownMenu>
<DropdownMenuTrigger
render={
<ToolbarButton icon={<FunnelSimpleIcon />}>
{statuses.length ? `Status: ${statuses.length}` : "Status"}
<CaretDownIcon />
</ToolbarButton>
}
/>
<DropdownMenuContent align="start">
{[...tones.keys()].map((status) => (
<DropdownMenuCheckboxItem
key={status}
checked={statuses.includes(status)}
onCheckedChange={(on) => setStatuses((current) => (on ? [...current, status] : current.filter((s) => s !== status)))}
>
{status}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
render={
<ToolbarButton icon={<UserCircleIcon />}>
{owner === "Anyone" ? "Owner" : owner}
<CaretDownIcon />
</ToolbarButton>
}
/>
<DropdownMenuContent align="start">
<DropdownMenuRadioGroup value={owner} onValueChange={setOwner}>
{owners.map((name) => (
<DropdownMenuRadioItem key={name} value={name}>
{name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<Toggle icon={<ArchiveIcon />} pressed={archived} onPressedChange={setArchived}>
Archived
</Toggle>
<ToolbarSeparator />
<ToolbarButton disabled={!filtered} onClick={clear}>
Clear
</ToolbarButton>
</Toolbar>
<div className="card">
<Table aria-label="Runs">
<thead>
<tr>
<th style={{ inlineSize: "40%" }}>Run</th>
<th>Status</th>
<th>Owner</th>
</tr>
</thead>
<tbody>
{shown.map((run) => (
<tr key={run.id}>
<th scope="row">
{run.name} {run.id}
</th>
<td>
<Pill tone={tones.get(run.status)}>{run.status}</Pill>
</td>
<td>{run.owner}</td>
</tr>
))}
{!shown.length && (
<tr>
<td colSpan={3}>No runs match these filters.</td>
</tr>
)}
</tbody>
</Table>
</div>
</div>
);
}Formatting
Toggle groups and a button, split into groups by separators. Arrow keys move through all of them.
import { LinkSimpleIcon, TextAlignCenterIcon, TextAlignLeftIcon, TextAlignRightIcon, TextBIcon, TextItalicIcon, TextUnderlineIcon } from "@phosphor-icons/react";
import { ToggleGroup } from "@etched/monet/toggle-group";
import { Toggle } from "@etched/monet/toggle";
import { Toolbar, ToolbarButton, ToolbarSeparator } from "@etched/monet/toolbar";
export function Formatting() {
return (
<Toolbar aria-label="Formatting">
<ToggleGroup multiple aria-label="Text style" defaultValue={["bold"]}>
<Toggle value="bold" shape="square" aria-label="Bold" icon={<TextBIcon />} />
<Toggle value="italic" shape="square" aria-label="Italic" icon={<TextItalicIcon />} />
<Toggle value="underline" shape="square" aria-label="Underline" icon={<TextUnderlineIcon />} />
</ToggleGroup>
<ToolbarSeparator />
<ToggleGroup aria-label="Text alignment" defaultValue={["left"]}>
<Toggle value="left" shape="square" aria-label="Align left" icon={<TextAlignLeftIcon />} />
<Toggle value="center" shape="square" aria-label="Align center" icon={<TextAlignCenterIcon />} />
<Toggle value="right" shape="square" aria-label="Align right" icon={<TextAlignRightIcon />} />
</ToggleGroup>
<ToolbarSeparator />
<ToolbarButton icon={<LinkSimpleIcon />}>Link</ToolbarButton>
</Toolbar>
);
}Small
size="sm" makes every control 28 px high, for a toolbar inside a table card or a panel.
import { DownloadSimpleIcon, GearSixIcon, MagnifyingGlassIcon } from "@phosphor-icons/react";
import { Toolbar, ToolbarButton, ToolbarInput } from "@etched/monet/toolbar";
export function Small() {
return (
<Toolbar size="sm" aria-label="Run actions" style={{ maxInlineSize: "24rem" }}>
<ToolbarInput icon={<MagnifyingGlassIcon />} aria-label="Filter rows" placeholder="Filter rows" />
<ToolbarButton icon={<DownloadSimpleIcon />}>Export</ToolbarButton>
<ToolbarButton shape="square" aria-label="Settings" icon={<GearSixIcon />} />
</Toolbar>
);
}Accessibility
- The toolbar is one
Tabstop, andShift+Tabcomes back to the last control used.ArrowLeft/ArrowRightmove between its controls and wrap. - In a
ToolbarInput,ArrowLeft/ArrowRightmove the caret, and go on to the next control at the ends of the text. - A
toolbarnamed by itsaria-label. AToggleGroupin it is a namedgroup, andToolbarSeparatoris a verticalseparator. - A disabled
ToolbarButtonstays in the arrow-key order witharia-disabled, so it can show a tooltip.focusableWhenDisabled={false}takes it out. - Give the toolbar and its
ToolbarInputanaria-label. An icon-onlyToolbarButtonneeds one too; the types require it.
API reference
Toolbar
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "base" | "base" | Height of every control in it. |
| className | string | – | Class for layout, such as width. |
ToolbarInput
| Prop | Type | Default | Description |
|---|---|---|---|
| icon | ReactNode | – | Icon before the text, for example a search glass. A press on it focuses the field. |
| className | string | – | Class on the field's frame, for layout such as a width. The field takes the toolbar's free width. |
ToolbarSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – |