VisorVisor
ComponentsAdmin

Bulk Action Bar

Admin bulk action bar compound — sticky or inline toolbar that appears when rows are selected. Shows a live-announced selection count, an actions slot, and an optional dismiss button. Built with CSS Modules and a reduced-motion-aware entrance animation — copy it into your project and own it completely.

Inline

The inline variant flows in the document instead of pinning to the viewport bottom — useful when the bar lives inside a scroll container or a dialog.

Custom Label

Provide a label renderer to customize the count string — useful for pluralization or domain-specific nouns.

Without Dismiss

Set dismissible={false} when the selection should only be cleared programmatically by consumer actions.

Installation

npx visor add bulk-action-bar

This copies two files into your project:

  • components/ui/bulk-action-bar/bulk-action-bar.tsx — the component
  • components/ui/bulk-action-bar/bulk-action-bar.module.css — the styles

The registry also pulls in button as a dependency.

Usage

import { useState } from 'react';
import { BulkActionBar } from '@/components/ui/bulk-action-bar/bulk-action-bar';
import { Button } from '@/components/ui/button/button';

export default function Example() {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <BulkActionBar
      count={selected.length}
      onClear={() => setSelected([])}
    >
      <Button variant="outline" size="sm">
        Archive
      </Button>
      <Button variant="destructive" size="sm">
        Delete
      </Button>
    </BulkActionBar>
  );
}

API Reference

BulkActionBarProps

PropTypeDefaultDescription
count*numberNumber of selected items. The bar returns null when count is 0 or less.
children*React.ReactNodeAction buttons cluster — typically one or more Button instances.
inlinebooleanfalseRender inline (non-sticky) instead of fixed to the viewport bottom.
label(count: number) => React.ReactNode(n) => `${n} selected`Renderer for the selection count label. Wrapped in aria-live="polite" so changes are announced.
clearLabelReact.ReactNode'Clear selection'Aria-label for the dismiss button. Used verbatim when it is a string.
onClear() => voidFired by the Escape key and the dismiss button. The Escape handler is only attached when this is provided.
dismissiblebooleantrueShow the dismiss (X) button. Requires onClear to render the button.
autoFocusbooleantrueAuto-focus the first enabled action button on mount. Only runs on mount, not on count changes.

The component also accepts all standard HTML attributes for the root <div>.

Behavior

  • Mounts only when count > 0 — zero selection renders nothing, no invisible DOM.
  • Escape-to-clear — when onClear is provided, pressing Escape anywhere on the page fires it.
  • Auto-focus — on mount, focus moves to the first enabled action button. Disable with autoFocus={false}.
  • Entrance animation — sticky variant slides up + fades in over 300ms. prefers-reduced-motion skips the animation entirely.
  • Live announcement — the count is wrapped in aria-live="polite" so assistive tech announces selection changes.

Source Files

After running npx visor add bulk-action-bar, you'll have:

bulk-action-bar.tsx

A forwardRef client component with role="toolbar" and aria-label="Bulk actions". The count span is aria-live="polite", the Escape handler is attached via useEffect only when onClear is supplied, and auto-focus runs once on mount via a useRef to the actions slot.

bulk-action-bar.module.css

All values use CSS custom properties from @loworbitstudio/visor-core. The sticky variant uses position: fixed with z-index: 50 (promote to a --z-index-toolbar token once the z-index scale is formalized), and the entrance keyframes are wrapped in a prefers-reduced-motion guard.

Customization

After copying the component, you own it completely. Common customizations:

  • Anchor the sticky variant inside a scroll container by swapping position: fixed for position: sticky.
  • Replace the default count label with a translation function that handles plural forms.
  • Add a "Select all" affordance next to the count for cross-page selection.
  • Promote the bar into a Portal when the surrounding layout clips fixed children.