# Action bar

A floating bar that rises when items are selected: how many, the actions for them, and a button to clear the selection.

- **Import:** `import { ActionBar, ActionBarButton, ActionBarSeparator } from "@booleanpress/ui/action-bar"`
- **Radix Toolbar:** <https://www.radix-ui.com/primitives/docs/components/toolbar>
- **APG Toolbar:** <https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/>
- **Page:** <https://ui.booleanpress.com/components/action-bar> · @booleanpress/ui 0.2.0

## Usage

Put the bar after the list or table it acts on, inside a wrapper with `relative`. It shows while `count` is above zero, 1rem above the wrapper's bottom edge.

```tsx
import { Trash2Icon } from "lucide-react"
import { ActionBar, ActionBarButton } from "@booleanpress/ui/action-bar"

export function TicketsBar({ selected, clear }: { selected: string[]; clear: () => void }) {
  return (
    <ActionBar count={selected.length} onClear={clear}>
      <ActionBarButton severity="danger"><Trash2Icon />Delete</ActionBarButton>
    </ActionBar>
  )
}
```

`count` is shown as the provider's `selectedCount` ("{count} selected"), formatted for the provider's locale, and announced when the bar appears and when it changes. `onClear` adds a × named by `clearSelection`. `open` shows or hides the bar whatever the count. `position="viewport"` floats it above the bottom of the window instead.

`ActionBarButton` is the library's Button (a text button by default, with every Button prop) in the bar's arrow-key order; `ActionBarSeparator` a line between groups. For more actions than fit, put an icon `ActionBarButton` in a [dropdown menu](/components/dropdown-menu)'s trigger, as the second example does.

## Examples

### Basic

Select tickets in a list: the bar rises with the count, Archive, Delete and the clear button.

```tsx
import { ArchiveIcon, Trash2Icon } from "lucide-react"
import { useState } from "react"
import { ActionBar, ActionBarButton } from "@booleanpress/ui/action-bar"
import { Checkbox } from "@booleanpress/ui/checkbox"

const TICKETS = [
  { id: "t-1041", subject: "Cannot connect to SES" },
  { id: "t-1040", subject: "Invoice is missing a VAT number" },
  { id: "t-1039", subject: "Import stops at row 200" },
  { id: "t-1038", subject: "Test email lands in spam" },
]

export default function ActionBarBasic() {
  const [selected, setSelected] = useState<string[]>(["t-1040"])
  const toggle = (id: string, on: boolean) => setSelected((cur) => (on ? [...cur, id] : cur.filter((x) => x !== id)))

  return (
    <div className="relative h-72 w-full max-w-md rounded-md border">
      <ul className="divide-y">
        {TICKETS.map((ticket) => (
          <li key={ticket.id} className="flex items-center gap-3 px-4 py-2.5 text-sm">
            <Checkbox
              id={ticket.id}
              checked={selected.includes(ticket.id)}
              onCheckedChange={(checked) => toggle(ticket.id, checked === true)}
            />
            <label htmlFor={ticket.id} className="flex-1">
              {ticket.subject}
            </label>
          </li>
        ))}
      </ul>
      <ActionBar count={selected.length} onClear={() => setSelected([])}>
        <ActionBarButton>
          <ArchiveIcon />
          Archive
        </ActionBarButton>
        <ActionBarButton severity="danger" onClick={() => setSelected([])}>
          <Trash2Icon />
          Delete
        </ActionBarButton>
      </ActionBar>
    </div>
  )
}
```

### With many actions

Two actions and a “More actions” menu of the rest.

