List Toolbar
The one header row of every list and table — tabs left; search, icon tools and the main button right — collapsing in a fixed, measured order.
- Status
- Since
0.25.100- Accessibility pattern
- named toolbar; tools named with state; / and F shortcuts
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for List Toolbar before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify list-toolbarInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/list-toolbarRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button, @vegastack/dropdown-menu, @vegastack/popover, @vegastack/search-input, @vegastack/sheet, @vegastack/toggle-group, @vegastack/tooltip, @vegastack/use-media-query, @vegastack/page-layout.
Usage
import { Plus } from "lucide-react";
import { AddFilterMenu } from "@/components/ui/add-filter-menu";
import { DisplayMenu } from "@/components/ui/display-menu";
import { FilterRow } from "@/components/ui/filter-row";
import {
ListToolbar,
ListToolbarAction,
ListToolbarMenu,
} from "@/components/ui/list-toolbar";
import { SortMenu } from "@/components/ui/sort-menu";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
<ListToolbar
tabs={
<Tabs value={scope} onValueChange={setScope}>
<TabsList size="sm" overflow="menu" aria-label="Whose tasks">
<TabsTrigger value="mine">My tasks</TabsTrigger>
<TabsTrigger value="all">All</TabsTrigger>
</TabsList>
</Tabs>
}
search={{
value: query,
onValueChange: setQuery,
placeholder: "Search tasks",
}}
filterRow={
<FilterRow
addFilter={<AddFilterMenu fields={fields} onPick={addFilter} ai={ai} />}
facets={facets}
chips={chips}
onClear={clearAll}
/>
}
sort={{
content: (
<SortMenu
fields={sortFields}
value={sort}
defaultValue={defaultSort}
onValueChange={setSort}
/>
),
isDefault,
summary: sortSummary(sortFields, sort),
}}
layout={{
value: layout,
options: ["list", "board"],
onValueChange: setLayout,
}}
display={
<DisplayMenu
toggles={showToggles}
isDefault={displayDefault}
onReset={resetDisplay}
/>
}
more={<ListToolbarMenu items={viewActions} />}
action={
<ListToolbarAction icon={<Plus />} label="New task" onClick={create} />
}
/>;Every list, table and settings table gets the same row under its title. The left zone holds the
one thing that says which set: scope Tabs, RouteTabs or a table's ViewTabs. The right
zone holds, in this order, a state button (state, a table's "Save view"), the edit group
(edit, undo/redo, then a divider), the search, the tool cluster — Filter · Sort · Layout ·
Display · ⋯ — and the page's main button (action). Display and ⋯ sit at the far end, so a tool
that hides (Sort on a board) never moves them.
A tool renders only when the page passes something for it to do: no sort on a board, no layout
where there is one layout, no ⋯ when ListToolbarMenu has no items. Nothing is ever shown disabled
as a placeholder. Tools are 32px borderless icon buttons, 4px apart, muted until hovered or open,
each with a tooltip and an accessible name that carries its state: Filter shows its count ("Filter,
2 active"), Sort and Display show a 6px dot when they differ from the page's default.
The filter row (filterRow, a FilterRow) opens under the row from
the Filter tool, 8px below it. It stays mounted while closed, so its facets keep reporting, and it
tells the Filter tool how many filters are applied. Opened with nothing applied, its
AddFilterMenu opens too, with focus in its input.
filter.storageKey remembers the open state per page.
The main button is a ListToolbarAction: the one filled button on the row,
labelled while the toolbar is wide ("+ New task") and an icon + with the label as its name and
tooltip once the tabs have folded. Pass it as a menu trigger's render for "New ▾".
Scope
| Behaviour | Where it lives |
|---|---|
| Which tools a page has, their values and defaults | Host |
| Measuring the row and the collapse order | ListToolbar |
| Folding scope tabs into a dropdown | TabsList overflow="menu" (also RouteTabs) |
| Sort and Layout inside Display once folded | ListToolbar → DisplayMenu (context) |
| Undo/redo inside ⋯ once folded | ListToolbar → ListToolbarMenu (context) |
| Filter counts | FilterRow (or filter.count) |
| Turning a plain-language request into filters (AI) | Host — AddFilterMenu ai.onSubmit |
| Bottom sheets on a phone | ListToolbarPopover |
Anatomy
Examples
At five widths
The toolbar measures its own width, not the viewport, and gives way one step at a time: the search shrinks to 144px, the tabs fold to a dropdown, Layout moves into Display (the search can go to 96px), Sort moves into Display, the search becomes an icon, and a table's undo/redo move into ⋯. Filter, Display, ⋯ and the main button never go, and the row never wraps.
A table with saved views
ViewTabs on the left with a dot on a changed view, "Save view" as the state button, undo/redo
as the edit group, a search that starts as an icon (collapsible), a multi-key SortBuilder
showing its key count, and Fields in Display.
A settings table
No tabs: the left zone stays empty and the tools sit right. No Display, so Sort never folds.
Compact, inside a record tab
size="compact" is the toolbar of a short list inside a record's tab: 28px tools, no shortcuts,
and the main button may be a ghost "+ New task".
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
action | React.ReactNode | — | The page's main button, the last item of the row — a ListToolbarAction: filled, labelled
on a wide toolbar, an icon on a narrow one. |
aria-label | string | "List controls" | The toolbar's accessible name. |
display | React.ReactNode | — | The Display tool — a DisplayMenu. |
edit | readonly ListToolbarMenuItem[] | — | Edit tools (a table's Undo, Redo): icon buttons with a divider after them. They move into ⋯ last of all. |
filter | ListToolbarFilter | — | The Filter tool. Shown when this or filterRow is given. |
filterRow | React.ReactNode | — | The row under the toolbar that the Filter tool opens — a FilterRow. |
layout | ListToolbarLayout<ListToolbarLayoutValue> | — | The Layout tool. Omit it where a page has one layout. |
more | React.ReactNode | — | The ⋯ tool — a ListToolbarMenu of actions on the view. |
search | ListToolbarSearch | — | The search field, right of the tabs and before the tools. |
shortcuts | boolean | true (false when `size="compact"`) | / focuses the search and F opens the filter row, unless focus is in a field. |
size | "compact" | "default" | "default" | compact is the toolbar of a short list inside a record tab: 28px tools, no shortcuts. |
sort | ListToolbarSort | — | The Sort tool. Omit it where order is fixed (a board). |
state | React.ReactNode | — | A state button before the edit group and the search ("Save view" on a changed table view). It leaves the row at the last collapse step, so keep its action in ⋯ too. |
tabs | React.ReactNode | — | The left zone: scope Tabs (TabsList size="sm" overflow="menu"), RouteTabs (overflow="menu")
or a table's ViewTabs. At most one. Omitted, the tools sit right. |
Data attributes and CSS variables on ListToolbar
| Attribute | Values |
|---|---|
data-layout | mirrors a prop or state value |
data-level | mirrors a prop or state value |
data-size | mirrors a prop or state value |
data-slot | "list-toolbar" | "list-toolbar-action" | "list-toolbar-cluster" | "list-toolbar-edit" | "list-toolbar-filter" | "list-toolbar-filter-row" | "list-toolbar-layout" | "list-toolbar-row" | "list-toolbar-search" | "list-toolbar-sort" | "list-toolbar-sort-content" | "list-toolbar-state" | "list-toolbar-tabs" | "list-toolbar-tools" |
data-state | "closed" | "open" |
Search
| Prop | Type | Default | Description |
|---|---|---|---|
onValueChange* | (value: string) => void | — | Called on every keystroke. |
value* | string | — | The query. |
aria-label | string | placeholder | The field's accessible name. |
collapsible | boolean | false | Start as an icon at every width (a table, where the grid's own header is the focus). |
debounceMs | number | 300 | Debounce for onValueCommitted, in ms. |
onValueCommitted | ((value: string) => void) | — | Called with the settled query (after the debounce, on Enter, on clear) — send the query here. |
placeholder | string | "Search" | The field's placeholder ("Search tasks"). |
Filter
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | — | The number of filters applied. Omitted, the FilterRow counts its own chips and set facets. |
defaultOpen | boolean | false | Whether the row starts open (uncontrolled). |
label | string | "Filter" | The tool's name. |
onOpenChange | ((open: boolean) => void) | — | Called when the row opens or closes. |
open | boolean | — | Whether the filter row is open (controlled). |
storageKey | string | — | Remember the open state for this page in sessionStorage under this key. |
Sort
| Prop | Type | Default | Description |
|---|---|---|---|
content* | React.ReactNode | — | The popover's body: a SortMenu (one key) or a SortBuilder (several). |
count | number | — | How many keys are set (multi): shown on the tool instead of the dot. |
isDefault | boolean | true | Whether the order is the page's default. false puts a dot on the tool. |
label | string | "Sort" | The tool's name. |
summary | React.ReactNode | — | One line that says the order ("Due date, oldest first") — Display's row once Sort folds into it. |
variant | "multi" | "single" | "single" | single (a SortMenu, default-width popover) or multi (a SortBuilder, wider). |
Layout
| Prop | Type | Default | Description |
|---|---|---|---|
onValueChange* | (value: L) => void | — | Called with the layout picked. |
options* | readonly L[] | — | The layouts offered, in order (two or three). |
value* | L | — | The current layout. |
labels | Partial<Record<ListToolbarLayoutValue, string>> | — | Relabel a layout ("Cards" for "Grid"). |
ListToolbarButton
| Prop | Type | Default | Description |
|---|---|---|---|
icon* | React.ReactNode | — | The tool's icon (16px). |
label* | string | — | The tool's name: its tooltip and the start of its accessible name. |
count | number | — | A count shown after the icon ("▽2") — filters applied, sort keys. Nothing at 0. |
dot | boolean | false | A 6px dot on the icon: the tool is not at the page's default. |
loading | boolean | — | Shows a spinner over the label, blocks activation and sets aria-busy. The label keeps its
box at opacity: 0, so the button's width does not move and its accessible name survives
(API-5, A11Y-12). |
shortcut | string | — | The keyboard shortcut shown in the tooltip ("F"). |
Data attributes and CSS variables on ListToolbarButton
| Attribute | Values |
|---|---|
data-dot | "" |
data-slot | "list-toolbar-count" | "list-toolbar-dot" |
ListToolbarPopover
| Prop | Type | Default | Description |
|---|---|---|---|
children* | React.ReactNode | — | The body. |
title* | string | — | The surface's name: the sheet's title on a phone, the popover's accessible name. |
trigger* | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | — | The trigger — usually a ListToolbarButton. |
align | "center" | "end" | "start" | "end" | The popover's alignment to its trigger. |
className | string | — | Extra classes for the surface. |
data-slot | string | "list-toolbar-popover" | The surface's data-slot. |
onOpenChange | ((open: boolean) => void) | — | Called when it opens or closes. |
open | boolean | — | Whether it is open (controlled). |
size | "default" | "md" | "default" | default (288px) or md (320px). |
Data attributes and CSS variables on ListToolbarPopover
| Attribute | Values |
|---|---|
data-slot | mirrors a prop or state value |
ListToolbarMenu
| Prop | Type | Default | Description |
|---|---|---|---|
items* | readonly ("separator" | ListToolbarMenuItem)[] | — | The actions on the view, in order; "separator" draws a divider. |
label | string | "More" | The tool's name. |
Data attributes and CSS variables on ListToolbarMenu
| Attribute | Values |
|---|---|
data-slot | "list-toolbar-more" |
Menu item
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | — | Stable key. |
label* | string | — | The item's text (and, in the edit group, the button's name and tooltip). |
onSelect* | () => void | — | Run the action. |
destructive | boolean | false | A destructive action ("Empty trash"), drawn in the destructive ink. |
disabled | boolean | false | Shown but not available (Undo with nothing to undo). |
icon | React.ReactNode | — | Leading icon — required for an edit-group button. |
shortcut | string | — | The keyboard shortcut shown in the tooltip ("⌘Z"). |
ListToolbarAction
| Prop | Type | Default | Description |
|---|---|---|---|
icon* | React.ReactNode | — | The leading icon (a +). |
label* | string | — | The action's name ("New task"): the label on a wide toolbar, the tooltip and name on a narrow one. |
loading | boolean | — | Shows a spinner over the label, blocks activation and sets aria-busy. The label keeps its
box at opacity: 0, so the button's width does not move and its accessible name survives
(API-5, A11Y-12). |
menuIndicator | React.ReactNode | — | A trailing chevron: the button opens a menu ("New ▾"). |
variant | "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | — |
Data attributes and CSS variables on ListToolbarAction
| Attribute | Values |
|---|---|
data-labelled | "" |
Accessibility
- The row is a
toolbarnamed byaria-label(default "List controls"). - Every icon tool has an accessible name that carries its state ("Filter, 2 active", "Sort, changed") and a tooltip with its name and shortcut on hover and focus.
- Filter is a toggle for the row under it:
aria-expandedandaria-controlspoint at the row. - Layout is a group of two or three toggle buttons named "List", "Board", "Grid".
- Folded tabs are one menu button named after the tabs and the active tab ("Scope: My tasks"); the hidden tab list leaves the tab order and the accessibility tree.
- An icon-only main button keeps its label as its name and tooltip.
| Key | Action |
|---|---|
| / | Focus the search (opens a collapsed search) |
| F | Open or close the filter row (opens + Filter when nothing is set) |
| Tab | Move through the tabs, the search, each tool and the main button |
| Esc | Close the open popover; on an empty collapsed search, fold it |
| Contract | States tested |
|---|---|
| Behaviour | default, open, changed, folded |
| Accessibility | labeled, browser-accessibility-test |
| Visual | default, hover |