# Bulk Bar

> The bar that rises while rows are selected, with the count and the verbs that work on all of them. Its keys work while it shows, and Esc clears the selection.

- Name: `bulk-bar`
- Status: beta
- Categories: actions
- File: the install writes `bulk-bar.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@arbokui/motion`, `@arbokui/tool-button`, `@arbokui/action-keys`, `@arbokui/tooltip`. The install adds them.
- Gallery: https://arbokui.com/components/bulk-bar
- Registry item: https://arbokui.com/r/bulk-bar.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/bulk-bar
   ```

## Use it for

- A list or table where people pick rows, then act on all of them at once, like Remind or Mark available.
- Pass the same action objects your rows give HoverActions, so each verb works on one row or many.
- Use floating over a long list, inside a parent with position: relative. Leave room at the bottom so it never hides the last row.
- To place it yourself, like sticky at the bottom of a long page, leave floating off and wrap it in your own box.
- Give an action a result when its press changes something, like 'Sent to 3', so the button says what happened, with Undo.

## Avoid it for

- A single row's actions. Use HoverActions or RowActions.
- More than three or four verbs. Put the rest in a Menu.
- A bar that shows with nothing selected. It leaves at count 0 on its own.

## Props

- `count`: `number`. Required. How many rows are selected. At 0 the bar goes away.
- `noun`: `[string, string]`. Default `["item", "items"]`. The selected thing, singular and plural, like \['person', 'people'\].
- `actions`: `BulkBarAction[]`. Default `[]`. The verbs for the selection. Each shows its icon and label, or its icon only when the labels do not fit on one line. A shortcut works while the bar shows, and the tooltip teaches it.
- `BulkBarAction`: `{ label: string; icon?: ReactNode; shortcut?: string; tone?: "neutral" | "danger"; onClick?: () => void | Promise<unknown>; result?: ActionResult; disabled?: boolean }`. Type. The same shape as HoverAction. A promise from onClick shows a spinner, and a failure shows Retry. result is ToolButton's ActionResult.
- `onClear`: `() => void`. Clears the selection. It adds the clear button, and Esc calls it too.
- `floating`: `boolean`. Default `false`. Floats 1rem above the bottom of the nearest positioned parent, centered. Otherwise it sits in the flow, centered, and keeps its space.
- `keys`: `boolean`. Default `true`. Listens for the actions' keys and Esc while the bar shows.
- `clearLabel`: `string`. Default `"Clear selection"`. Names the clear button.
- `ref`: `Ref<HTMLDivElement>`. Reaches the toolbar.
- `className`: `string`. Extra classes on the toolbar. Later classes win.

## Accessibility

- The bar is a toolbar named by its count, like '2 people selected'. It is one Tab stop, and the arrow keys move between its buttons.
- The count sits in a polite live region, so screen readers hear it change.
- While it is away, the bar is inert. It leaves the Tab order and the accessibility tree, and it ignores the pointer.
- If focus is inside when the bar leaves, focus goes back to where it came from.
- When the labels do not fit on one line, the actions show their icons only. Each keeps its name, and its tooltip shows the label and key.
- Keys are ignored while someone types, while a modifier is held, and inside an open dialog or menu. A control marked data-owns-escape keeps its Esc.
- The clear button sets aria-keyshortcuts to Escape, and each action with a single-key shortcut sets its own.

## Examples

### Over a list

```tsx
import { BellRing, ThumbsUp } from "lucide-react";
import { useState } from "react";

function People({ names }: { names: string[] }) {
  const [picked, setPicked] = useState<string[]>([]);
  return (
    <div className="relative pb-16">
      {names.map((name) => (
        <label key={name} className="flex items-center gap-2">
          <input
            type="checkbox"
            checked={picked.includes(name)}
            onChange={(event) => setPicked((last) => (event.target.checked ? [...last, name] : last.filter((n) => n !== name)))}
          />
          {name}
        </label>
      ))}
      <BulkBar
        floating
        count={picked.length}
        noun={["person", "people"]}
        onClear={() => setPicked([])}
        actions={[
          { icon: <BellRing />, label: "Remind", shortcut: "R", result: { label: `Sent to ${picked.length}` } },
          { icon: <ThumbsUp />, label: "Mark available", shortcut: "A", onClick: () => setPicked([]) },
        ]}
      />
    </div>
  );
}
```