```tsx
import { ArchiveIcon, EllipsisIcon, MailIcon, TagIcon, Trash2Icon, UserPlusIcon } from "lucide-react"
import { useState } from "react"
import { ActionBar, ActionBarButton, ActionBarSeparator } from "@booleanpress/ui/action-bar"
import { Checkbox } from "@booleanpress/ui/checkbox"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@booleanpress/ui/dropdown-menu"

const CUSTOMERS = ["Ana Ruiz", "Li Wei", "Sam Okoye", "Grace Hall"]

export default function ActionBarManyActions() {
  const [selected, setSelected] = useState<string[]>(["Li Wei", "Sam Okoye"])
  const toggle = (name: string, on: boolean) => setSelected((cur) => (on ? [...cur, name] : cur.filter((x) => x !== name)))

  return (
    <div className="relative h-72 w-full max-w-md rounded-md border">
      <ul className="divide-y">
        {CUSTOMERS.map((name) => (
          <li key={name} className="flex items-center gap-3 px-4 py-2.5 text-sm">
            <Checkbox id={`c-${name}`} checked={selected.includes(name)} onCheckedChange={(on) => toggle(name, on === true)} />
            <label htmlFor={`c-${name}`}>{name}</label>
          </li>
        ))}
      </ul>
      <ActionBar count={selected.length} onClear={() => setSelected([])} aria-label="Customer actions">
        <ActionBarButton>
          <MailIcon />
          Email
        </ActionBarButton>
        <ActionBarButton>
          <TagIcon />
          Tag
        </ActionBarButton>
        <ActionBarSeparator />
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <ActionBarButton size="icon" aria-label="More actions">
              <EllipsisIcon />
            </ActionBarButton>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end" side="top">
            <DropdownMenuItem>
              <UserPlusIcon />
              Assign to…
            </DropdownMenuItem>
            <DropdownMenuItem>
              <ArchiveIcon />
              Archive
            </DropdownMenuItem>
            <DropdownMenuItem variant="destructive" onSelect={() => setSelected([])}>
              <Trash2Icon />
              Delete
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </ActionBar>
    </div>
  )
}
```

### In a table

The library's Table with a checkbox per row and one for all.

```tsx
import { RotateCwIcon, Trash2Icon } from "lucide-react"
import { useState } from "react"
import { ActionBar, ActionBarButton } from "@booleanpress/ui/action-bar"
import { Checkbox } from "@booleanpress/ui/checkbox"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@booleanpress/ui/table"

const ROWS = [
  { id: "4120", to: "ana@example.com", status: "Bounced" },
  { id: "4119", to: "li@example.com", status: "Failed" },
  { id: "4118", to: "sam@example.com", status: "Bounced" },
  { id: "4117", to: "grace@example.com", status: "Failed" },
]

export default function ActionBarInTable() {
  const [selected, setSelected] = useState<string[]>([])
  const all = selected.length === ROWS.length ? true : selected.length > 0 ? "indeterminate" : false
  const toggle = (id: string, on: boolean) => setSelected((cur) => (on ? [...cur, id] : cur.filter((x) => x !== id)))

  return (
    <div className="relative h-80 w-full max-w-xl">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead className="w-8">
              <Checkbox
                aria-label="Select all emails"
                checked={all}
                onCheckedChange={(on) => setSelected(on === true ? ROWS.map((r) => r.id) : [])}
              />
            </TableHead>
            <TableHead>Email</TableHead>
            <TableHead>Recipient</TableHead>
            <TableHead>Status</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {ROWS.map((row) => (
            <TableRow key={row.id} data-state={selected.includes(row.id) ? "selected" : undefined}>
              <TableCell>
                <Checkbox
                  aria-label={`Select email ${row.id}`}
                  checked={selected.includes(row.id)}
                  onCheckedChange={(on) => toggle(row.id, on === true)}
                />
              </TableCell>
              <TableCell className="tabular-nums">#{row.id}</TableCell>
              <TableCell>{row.to}</TableCell>
              <TableCell>{row.status}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      <ActionBar count={selected.length} onClear={() => setSelected([])}>
        <ActionBarButton>
          <RotateCwIcon />
          Resend
        </ActionBarButton>
        <ActionBarButton severity="danger" onClick={() => setSelected([])}>
          <Trash2Icon />
          Delete
        </ActionBarButton>
      </ActionBar>
    </div>
  )
}
```

