Skip to content
Component installs need the registry setup
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

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-bar

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

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

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/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

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-more" | "action-bar-status"
ActionBarButton — data-slot="action-bar-button"
ActionBarSeparator — data-slot="action-bar-separator"

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

PropTypeDefaultDescription
announcementstring—Text 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.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.
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.)
endReact.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.
moreLabelstring"More actions"The ⋯ menu trigger's accessible name.
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.
secondaryActionsreadonly 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.
statusReact.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

AttributeValues
data-active"false" | "true"
data-pending""
data-slot"action-bar" | "action-bar-actions" | "action-bar-more" | "action-bar-status"
PropTypeDefaultDescription
label*React.ReactNode—The action's label.
destructiveboolean—Paint it as a destructive action.
disabledboolean—Disable the action.
iconReact.ReactNode—A leading icon.
keystring—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 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 at z-10, below the z-50 band upstream's overlays use, 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