Skip to main content

ViewToolbar

Layoutlayout

The row above a list, table or board — status and filters on the left, actions on the right, every control its own tab stop.

import { ViewToolbar } from "@elabs-ai/components-ui";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui ViewToolbar component (from @elabs-ai/components-ui) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look ViewToolbar up before writing code: `npx -y @elabs-ai/components-cli docs ViewToolbar` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-ui is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Avoid

  • Giving it role=toolbar — it deliberately does not claim that role; a dense row that should cost ONE tab stop with arrow-key roving is Toolbar.
  • Putting the primary page action in it — this row acts on the VIEW; a page-level action belongs in the page header.
  • Hiding the result count while filters are applied — the count is how a person tells an empty filter from an empty dataset.

Works with

Contains

Avoid next to

Examples

9 live examples

Actions Only

The minimum viable row: actions only. No info, no filters, no count — the row still holds its `min-h-9` floor so it lines up with a populated sibling view.

No Filters Applied

**No filters applied** — the filters region is ABSENT, not an empty box, and the count is a bare total ("128 runs"), because "128 of 128" is noise.

Empty Results

**Empty results.** The count still tells the truth ("0 of 128") and the chips STAY, so the filter that emptied the view is the thing the user can undo. The toolbar does not own the empty state itself — that is a `StatePanel` below it.

Result Count Loading

**Not ready.** `ResultCount loading` renders a layout-shaped placeholder instead of a "0" that lies while the first page is in flight, and announces the wait once at the region rather than per skeleton box.

Overflowing

**Overflowing content at a narrow width.** Long chip labels truncate, the chip run wraps inside the filters group, and the two clusters stack instead of the left one being squeezed to nothing. The play function asserts the AC directly: the last action is still inside the toolbar's box, and nothing in the row clips it.

Keyboard Removal

**Keyboard removal (AC).** The whole chip is one `<button>`, so Enter and Space both remove it and there is exactly one tab stop per active filter.

Filter Chip With Trailing

`FilterChip`'s `trailing` slot (#284) — a short, non-shrinking secondary text segment (a count) that CSS truncation can never reach, unlike `label`. In a narrow container the label truncates with an ellipsis; the count stays fully visible in its own element.

Dark Theme

The same row on `dark`.

API

ViewToolbar

PropTypeDescription
infoReactNodeWhat this view is, as a tooltip on an ⓘ trigger at the start of the row — so a view does not need an in-page `<h1>` + paragraph block just to explain itself. Omit it when the surrounding page already says what the view is. **Pointer/keyboard only.** A Radix tooltip opens on hover and on keyboard focus but deliberately NOT on tap (and Radix suppresses the focus-open when focus arrived from a pointer), so on a touch device this text is unreachable. Treat `info` as progressive enhancement: never put something a phone user cannot do without in it. See `Layout/ViewToolbar → Contract`.
childrenReactNodeLeft cluster — status/context, `ViewToolbarFilters`, `ResultCount`.
actionsReactNodeRight cluster — the view's actions. Stays reachable at every width.

Also accepts: HTMLAttributes<HTMLDivElement>

ViewToolbarFilters

PropTypeDescription
onClearAll() => voidClear every active filter. Supplying it renders the "clear all" affordance at the end of the chip run; omit it when a view has no bulk reset.

Also accepts: HTMLAttributes<HTMLDivElement>

FilterChip

PropTypeDescription
labelrequiredstringLabel-in-value text — `"Status: Failed"`, never `"Status = failed"` and never a bare `"Failed"`. The caller composes the string so the chip reads as a sentence fragment out of context (and in a screen reader).
trailingstringShort, non-shrinking secondary text — a count, a unit (`"excluded 1,204"`). A second text slot rather than folding it into `label` (#284): `label` is the shrinkable, safe-to-truncate part; `trailing` is short and fixed-width and must never lose characters to the ellipsis. Same shape as `TurnStatus`'s and `TerminalWorking`'s `trailing` slot — a fixed-width companion beside a truncating label. Composed into the accessible name after `label` (`"Remove filter: <label> · <trailing>"`), so the byte- identical name from before this slot existed is unchanged. Trade-off: `trailing` is `shrink-0`, so an extremely narrow container now truncates `label` down to nothing before it touches `trailing`. That is the right default — the count is short and fixed, the label is the long, prose-shaped part — but it is a choice, not a free win.
onRemoverequired() => voidRemove this filter. The WHOLE chip is the remove control — see below.

Also accepts: Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children">

ResultCount

PropTypeDescription
countrequirednumberHow many rows/cards the view is showing right now.
totalnumberThe unfiltered total. Supplying it renders "N of M" — the honest reading when a filter is active. Omit it for an unfiltered view's bare count.
loadingbooleanNo count yet. Renders a layout-shaped placeholder instead of a `0` that lies while the data is in flight (see `.claude/rules/loading-states.md`).
childrenReactNodeOptional trailing noun ("results", "runs", "rows") joined with a non-breaking space, so the number is never announced bare.

Also accepts: HTMLAttributes<HTMLSpanElement>