Bulk Bar
betaThe 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.
Preview
Pick people. The bar rises with R and A working, and Esc clears.
Last action: Nothing yet
in flow
danger
states
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/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
Required. How many rows are selected. At 0 the bar goes away.
number
- noun
The selected thing, singular and plural, like ['person', 'people'].
[string, string] · default ["item", "items"]
- actions
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[] · default []
- BulkBarAction
Type. The same shape as HoverAction. A promise from onClick shows a spinner, and a failure shows Retry. result is ToolButton's ActionResult.
{ label: string; icon?: ReactNode; shortcut?: string; tone?: "neutral" | "danger"; onClick?: () => void | Promise<unknown>; result?: ActionResult; disabled?: boolean }
- onClear
Clears the selection. It adds the clear button, and Esc calls it too.
() => void
- floating
Floats 1rem above the bottom of the nearest positioned parent, centered. Otherwise it sits in the flow, centered, and keeps its space.
boolean · default false
- keys
Listens for the actions' keys and Esc while the bar shows.
boolean · default true
- clearLabel
Names the clear button.
string · default "Clear selection"
- ref
Reaches the toolbar.
Ref<HTMLDivElement>
- className
Extra classes on the toolbar. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| count | number | - | Required. How many rows are selected. At 0 the bar goes away. |
| noun | [string, string] | ["item", "items"] | The selected thing, singular and plural, like ['person', 'people']. |
| actions | BulkBarAction[] | [] | 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 | 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 | true | Listens for the actions' keys and Esc while the bar shows. |
| clearLabel | string | "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
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>
);
}