Give buttons, toggles, menus and fields one tab stop with arrow-key navigation.
Use a Toolbar to group the controls of an editor, a canvas or a table. Tab enters it once, and the arrow keys move between its buttons, toggles, menus and fields.
import { Toggle, ToggleIcon } from "@ui-sdk/solid/toggle"
import { Toolbar, ToolbarButton, ToolbarSeparator } from "@ui-sdk/solid/toolbar"
import { For } from "solid-js"
import { useLog } from "./log"
const icon = (markup: string) => () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
innerHTML={markup}
/>
)
const Bold = icon(
'<path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" />'
)
const Italic = icon(
'<line x1="19" x2="10" y1="4" y2="4" /><line x1="14" x2="5" y1="20" y2="20" /><line x1="15" x2="9" y1="4" y2="20" />'
)
const Underline = icon(
'<path d="M6 4v6a6 6 0 0 0 12 0V4" /><line x1="4" x2="20" y1="20" y2="20" />'
)
const styles = [
{ label: "Bold", icon: Bold },
{ label: "Italic", icon: Italic },
{ label: "Underline", icon: Underline },
]
function Example() {
const log = useLog()
return (
<Toolbar aria-label="Formatting">
<For each={styles}>
{(style) => (
<Toggle
aria-label={style.label}
onPressedChange={(pressed) =>
log(`${style.label} ${pressed ? "on" : "off"}`)
}
>
<ToggleIcon>
<style.icon />
</ToggleIcon>
</Toggle>
)}
</For>
<ToolbarSeparator />
<ToolbarButton
aria-label="Insert link"
onClick={() => log("Insert link")}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</ToolbarButton>
</Toolbar>
)
}
export { Example }Installation
npm install @ui-sdk/solid @ui-sdk/coreComposition
Use the following composition to build a Toolbar:
Toolbar
├── ToolbarButton
├── ToolbarSeparator
├── ToolbarGroup
│ ├── ToolbarButton
│ └── ToolbarButton
├── ToolbarLink
└── ToolbarInputPut buttons, links and fields straight inside the toolbar. ToolbarGroup names a set of related items, and ToolbarSeparator draws a line between sets. Toggles and toggle groups join the toolbar on their own, and menu and select triggers join through ToolbarButton.
Usage
Import the parts and put them together:
import { Toolbar, ToolbarButton, ToolbarSeparator } from "@ui-sdk/solid/toolbar"
function Example() {
return (
<Toolbar aria-label="Formatting">
<ToolbarButton>Undo</ToolbarButton>
<ToolbarButton>Redo</ToolbarButton>
<ToolbarSeparator />
<ToolbarButton>Insert Link</ToolbarButton>
</Toolbar>
)
}
export { Example }Name the toolbar with aria-label, and give every icon-only control its own.
Toggle Groups
A Toggle group inside a toolbar joins its navigation instead of adding another tab stop. Arrow keys move focus without pressing anything; Enter or Space presses the focused toggle.
import { Toggle, ToggleGroup, ToggleIcon } from "@ui-sdk/solid/toggle"
import { Toolbar, ToolbarSeparator } from "@ui-sdk/solid/toolbar"
import { For } from "solid-js"
import { useLog } from "./log"
const icon = (markup: string) => () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
innerHTML={markup}
/>
)
const AlignLeft = icon(
'<path d="M21 5H3" /><path d="M15 12H3" /><path d="M17 19H3" />'
)
const AlignCenter = icon(
'<path d="M21 5H3" /><path d="M17 12H7" /><path d="M19 19H5" />'
)
const AlignRight = icon(
'<path d="M21 5H3" /><path d="M21 12H9" /><path d="M21 19H7" />'
)
const List = icon(
'<path d="M3 5h.01" /><path d="M3 12h.01" /><path d="M3 19h.01" /><path d="M8 5h13" /><path d="M8 12h13" /><path d="M8 19h13" />'
)
const ListOrdered = icon(
'<path d="M11 5h10" /><path d="M11 12h10" /><path d="M11 19h10" /><path d="M4 4h1v5" /><path d="M4 9h2" /><path d="M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02" />'
)
const alignments = [
{ value: "left", label: "Align left", icon: AlignLeft },
{ value: "center", label: "Align center", icon: AlignCenter },
{ value: "right", label: "Align right", icon: AlignRight },
]
const lists = [
{ value: "bulleted", label: "Bulleted list", icon: List },
{ value: "numbered", label: "Numbered list", icon: ListOrdered },
]
function Example() {
const log = useLog()
return (
<Toolbar aria-label="Paragraph">
<ToggleGroup
defaultValue={["left"]}
deselectable={false}
aria-label="Alignment"
onValueChange={([value]) => log(`Align ${value}`)}
>
<For each={alignments}>
{(alignment) => (
<Toggle value={alignment.value} aria-label={alignment.label}>
<ToggleIcon>
<alignment.icon />
</ToggleIcon>
</Toggle>
)}
</For>
</ToggleGroup>
<ToolbarSeparator />
<ToggleGroup
aria-label="List"
onValueChange={([value]) =>
log(lists.find((list) => list.value === value)?.label ?? "No list")
}
>
<For each={lists}>
{(list) => (
<Toggle value={list.value} aria-label={list.label}>
<ToggleIcon>
<list.icon />
</ToggleIcon>
</Toggle>
)}
</For>
</ToggleGroup>
</Toolbar>
)
}
export { Example }Groups
Use ToolbarGroup to name related controls. Set disabled on the group to disable them together; they stay focusable with aria-disabled.
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
ToolbarSeparator,
} from "@ui-sdk/solid/toolbar"
import { For } from "solid-js"
import { useLog } from "./log"
const icon = (markup: string) => () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
innerHTML={markup}
/>
)
const Scissors = icon(
'<circle cx="6" cy="6" r="3" /><path d="M8.12 8.12 12 12" /><path d="M20 4 8.12 15.88" /><circle cx="6" cy="18" r="3" /><path d="M14.8 14.8 20 20" />'
)
const Copy = icon(
'<rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />'
)
const Clipboard = icon(
'<rect width="8" height="4" x="8" y="2" rx="1" ry="1" /><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />'
)
const clipboard = [
{ label: "Cut", icon: Scissors },
{ label: "Copy", icon: Copy },
{ label: "Paste", icon: Clipboard },
]
function Example() {
const log = useLog()
return (
<Toolbar aria-label="Edit">
<ToolbarGroup aria-label="Clipboard">
<For each={clipboard}>
{(item) => (
<ToolbarButton
aria-label={item.label}
onClick={() => log(item.label)}
>
<item.icon />
</ToolbarButton>
)}
</For>
</ToolbarGroup>
<ToolbarSeparator />
<ToolbarGroup disabled aria-label="History">
<ToolbarButton aria-label="Undo">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M3 7v6h6" />
<path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" />
</svg>
</ToolbarButton>
<ToolbarButton aria-label="Redo">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M21 7v6h-6" />
<path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7" />
</svg>
</ToolbarButton>
</ToolbarGroup>
</Toolbar>
)
}
export { Example }Menus
Render a Menu trigger through ToolbarButton. In a horizontal toolbar, Down opens the menu while Left and Right keep moving between toolbar items.
<Menu>
<ToolbarButton render={(props) => <MenuTrigger {...props} />}>
Share
</ToolbarButton>
<MenuContent>…</MenuContent>
</Menu>import { Menu, MenuContent, MenuItem, MenuTrigger } from "@ui-sdk/solid/menu"
import { Toolbar, ToolbarButton } from "@ui-sdk/solid/toolbar"
import { useLog } from "./log"
function Example() {
const log = useLog()
return (
<Toolbar aria-label="Document">
<ToolbarButton onClick={() => log("Saved")}>Save</ToolbarButton>
<Menu onAction={(value) => log(`Shared by ${value}`)}>
<ToolbarButton render={(props) => <MenuTrigger {...props} />}>
Share
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m6 9 6 6 6-6" />
</svg>
</ToolbarButton>
<MenuContent>
<MenuItem value="email">Email</MenuItem>
<MenuItem value="link">Copy Link</MenuItem>
</MenuContent>
</Menu>
<ToolbarButton onClick={() => log("Printed")}>Print</ToolbarButton>
</Toolbar>
)
}
export { Example }Selects
A Select trigger joins the same way. It keeps its select behavior while it takes part in the toolbar's focus order.
import {
Select,
SelectContent,
SelectItem,
SelectItemIndicator,
SelectTrigger,
SelectValue,
} from "@ui-sdk/solid/select"
import { Toolbar, ToolbarButton } from "@ui-sdk/solid/toolbar"
import { createSignal, For } from "solid-js"
import { useLog } from "./log"
const zooms = ["50%", "75%", "100%", "150%", "200%"]
function Example() {
const log = useLog()
const [zoom, setZoom] = createSignal("100%")
const index = () => zooms.indexOf(zoom())
const change = (value: string) => {
setZoom(value)
log(`Zoom ${value}`)
}
return (
<Toolbar aria-label="View">
<ToolbarButton
aria-label="Zoom out"
disabled={index() === 0}
onClick={() => change(zooms[index() - 1])}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" x2="16.65" y1="21" y2="16.65" />
<line x1="8" x2="14" y1="11" y2="11" />
</svg>
</ToolbarButton>
<Select value={zoom()} onValueChange={(value) => value && change(value)}>
<ToolbarButton
render={(props) => <SelectTrigger {...props} />}
aria-label="Zoom"
>
<SelectValue />
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m7 15 5 5 5-5" />
<path d="m7 9 5-5 5 5" />
</svg>
</ToolbarButton>
<SelectContent>
<For each={zooms}>
{(zoom) => (
<SelectItem value={zoom}>
{zoom}
<SelectItemIndicator>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M20 6 9 17l-5-5" />
</svg>
</SelectItemIndicator>
</SelectItem>
)}
</For>
</SelectContent>
</Select>
<ToolbarButton
aria-label="Zoom in"
disabled={index() === zooms.length - 1}
onClick={() => change(zooms[index() + 1])}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" x2="16.65" y1="21" y2="16.65" />
<line x1="11" x2="11" y1="8" y2="14" />
<line x1="8" x2="14" y1="11" y2="11" />
</svg>
</ToolbarButton>
</Toolbar>
)
}
export { Example }Fields
Use ToolbarInput for a text field, or render another field through it, like a Number Input control. A field keeps Home, End and the arrow keys while they move its caret. At the matching end, with no text selected, an arrow moves to the next item.
Entering a field with an arrow puts its caret at the far end in that direction: press the same arrow again to leave, or the other arrow to edit. A number input keeps Up and Down for stepping its value.
import {
NumberInput,
NumberInputControl,
NumberInputDecrement,
NumberInputGroup,
NumberInputIncrement,
} from "@ui-sdk/solid/number-input"
import { Toolbar, ToolbarInput, ToolbarSeparator } from "@ui-sdk/solid/toolbar"
function Example() {
return (
<Toolbar aria-label="Text">
<NumberInput defaultValue={14} min={8} max={72}>
<NumberInputGroup>
<NumberInputDecrement aria-label="Decrease font size">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M5 12h14" />
</svg>
</NumberInputDecrement>
<ToolbarInput
render={(props) => (
<NumberInputControl {...props} aria-label="Font size" />
)}
/>
<NumberInputIncrement aria-label="Increase font size">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
</NumberInputIncrement>
</NumberInputGroup>
</NumberInput>
<ToolbarSeparator />
<ToolbarInput aria-label="Find" placeholder="Find…" />
</Toolbar>
)
}
export { Example }Links
ToolbarLink joins the same focus order and keeps its link behavior. The demo handles a plain click itself and leaves modified clicks, like opening in a new tab, to the browser.
import {
Toolbar,
ToolbarButton,
ToolbarLink,
ToolbarSeparator,
} from "@ui-sdk/solid/toolbar"
import { useLog } from "./log"
function Example() {
const log = useLog()
const follow = (event: MouseEvent & { currentTarget: HTMLAnchorElement }) => {
if (
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return
}
event.preventDefault()
log(`Opened ${event.currentTarget.textContent}`)
}
return (
<Toolbar aria-label="Pull request">
<ToolbarButton aria-label="Approve" onClick={() => log("Approved")}>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M20 6 9 17l-5-5" />
</svg>
</ToolbarButton>
<ToolbarButton aria-label="Comment" onClick={() => log("Comment")}>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
</svg>
</ToolbarButton>
<ToolbarSeparator />
<ToolbarLink href="#files" onClick={follow}>
Files Changed
</ToolbarLink>
<ToolbarLink href="#checks" onClick={follow}>
Checks
</ToolbarLink>
</Toolbar>
)
}
export { Example }Vertical
Set orientation="vertical" to move with Up and Down, like a canvas tool palette. Separators turn to run across the toolbar.
import { Toggle, ToggleGroup, ToggleIcon } from "@ui-sdk/solid/toggle"
import { Toolbar, ToolbarButton, ToolbarSeparator } from "@ui-sdk/solid/toolbar"
import { For } from "solid-js"
import { useLog } from "./log"
const icon = (markup: string) => () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
innerHTML={markup}
/>
)
const MousePointer2 = icon(
'<path d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z" />'
)
const Pencil = icon(
'<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /><path d="m15 5 4 4" />'
)
const Square = icon('<rect width="18" height="18" x="3" y="3" rx="2" />')
const Type = icon(
'<path d="M12 4v16" /><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" /><path d="M9 20h6" />'
)
const tools = [
{ value: "select", label: "Select", icon: MousePointer2 },
{ value: "draw", label: "Draw", icon: Pencil },
{ value: "shape", label: "Shape", icon: Square },
{ value: "text", label: "Text", icon: Type },
]
function Example() {
const log = useLog()
return (
<Toolbar orientation="vertical" aria-label="Canvas">
<ToggleGroup
defaultValue={["select"]}
deselectable={false}
orientation="vertical"
aria-label="Tool"
onValueChange={([value]) =>
log(`${tools.find((tool) => tool.value === value)?.label} tool`)
}
>
<For each={tools}>
{(tool) => (
<Toggle value={tool.value} aria-label={tool.label}>
<ToggleIcon>
<tool.icon />
</ToggleIcon>
</Toggle>
)}
</For>
</ToggleGroup>
<ToolbarSeparator />
<ToolbarButton
aria-label="Delete"
onClick={() => log("Deleted selection")}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M10 11v6" />
<path d="M14 11v6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
<path d="M3 6h18" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
</ToolbarButton>
</Toolbar>
)
}
export { Example }Disabled
A disabled item stays reachable with aria-disabled, so people can find it, but it ignores presses, including keys meant for a component rendered through it. Set focusableWhenDisabled to false to skip it instead.
import { Toolbar, ToolbarButton } from "@ui-sdk/solid/toolbar"
import { useLog } from "./log"
function Example() {
const log = useLog()
return (
<Toolbar aria-label="Message">
<ToolbarButton onClick={() => log("Reply")}>Reply</ToolbarButton>
<ToolbarButton disabled onClick={() => log("Archive")}>
Archive
</ToolbarButton>
<ToolbarButton
disabled
focusableWhenDisabled={false}
onClick={() => log("Delete")}
>
Delete
</ToolbarButton>
</Toolbar>
)
}
export { Example }Set disabled on the toolbar to disable every item. The toolbar keeps its tab stop, and its fields can't be edited.
<Toolbar disabled aria-label="Formatting">
<ToolbarButton>Bold</ToolbarButton>
<ToolbarInput aria-label="Find" />
</Toolbar>Loop
Arrow keys wrap from the last item to the first by default. Set loop to false to stop at the ends.
import { Toolbar, ToolbarButton } from "@ui-sdk/solid/toolbar"
import { For } from "solid-js"
import { useLog } from "./log"
function Example() {
const log = useLog()
return (
<Toolbar loop={false} aria-label="Pages">
<For each={["First", "Previous", "Next", "Last"]}>
{(label) => (
<ToolbarButton onClick={() => log(`${label} page`)}>
{label}
</ToolbarButton>
)}
</For>
</Toolbar>
)
}
export { Example }Controller
Create a controller with createToolbarController and pass it as controller. Its focus() moves focus to the item focused last, like the Alt+F10 shortcut editors give their toolbars. Create one controller per toolbar.
import { useShortcut } from "@ui-sdk/solid/shortcuts"
import {
createToolbarController,
Toolbar,
ToolbarButton,
} from "@ui-sdk/solid/toolbar"
import { For } from "solid-js"
import { useLog } from "./log"
const icon = (markup: string) => () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
innerHTML={markup}
/>
)
const Bold = icon(
'<path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" />'
)
const Italic = icon(
'<line x1="19" x2="10" y1="4" y2="4" /><line x1="14" x2="5" y1="20" y2="20" /><line x1="15" x2="9" y1="4" y2="20" />'
)
const Link = icon(
'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />'
)
const actions = [
{ label: "Bold", icon: Bold },
{ label: "Italic", icon: Italic },
{ label: "Insert link", icon: Link },
]
function Example() {
const log = useLog()
const toolbar = createToolbarController()
useShortcut("alt+f10", () => toolbar.focus(), { allowInInput: true })
return (
<div data-slot="stack">
<div data-slot="controls">
<button type="button" onClick={() => toolbar.focus()}>
Focus Toolbar
</button>
</div>
<Toolbar controller={toolbar} aria-label="Notes formatting">
<For each={actions}>
{(action) => (
<ToolbarButton
aria-label={action.label}
onClick={() => log(action.label)}
>
<action.icon />
</ToolbarButton>
)}
</For>
</Toolbar>
<textarea aria-label="Notes" placeholder="Type, then press Alt+F10" />
</div>
)
}
export { Example }Custom Rendering
Render a component through ToolbarButton or ToolbarInput to combine their behavior, as the menu, select and number input examples do. A render function must spread the toolbar props onto the inner component. Parts and Composition explains the rendering contract.
Styling
Use data-orientation on the root for a row or a column, and on a separator for the way its line runs, across the toolbar. Use data-disabled to style unavailable items and groups. Toggles inside keep their own data-pressed.
Each part has a data-slot, such as toolbar-button, and its state attributes are listed under Data Attributes.
Data Attributes
Toolbar
[data-slot="toolbar"]
| Attribute | What it means |
|---|---|
data-orientation | horizontalverticalThe toolbar's orientation. On a separator, which way the line runs: across the toolbar. |
data-disabled | Present when disabled, by the item itself, its group or the toolbar. |
ToolbarButton
[data-slot="toolbar-button"]
| Attribute | What it means |
|---|---|
data-disabled | Present when disabled, by the item itself, its group or the toolbar. |
data-orientation | horizontalverticalThe toolbar's orientation. On a separator, which way the line runs: across the toolbar. |
ToolbarLink
[data-slot="toolbar-link"]
| Attribute | What it means |
|---|---|
data-disabled | Present when disabled, by the item itself, its group or the toolbar. |
data-orientation | horizontalverticalThe toolbar's orientation. On a separator, which way the line runs: across the toolbar. |
ToolbarInput
[data-slot="toolbar-input"]
| Attribute | What it means |
|---|---|
data-disabled | Present when disabled, by the item itself, its group or the toolbar. |
data-orientation | horizontalverticalThe toolbar's orientation. On a separator, which way the line runs: across the toolbar. |
ToolbarGroup
[data-slot="toolbar-group"]
| Attribute | What it means |
|---|---|
data-disabled | Present when disabled, by the item itself, its group or the toolbar. |
data-orientation | horizontalverticalThe toolbar's orientation. On a separator, which way the line runs: across the toolbar. |
ToolbarSeparator
[data-slot="toolbar-separator"]
| Attribute | What it means |
|---|---|
data-orientation | horizontalverticalThe toolbar's orientation. On a separator, which way the line runs: across the toolbar. |
API Reference
Props are the same as below, as Solid props.
Toolbar
orientation?ToolbarOrientation- Which arrow keys move between items: left and right, or up and down.
loop?boolean- Whether arrow keys wrap from the last item to the first.
disabled?boolean- Stops every item from being used. Items stay focusable with
aria-disabled, so the toolbar keeps its tab stop and stays discoverable. controller?{ focus: () => void; }- Focuses the toolbar from anywhere. Create one with
createToolbarController(). debug?boolean- Logs every state change with its reason to the console, in development builds.
ToolbarButton
disabled?boolean- Stops the item from being used. It stays focusable with
aria-disabled, so arrow keys still reach it and a tooltip can say why. focusableWhenDisabled?boolean- Whether a disabled item stays reachable with arrow keys. Turn it off to skip it, natively disabled.
ToolbarInput
disabled?boolean- Stops the item from being used. It stays focusable with
aria-disabled, so arrow keys still reach it and a tooltip can say why. focusableWhenDisabled?boolean- Whether a disabled item stays reachable with arrow keys. Turn it off to skip it, natively disabled.
ToolbarGroup
disabled?boolean- Disables every item in the group. They stay focusable with
aria-disabled.
ToolbarLink and ToolbarSeparator take no props of their own.
State
What render functions and state-based class and style receive on ToolbarButton and ToolbarInput, and children on ToolbarButton:
disabledboolean- Whether the item is disabled, by itself, its group or the toolbar.
orientationToolbarOrientation- The toolbar's
orientationoption:"vertical", or"horizontal"for anything else.
On ToolbarLink:
disabledboolean- Whether the link is disabled, by its group or the toolbar.
orientationToolbarOrientation- The toolbar's
orientationoption:"vertical", or"horizontal"for anything else.
On ToolbarSeparator:
orientationToolbarOrientation- Which way the line runs: across the toolbar, so vertical in a horizontal toolbar.
Controller
focus()() => void- Moves focus to the item that holds the toolbar's tab stop: the one focused last, or the first.
Accessibility
The toolbar follows the WAI-ARIA toolbar pattern. The root has the toolbar role and aria-orientation, and separators have the separator role.
Keyboard
The toolbar has one tab stop, a roving focus. Tab enters on the first enabled item, then returns to the item focused last. The next Tab leaves the toolbar.
| Key | Action |
|---|---|
ArrowRight / ArrowLeft | Move focus in a horizontal toolbar |
ArrowDown / ArrowUp | Move focus in a vertical toolbar |
Home / End | Focus the first or last item, except in a field |
Enter / Space | Activate a button or toggle, or open its popup |
Controls keep the keys they use: Down opens a menu, and Up and Down step a number input. Moving focus never presses a toggle. In right-to-left text, Left and Right swap.
Tested Behavior
Every line below is an automated test.
- The toolbar is named and has a horizontal orientation.
- Tab lands on the first item, and Tab again leaves the toolbar.
- Shift+Tab from after the toolbar lands on its tab stop.
- ArrowRight moves through every item, into toggle groups and out of fields.
- ArrowRight on the last item wraps to the first, and ArrowLeft back.
- Arrow keys never press a toggle, and Space and Enter do.
- Tab comes back to the item focused last.
- A toggle group in the toolbar is no tab stop of its own.
- A disabled button stays reachable, says it's disabled and does nothing.
- A button runs its action when pressed.
- A menu trigger rendered through a button opens with ArrowDown, and arrows still move past it.
- Choosing from a menu in the toolbar returns focus to its trigger.
- A text field keeps Home and End for its caret.
- Up and Down step a number field in the toolbar.
- Separators run across the toolbar.
- A group of buttons is named.
- A link in the toolbar follows its href with Enter.
- With loop off, arrows stop at the ends.
- A disabled toolbar keeps one tab stop, and every item says it's disabled.
- ArrowDown and ArrowUp move between items, and Left and Right don't.
- Separators run across a vertical toolbar.
- Toggles and toggle groups in a disabled group are disabled.
- A link in a disabled group is disabled, and follows its href once enabled.
- Enabling a group enables the toggles and buttons in it.
Runs in React, Vue and web components.