Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Action Bar

Floating contextual bar — a status region and actions, for bulk selection, unsaved changes, and batch progress.

Status
stable
Since
0.4.0
Accessibility pattern
APG toolbar

Last updated

Install

Add Action Bar from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/action-bar

The same command installs the registry items it composes: @vegastack/button.

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="soft" tone="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 IconButton, a menu trigger).

Scope

BehaviourWhere it lives
Selection stateThe host's list — DataList owns selectedIds; the bar consumes a count
The actions themselvesHost, as ActionBarButton children (render a Button, a DropdownMenu trigger, an AlertDialog trigger)
Outcome persistenceThe 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):

ActionBar — data-slot="action-bar" | "action-bar-actions" | "action-bar-status"
ActionBarButton — data-slot="action-bar-button"
ActionBarSeparator — data-slot="action-bar-separator"

Examples

Unsaved changes

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

PropTypeDefaultDescription
announcementstringText announced politely when it changes. Defaults to status when that is a plain string; pass explicitly when status is composite markup.
aria-labelstring"Actions"Accessible name for the toolbar.
childrenReact.ReactNodeThe 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.
containerRefReact.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.)
openbooleantrueWhether 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.
pendingbooleanfalseDim and inert the actions while a bulk operation is in flight (aria-busy + non-interactive), keeping the status region readable.
statusReact.ReactNodeThe 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

AttributeValues
data-active"false" | "true"
data-pending""
data-slot"action-bar" | "action-bar-actions" | "action-bar-status"

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 Toolbar labelled by aria-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 status announces itself; composite status markup needs an announcement.
  • Actions with visible text must not carry aria-label (WCAG 2.5.3) — the visible text is the name.
  • The bar sits in the raised band, so a dialog opened from one of its actions correctly covers it. While hidden (and while pending, for the actions) the subtree is inert — nothing invisible or in-flight stays focusable or activatable.
KeyAction
TabEnter the bar at the last-focused action, or leave it.
/ Move between actions; focus wraps at either end.
Shift + TabLeave the bar in one press.
Enter / SpaceActivate the focused action.
ContractStates tested
Behaviouropen, hidden, pending, measured-centring
Accessibilitylabeled, status-announcement, keyboard, semantic-html
Visualdefault, hidden, pending, dark

Do / Don't

Do
Keep outcome summaries in the status slot ('5 dismissed · 1 failed') so retained selection and its explanation live together.
Don't
Own selection state in the bar or announce outcomes only in a toast — the notification disappears while the user still holds the selection.
Do
Compose the actions as ActionBarButton so every one of them joins the bar's single tab stop.
Don't
Drop a bare Button into the bar — it renders, but it keeps its own tab stop and the arrow keys skip it.

On this page