Action Bar
Floating contextual bar — a status region and actions, for bulk selection, unsaved changes, and batch progress.
- Status
- Since
0.4.0- Accessibility pattern
- APG toolbar
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 Action Bar before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify action-barInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/action-barRun 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/use-media-query.
Usage
import {
ActionBar,
ActionBarButton,
ActionBarSeparator,
} from "@/components/ui/action-bar";
<ActionBar open={count > 0} status={`${count} selected`}>
<ActionBarButton onClick={tag}>Tag</ActionBarButton>
<ActionBarSeparator />
<ActionBarButton
render={<Button variant="destructive" size="sm" />}
onClick={archive}
>
Archive
</ActionBarButton>
</ActionBar>;Bulk selection is the bar's most common recipe — never its identity. The same object serves an unsaved-changes bar and a batch-progress bar; only the words change.
Compose the actions as ActionBarButton. The bar is a Base UI Toolbar, and a
toolbar builds its single tab stop from the items that register with it — a bare
<Button> still renders, but keeps its own tab stop and the arrow keys skip it.
Pass render when an action needs a different shape (a destructive button, an icon-only
Button size="icon-sm", a menu trigger).
On a phone (below 640px) the bar docks full width, 8px in from each edge, above the safe area,
and an ActionBarButton whose children are an icon and a text label shows its icon alone — the text
stays its accessible name — so the count, the actions, ⋯ and × fit one row.
Scope
| Behaviour | Where it lives |
|---|---|
| Selection state | The host's list — DataList owns selectedIds; the bar consumes a count |
| The actions themselves | Host, as ActionBarButton children (render a Button, a DropdownMenu trigger, an AlertDialog trigger) |
| Outcome persistence | The status slot — not a toast, which discards state the user still holds |
Anatomy
Action Bar is a compound component. Every exported part, with the
data-slot it renders (generated from the canonical source):
Examples
Unsaved changes
Over a docked player
The bar sits 16px above the bottom edge plus --dock-inset-bottom. GlobalAudioPlayer sets that
on the AppShell root while it is open, so the bar floats 8px above the player instead of covering
it; set it yourself for another bottom dock.
Secondary actions on a phone
Pass the primary actions as children and the rest as secondaryActions. From sm up they are
buttons in the bar; on a phone they fold into a ⋯ menu (moreLabel, "More actions"), so the
primary actions stay visible and the bar never scrolls sideways.
<ActionBar
open={count > 0}
status={`${count} selected`}
secondaryActions={[
{ label: "Move", onClick: move },
{ label: "Delete", destructive: true, onClick: remove },
]}
>
<ActionBarButton onClick={share}>Share</ActionBarButton>
</ActionBar>Closing the set
end holds what closes the set — a separator and a Clear button — after secondaryActions, so a
Clear never sits between the primary actions and the secondary ones. It stays a button beside the ⋯
menu on a phone. DataList's selection bar puts its Clear here.
<ActionBar
status={`${count} selected`}
secondaryActions={[{ label: "Archive", onClick: archive }]}
end={
<>
<ActionBarSeparator />
<ActionBarButton
render={<Button variant="ghost" size="icon-sm" />}
aria-label="Clear selection"
onClick={clear}
>
<X />
</ActionBarButton>
</>
}
>
<ActionBarButton onClick={move}>Move…</ActionBarButton>
</ActionBar>Batch progress (pending)
While pending, the actions are inert and dimmed but the status stays
readable — a bulk operation in flight should not be re-triggerable.
API Reference
ActionBar
| Prop | Type | Default | Description |
|---|---|---|---|
announcement | string | — | Text announced politely when it changes. Defaults to status when that is
a plain string; pass explicitly when status is composite markup. |
aria-label | string | "Actions" | Accessible name for the toolbar. |
children | React.ReactNode | — | The actions. Compose ActionBarButton and ActionBarSeparator: Base UI's toolbar
builds its single tab stop from the items that register with it, so a bare <Button> renders
but keeps its own tab stop and the arrow keys skip it. |
containerRef | React.RefObject<HTMLElement | null> | — | Centre the bar over this element instead of the viewport — measured via
getBoundingClientRect, kept live through ResizeObserver plus window
resize/scroll, so a content area beside a sidebar gets a truly centred
bar. (A container that moves without any resize or scroll event — a
transition-driven layout shift — re-measures on the next of either.) |
end | React.ReactNode | — | Controls that close the set, after secondaryActions — a separator and a Clear button. They
join the toolbar's arrow-key order (compose ActionBarButton and
ActionBarSeparator) and never fold into the ⋯ menu. |
moreLabel | string | "More actions" | The ⋯ menu trigger's accessible name. |
open | boolean | true | Whether the bar is shown. It stays mounted while hidden (translated below
the viewport edge, data-active="false"), so the enter/exit transition is
pure CSS and the live region survives. |
pending | boolean | false | Dim and inert the actions while a bulk operation is in flight
(aria-busy + non-interactive), keeping the status region readable. |
secondaryActions | readonly ActionBarSecondaryAction[] | — | Secondary actions, after the children (the primary ones). From sm up they are buttons in
the bar; below sm (a phone) they fold into a ⋯ menu, so the primary actions stay visible and
the bar never scrolls sideways. |
status | React.ReactNode | — | The status region — a count ("5 selected"), a state ("Unsaved changes"), or progress ("Importing 340 of 1,000…"). Rendered before the actions. |
Data attributes and CSS variables on ActionBar
| Attribute | Values |
|---|---|
data-active | "false" | "true" |
data-pending | "" |
data-slot | "action-bar" | "action-bar-actions" | "action-bar-more" | "action-bar-status" |
| Prop | Type | Default | Description |
|---|---|---|---|
label* | React.ReactNode | — | The action's label. |
destructive | boolean | — | Paint it as a destructive action. |
disabled | boolean | — | Disable the action. |
icon | React.ReactNode | — | A leading icon. |
key | string | — | A stable key (defaults to the label when it is a string). |
onClick | (() => void) | — | Called when the action is chosen. |
ActionBarButton
One action, registered with the bar's roving focus. Takes every
Base UI Toolbar.Button prop;
render defaults to a ghost sm Button.
ActionBarSeparator
The hairline between two clusters of actions. Takes every Base UI
Toolbar.Separator prop; the orientation defaults to perpendicular to the bar,
so a horizontal bar gets a vertical rule.
Accessibility
- The bar is a Base UI
Toolbarlabelled byaria-label— pass a specific one ("Bulk actions", "Import progress") rather than the generic default. The whole bar is one tab stop: the arrow keys move between actions and wrap at the ends, and Shift + Tab leaves in a single press. - Status changes are announced through a polite live region — a plain-string
statusannounces itself; composite status markup needs anannouncement. - Actions with visible text must not carry
aria-label(WCAG 2.5.3) — the visible text is the name. - The bar sits at
z-10, below thez-50band upstream's overlays use, so a dialog opened from one of its actions correctly covers it. While hidden (and whilepending, for the actions) the subtree isinert— nothing invisible or in-flight stays focusable or activatable.
| Key | Action |
|---|---|
| Tab | Enter the bar at the last-focused action, or leave it. |
| ← / → | Move between actions; focus wraps at either end. |
| Shift + Tab | Leave the bar in one press. |
| Enter / Space | Activate the focused action. |
| Contract | States tested |
|---|---|
| Behaviour | open, hidden, pending, measured-centring |
| Accessibility | labeled, status-announcement, keyboard, semantic-html |
| Visual | default, hidden, pending, dark |