Toolbar
Free5 visual variants (default, glass, bordered, elevated, ghost) and 3 sizes, with grouped action buttons, separators and an actionClick output.
import { SvToolbarComponent } from '@styloviz/layout';Demo & usage
Basic usage
Usage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Grouped, sticky
<sv-toolbar [groups]="toolbarGroups" variant="elevated" [sticky]="true" (actionClick)="run($event)" />
Ghost (embedded in a card/header)
<sv-toolbar [groups]="toolbarGroups" variant="ghost" size="sm" (actionClick)="run($event)" />
Overflow menu
<!-- Actions that do not fit move into a "More" menu, from the end of the list --> <sv-toolbar [actions]="actions" overflow="menu" overflowLabel="More editing actions" (actionClick)="run($event)" />
When to use
- Hosting a row of actions above content: editors, tables, document views.
- You want grouped icon buttons with separators, toggle state and keyboard navigation.
- For a floating single primary action, use the Speed Dial.
Async states
- loading
- error
- retry
Item identity & owned state
- Restorable state
- activeIds
- busyIds
Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their
Changeevents to store what the user does.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| groups | readonly ToolbarGroup[] | [] | Grouped actions rendered with separators between groups. |
| actions | readonly ToolbarAction[] | [] | Flat action list (alternative to `groups`). |
| size | ToolbarSize | 'md' | Toolbar size: sm, md or lg. |
| variant | ToolbarVariant | 'default' | Visual variant: default, glass, bordered, elevated or ghost. |
| label | string | 'Toolbar' | Accessible label for the toolbar region. |
| sticky | boolean | false | Stick the toolbar to the top while scrolling. |
| floatingAnchor | SvFloatingAnchor | null | null | Opt in to the shared floating layer: register the toolbar strip as an occupied region on this edge or corner, so kit controls there (Scroll to Top, a floating Speed Dial) and host regions stack clear of it. For a `sticky` toolbar, or one the host pins `position: fixed` — the component cannot tell where the host's CSS puts it, so it never guesses. Registers at `SV_FLOATING_ORDER.edge` (nearest the edge). `null` registers nothing and the rendered output is unchanged. |
| overflow | ToolbarOverflowMode | 'scroll' | What happens to actions that do not fit: `scroll` keeps them inline in a horizontally scrolling strip; `menu` measures the available width and moves the trailing actions into a keyboard-accessible "More" menu (a body-portal `role="menu"`). In `menu` mode the host element becomes `display: block` so it can measure the width its parent allows. |
| overflowLabel | string | 'More actions' | Accessible name (and tooltip) of the "More" button and its menu. |
| loading | boolean | false | True while the action set is being fetched. A toolbar whose actions depend on the user's permissions is fetched, not hard-coded. |
| loadError | SvDataError | string | null | null | The definition fetch failed — the toolbar has no actions to show. Separate from `error`, which is a failure of an action the user just TRIGGERED. The two need opposite affordances: a failed action is retried by clicking that action again, while a failed load has nothing left to click. Collapsing them into one input gave the action strip a "Try again" button that could not know which action to re-run. |
| busyIds | readonly string[] | ReadonlySet<string> | [] | Ids of actions with a request in flight. Those controls disable and show a busy state, so a double-click cannot fire the same request twice — the most common way an action button creates duplicate records. |
| error | SvDataError | string | null | null | Failure from the last action. Rendered above the control group; nothing is removed from view, so the user can see what state things are in. |
| activeIds | ReadonlySet<string> (two-way) | new Set() | Pre-seeded set of active toggle IDs. Two-way bindable. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| retryLoad | void | The host should re-run the failed definition fetch. Named `retryLoad`, matching `sv-agent-task-queue`: on a component where `error` already means something else, the load states carry the `…Load` spelling rather than stealing a name that is already taken. |
| actionClick | string | Emitted with the id of the clicked action. |
| activeIdsChange | ReadonlySet<string> | Emits the new `activeIds` whenever it changes — the output half of `[(activeIds)]`. |
Wire it to your API
This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.
Template
Bound to the names this component actually exposes.
<sv-toolbar [groups]="data.items()" [loading]="data.loading()" [error]="data.error()" (retryLoad)="data.reload()" />
Host class
Drive the three async inputs from your own fetch.
// This component reports its state but does not emit a query, so drive // it from your own fetch and bind the three async inputs. readonly items = signal<Row[]>([]); readonly loading = signal(false); readonly error = signal<SvDataError | null>(null); async load(): Promise<void> { this.loading.set(true); this.error.set(null); try { this.items.set(await this.api.fetchRows()); } catch (e) { this.error.set(toSvDataError(e)); } finally { this.loading.set(false); } }
- This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `groups`, `loading` and `error`.
- The load-level retry is `retryLoad` here, because `retry` already means a per-item action on this component.
- Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.
Full walkthrough: Connecting to a backend.
Customization & theming
- Five visual variants incl. a chrome-less `ghost` for embedding in an existing card or header.
- Three sizes: sm / md / lg.
- Grouped actions with separators.
- Toggle buttons with two-way `activeIds`.
- Per-action color accents: primary, danger, success.
- Responsive: an action-heavy toolbar scrolls horizontally when it can't fit (no scrollbar while it does), so it never overflows the viewport on small screens.
- Overflow menu mode: `overflow="menu"` measures the width the parent allows and moves the trailing actions into a "More" menu button (`overflowLabel` names it). Order the actions by importance — the end of the list overflows first. The default, `overflow="scroll"`, is unchanged.
- Variant axes (X-8): `variant`, `size` and each action's `variant` are APPEARANCE and combine freely. `overflow`, `sticky`, toggles (`activeIds`) and `busyIds` are BEHAVIOURAL modes. Known limits: in `overflow="menu"` the host becomes `display: block` to measure its parent; a `sticky` toolbar's overflow menu is a body portal that re-anchors on scroll but does not itself stick; `loading` replaces the strip, so there is nothing to overflow.
- Floating layer (X-5, `floatingAnchor`): a `sticky` or host-fixed toolbar can register its strip as an occupied region — usually `'top'` or `'bottom'` — so Scroll to Top, a floating Speed Dial and host regions on that side stack clear of it. Opt-in because the toolbar cannot tell where the host's CSS puts it; `null` (default) registers nothing and renders exactly as before.
Best practices
- Do: Group related actions and order them by frequency of use.
- Do: Make the toolbar `sticky` when it controls a long scrollable area.
- Tip: Use the `ghost` variant with `size="sm"` when embedding the toolbar inside a card or panel header that already has its own border.
- Do: Use `overflow="menu"` for a toolbar in a resizable pane or on mobile, and put the most important actions first — the end of the list moves into "More" first.
- Don't: Don't give two actions (or two groups) the same id — `actionClick` would emit the same value for both.
- Tip: Scroll roots and portals (X-9): `sticky` is CSS `position: sticky`, so it sticks within the nearest scrolling ancestor — an ancestor with `overflow: auto/hidden` becomes the scroll root, and a transformed ancestor does not break it. The overflow menu renders in a `<body>` portal, so no ancestor `overflow` or `transform` can clip it; it carries the origin's `dir`, `lang` and theme marker, re-anchors on any ancestor scroll and closes on a viewport resize.
- Tip: Platform limits (X-11): overflow measurement uses `ResizeObserver` (every evergreen browser). Where it is missing, or on the server, the split is decided from a single measurement — and an unmeasured toolbar (SSR, hidden at init) shows every action inline rather than collapsing them all. The split is computed from the kit's button sizes, so a host that restyles button widths should keep to the `size` scale.
- Tip: Forced colours (X-7/X-11): the focus ring and the active accent are a box-shadow and a tint, both of which Windows forced colours removes — so every button and overflow-menu item draws a real system-colour outline on keyboard focus, a pressed/active toggle gets a `Highlight` border, and the group separators paint `CanvasText`. The five visual variants collapse to the same system palette there by design.
Accessibility
- Exposes a labelled `role="toolbar"` with labelled buttons; toggles report `aria-pressed`.
- Forced colors (X-7): focus is a box-shadow ring, which forced colors removes, so every action, the More button and every menu item also draws a system-coloured focus OUTLINE in that mode only. An active or pressed tool keeps a Highlight border.
- Overflow menu layering: the menu takes its z-index from the shared overlay stack (above page chrome, below a dialog opened from it), handles Escape only while it is the topmost overlay, and treats a click inside a surface opened above it as inside. If the menu item holding focus is removed or turns busy while the menu is open, focus moves to the item now in its place, else the one before it, else the menu itself; if the menu empties, to the toolbar's tab stop — never `<body>`.
- Single tab stop with roving tabindex; Arrow keys, Home and End move between buttons (ARIA toolbar pattern). The tab stop follows a click too, so Tab returns to the action last used.
- Roving focus is scoped to each toolbar's own host, so two toolbars sharing action ids never steal focus from each other.
- Focus after change (X-10): when the action holding the tab stop is disabled or removed, the stop moves to its successor (the action now in its place, else the one before it). If that action held keyboard focus, focus moves there too; with nothing focusable left it goes to the toolbar itself — never `<body>`. A toolbar the user has already left does not pull focus back.
- Overflow menu: the More button is part of the roving tab stop and exposes `aria-haspopup="menu"` / `aria-expanded`; Down or Enter opens it on the first item, Up on the last. Items are `menuitem` (toggles are `menuitemcheckbox` with `aria-checked`); Arrow keys, Home and End move, Escape closes and returns focus to More, Tab closes and continues past the toolbar.
- Action ids must be unique: a duplicate logs a dev-mode warning, because `actionClick` emits the id and the host could not tell the two apart.
Feedback on Toolbar
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.