Toolbar
A floating, always-icon-only rail of actions docked to a viewport edge — for a persistent quick-actions dock, the way an editor's formatting bar or an app's activity rail behaves.
Usage#
import { Separator, Toolbar, ToolbarButton } from "neelam-ui";
import { MessageCircle, Plus, Settings } from "lucide-react";
<Toolbar label="Quick actions">
<ToolbarButton icon={<Plus />} label="New" />
<ToolbarButton icon={<MessageCircle />} label="Comments" />
<Separator className="mx-0.5" />
<ToolbarButton icon={<Settings />} label="Settings" />
</Toolbar>label is required on Toolbar — role="toolbar" has no name-from-content the
way a <button> does, so it needs one explicitly. ToolbarButton's own label
does double duty: it's both the button's accessible name and the text shown in
its tooltip.
Not a Sidebar variant#
Toolbar is a separate component from Sidebar
rather than another collapsible mode of it, because the two differ in ways
that go past width:
Sidebaris a flex sibling of your page's main content —SidebarProviderrenders the shared row wrapper, and<main>makes room for it.Toolbarisposition: fixed, floating over content; it never participates in page layout at all.Sidebar's icon rail (collapsible="icon") is one state a normally-wide panel can collapse into.Toolbarhas no open/collapsed state whatsoever — it's always icon-only.Sidebar's menu is just links in ordinary Tab order.Toolbarimplements the WAI-ARIA Toolbar pattern, a genuinely different keyboard model — see below.
Docking to an edge#
side — "left" (default), "right", "top", or "bottom" — picks which
viewport edge to float against. Orientation follows directly from the edge
rather than a separate setting: "left"/"right" render a vertical rail,
"top"/"bottom" a horizontal one.
Fixed to the viewport
Toolbar renders with position: fixed, so in a real app it's typically
mounted once, near your root — not re-created per page. The demos on this
page override that to absolute (via className) purely so they stay
inside their preview card instead of docking to the actual page edge.
Grouping items#
There's no dedicated ToolbarSeparator — compose the existing
Separator directly, the same component a
vertical toolbar divider anywhere else in the library already uses.
<Toolbar label="Formatting" side="top">
<ToolbarButton icon={<Bold />} label="Bold" />
<Separator orientation="vertical" className="my-0.5" />
<ToolbarButton icon={<Underline />} label="Underline" />
</Toolbar>For a vertical rail, use the default horizontal Separator instead (it
stretches across the rail's width); a horizontal rail wants
orientation="vertical" (it stretches to the rail's height).
Not limited to ToolbarButton#
The roving-focus mechanism below isn't special-cased to ToolbarButton — it
manages any enabled <button> descendant it finds. Composing in a
Toggle for a pressable item (bold/italic-style)
still participates for free:
<Toolbar label="Formatting" side="top">
<Toggle aria-label="Bold" pressed={bold} onPressedChange={setBold}>
<Bold className="h-4 w-4" />
</Toggle>
<ToolbarButton icon={<Italic />} label="Italic" />
</Toolbar>Keyboard#
| Key | Behaviour |
|---|---|
| Tab | Moves to the toolbar as one stop — only one item is in the page's Tab sequence at a time. |
| →↓ | Moves focus to the next item. Left/Right for a horizontal toolbar (side="top"/"bottom"), Up/Down for a vertical one. |
| ←↑ | Moves focus to the previous item, matching the toolbar's orientation the same way. |
| HomeEnd | Jumps to the first / last item. |
| EnterSpace | Activates the focused button. |
This is the WAI-ARIA Toolbar pattern's roving tabIndex: exactly one item is
ever tabIndex={0}, so Tab moves past the whole toolbar in a single stop and
arrow keys move between items inside it — the same pattern
Calendar and
Context Menu use for their own roving
focus, applied here to a button group.
Accessibility#
- The container is
role="toolbar"witharia-labelset from yourlabelprop, andaria-orientationset automatically fromside. ToolbarButtonsetsaria-labelfrom its ownlabelprop — required, since an icon-only button has no accessible name from its content alone.- The
Tooltipshowing that samelabelwraps the real<button>itself viaTooltipTrigger'sasChild(see Tooltip), rather than an extra<span tabIndex={0}>around it — so it stays the one and only focusable node for the control, not two redundant tab stops. - A
disabledToolbarButtonis skipped by arrow-key navigation, the same as adisabledcell inCalendar's grid.
It's still just a button underneath
ToolbarButton renders a real <button>, so anything that already works on
a button — disabled, onClick, form, a ref — works here unchanged.
API reference#
Toolbar#
| Prop | Type | Required | Default |
|---|---|---|---|
labelAccessible name for the `role="toolbar"` landmark, e.g. `"Formatting"` or `"Quick actions"` — required, since a toolbar (unlike a `<button>`) has no name-from-content. | string | Yes | — |
sideWhich viewport edge to float against. `"left"`/`"right"` render a vertical rail, `"top"`/`"bottom"` a horizontal one — orientation follows directly from the edge rather than a separate setting, since a combination like a horizontal bar hugging the left edge isn't a real layout anyone wants. Defaults to `"left"`. | enum | — | left |
ToolbarButton#
| Prop | Type | Required | Default |
|---|---|---|---|
iconThe button's icon — required, since a `Toolbar` item is icon-only by design (see `Toolbar`'s own doc). | ReactNode | Yes | — |
labelBoth the button's accessible name and the text shown in its `Tooltip` on hover/focus. | string | Yes | — |