## Accessibility

**Semantics.** A `div` with `role="toolbar"`, named by the count unless you pass `aria-label`. A separate `role="status"` region, always on the page, announces "{count} selected" when the bar appears and when the count changes. Separators are `role="separator"`.

**Labels.** Name every icon-only action. The × is named by the provider's `clearSelection` string.

**Focus.** The bar does not take focus when it appears; it follows the list in the tab order, as one tab stop, and the arrow keys move between its buttons. When it closes with focus inside (the ×, an action that clears the selection, or an item of a menu in the bar), focus returns to where it came from, or to `returnFocusTo` when that is gone. A bar that is fading out takes no clicks.

**Known limits.**

- It covers the bottom of the list while it shows; leave room below the last row (padding) so it stays reachable.
- The bar does not shrink its actions: past a few, move them into a menu.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Tab | Moves into the bar, onto the last button used, and out again: one stop. |
| → or ← | Moves to the next or previous button, wrapping at the ends. Reversed in a right-to-left page. |
| Home or End | Moves to the first or last button. |
| Enter or Space | Activates the focused button; on the ×, clears the selection. |

## API

### ActionBar

Renders Radix Toolbar.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `count` (required) | `number` |  | How many items are selected: shown as "{count} selected", announced politely, and the bar shows while it is above 0. |
| `asChild` | `boolean` |  |  |
| `dir` | `"ltr" \| "rtl"` |  | The reading direction. The provider's `dir` by default. |
| `loop` | `boolean` |  | Whether the arrows wrap from the last button to the first. `true` by default. |
| `onClear` | `(() => void)` |  | Renders the × button, which should empty the selection. |
| `open` | `boolean` |  | Shows or hides the bar whatever `count` is. |
| `position` | `"container" \| "viewport"` | `container` | `container` floats it 1rem above the bottom of the nearest positioned ancestor (give the list's wrapper `relative`); `viewport` floats it above the bottom of the window. |
| `returnFocusTo` | `ReturnFocusTarget` |  | Where focus goes when the bar closes with focus in it and the element focused before it is gone. By default focus goes back to the element it came from. |

### ActionBarButton

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` |  | Shows the spinner in place of the leading icon, sets `aria-busy` and `aria-disabled` and ignores presses (a click, Enter, Space, a form's submission), while the button keeps its focus and its place in the tab order. With `asChild` the child (a link) gets the same. |
| `raised` | `boolean \| null` |  |  |
| `rounded` | `boolean \| null` |  |  |
| `severity` | `"success" \| "info" \| "warning" \| "help" \| "danger" \| "contrast" \| null` |  | Button's severity colour, such as `danger` for a delete. |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg" \| null` |  | Button's size: `default`, `sm`, `lg`, or a square `icon` size. |
| `variant` | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` | `ghost` | Button's look: `ghost` (default), `outline`, `default`, `secondary`, `destructive`, `link`. |

### ActionBarSeparator

Renders Radix Toolbar.Separator and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `decorative` | `boolean` |  | Whether or not the component is purely decorative. When true, accessibility-related attributes are updated so that that the rendered element is removed from the accessibility tree. |
| `orientation` | `"horizontal" \| "vertical"` |  | Either `vertical` or `horizontal`. Defaults to `horizontal`. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="action-bar-status"` (ActionBar), `data-slot="action-bar-button"` (ActionBarButton), `data-slot="action-bar-separator"` (ActionBarSeparator), and `data-state`, `data-position`.

**Provider strings:** `selectedCount`, `clearSelection` (`BooleanUIProvider`'s `strings`).

## Theming

A floating surface: `--popover`, `--popover-foreground` and `--border`, 8px radius, the overlay shadow. It rises and fades with the overlay motion tokens.

| Token | Used for |
| --- | --- |
| `--accent-foreground` | text |
| `--border` | background |
| `--foreground` | text |
| `--muted-foreground` | text |
| `--popover` | background |
| `--popover-foreground` | text |
