Skip to content
Component installs need the registry setup
VegaStack Design

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
stable
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-toolbar

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/list-toolbar

Run 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

BehaviourWhere it lives
Which tools a page has, their values and defaultsHost
Measuring the row and the collapse orderListToolbar
Folding scope tabs into a dropdownTabsList overflow="menu" (also RouteTabs)
Sort and Layout inside Display once foldedListToolbar → DisplayMenu (context)
Undo/redo inside ⋯ once foldedListToolbar → ListToolbarMenu (context)
Filter countsFilterRow (or filter.count)
Turning a plain-language request into filters (AI)Host — AddFilterMenu ai.onSubmit
Bottom sheets on a phoneListToolbarPopover

Anatomy

ListToolbar — 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"
ListToolbarButton — data-slot="list-toolbar-count" | "list-toolbar-dot"
ListToolbarPopover
ListToolbarMenu — data-slot="list-toolbar-more"
ListToolbarAction

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.

Desktop 1440 · 1120px of content
Laptop 1280 · 960px of content
iPad landscape 1024 · 700px of content
iPad portrait 768 · 640px of content
Phone 390 · 358px of content

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

PropTypeDefaultDescription
actionReact.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-labelstring"List controls"The toolbar's accessible name.
displayReact.ReactNode—The Display tool — a DisplayMenu.
editreadonly ListToolbarMenuItem[]—Edit tools (a table's Undo, Redo): icon buttons with a divider after them. They move into ⋯ last of all.
filterListToolbarFilter—The Filter tool. Shown when this or filterRow is given.
filterRowReact.ReactNode—The row under the toolbar that the Filter tool opens — a FilterRow.
layoutListToolbarLayout<ListToolbarLayoutValue>—The Layout tool. Omit it where a page has one layout.
moreReact.ReactNode—The ⋯ tool — a ListToolbarMenu of actions on the view.
searchListToolbarSearch—The search field, right of the tabs and before the tools.
shortcutsbooleantrue (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.
sortListToolbarSort—The Sort tool. Omit it where order is fixed (a board).
stateReact.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.
tabsReact.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

AttributeValues
data-layoutmirrors a prop or state value
data-levelmirrors a prop or state value
data-sizemirrors 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"
PropTypeDefaultDescription
onValueChange*(value: string) => void—Called on every keystroke.
value*string—The query.
aria-labelstringplaceholderThe field's accessible name.
collapsiblebooleanfalseStart as an icon at every width (a table, where the grid's own header is the focus).
debounceMsnumber300Debounce for onValueCommitted, in ms.
onValueCommitted((value: string) => void)—Called with the settled query (after the debounce, on Enter, on clear) — send the query here.
placeholderstring"Search"The field's placeholder ("Search tasks").

Filter

PropTypeDefaultDescription
countnumber—The number of filters applied. Omitted, the FilterRow counts its own chips and set facets.
defaultOpenbooleanfalseWhether the row starts open (uncontrolled).
labelstring"Filter"The tool's name.
onOpenChange((open: boolean) => void)—Called when the row opens or closes.
openboolean—Whether the filter row is open (controlled).
storageKeystring—Remember the open state for this page in sessionStorage under this key.

Sort

PropTypeDefaultDescription
content*React.ReactNode—The popover's body: a SortMenu (one key) or a SortBuilder (several).
countnumber—How many keys are set (multi): shown on the tool instead of the dot.
isDefaultbooleantrueWhether the order is the page's default. false puts a dot on the tool.
labelstring"Sort"The tool's name.
summaryReact.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

PropTypeDefaultDescription
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.
labelsPartial<Record<ListToolbarLayoutValue, string>>—Relabel a layout ("Cards" for "Grid").

ListToolbarButton

PropTypeDefaultDescription
icon*React.ReactNode—The tool's icon (16px).
label*string—The tool's name: its tooltip and the start of its accessible name.
countnumber—A count shown after the icon ("▽2") — filters applied, sort keys. Nothing at 0.
dotbooleanfalseA 6px dot on the icon: the tool is not at the page's default.
loadingboolean—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).
shortcutstring—The keyboard shortcut shown in the tooltip ("F").

Data attributes and CSS variables on ListToolbarButton

AttributeValues
data-dot""
data-slot"list-toolbar-count" | "list-toolbar-dot"

ListToolbarPopover

PropTypeDefaultDescription
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.
classNamestring—Extra classes for the surface.
data-slotstring"list-toolbar-popover"The surface's data-slot.
onOpenChange((open: boolean) => void)—Called when it opens or closes.
openboolean—Whether it is open (controlled).
size"default" | "md""default"default (288px) or md (320px).

Data attributes and CSS variables on ListToolbarPopover

AttributeValues
data-slotmirrors a prop or state value

ListToolbarMenu

PropTypeDefaultDescription
items*readonly ("separator" | ListToolbarMenuItem)[]—The actions on the view, in order; "separator" draws a divider.
labelstring"More"The tool's name.

Data attributes and CSS variables on ListToolbarMenu

AttributeValues
data-slot"list-toolbar-more"
PropTypeDefaultDescription
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.
destructivebooleanfalseA destructive action ("Empty trash"), drawn in the destructive ink.
disabledbooleanfalseShown but not available (Undo with nothing to undo).
iconReact.ReactNode—Leading icon — required for an edit-group button.
shortcutstring—The keyboard shortcut shown in the tooltip ("⌘Z").

ListToolbarAction

PropTypeDefaultDescription
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.
loadingboolean—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).
menuIndicatorReact.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

AttributeValues
data-labelled""

Accessibility

  • The row is a toolbar named by aria-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-expanded and aria-controls point 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.
KeyAction
/Focus the search (opens a collapsed search)
FOpen or close the filter row (opens + Filter when nothing is set)
TabMove through the tabs, the search, each tool and the main button
EscClose the open popover; on an empty collapsed search, fold it
ContractStates tested
Behaviourdefault, open, changed, folded
Accessibilitylabeled, browser-accessibility-test
Visualdefault, hover

Do / Don't

Do
Pass a tool only when the page has something for it to do, keep the page's one create action in action, and let the toolbar fold Sort and Layout into Display.
Don't
Add labelled text buttons beside the icons, show a disabled tool as a placeholder, or put a second main button in the page header.

On this page