Bulk actions bar
StableA dark floating toolbar for a selection: the count, clear, five property pickers and delete.
BulkBar shows how many tasks are selected and offers Status, Priority, Assignee, Labels, Due date and Delete for all of them at once. It only reports what was pressed: onPick hands you the kind and the button to anchor a picker to (pick() from usePick does the rest). The words show once its container is wide enough; below that the buttons are icons that keep their names for screen readers.
Updated View source
Three selected tasks
import { useRef, useState } from 'react'import { sampleTasks } from '@rchursin/product/data/sample'import { AssigneeAvatar, BulkBar, labelIndex, PickerHost, PriorityIcon, StatusIcon, usePick, type PickChange, type Task } from '@rchursin/product/tasks'import { Button } from '@rchursin/product/ui'
const NOW = new Date(2026, 9, 13, 9, 30)const IDS = ['RC-127', 'RC-123', 'RC-118']const labels = labelIndex([])const fresh = () => sampleTasks(NOW).filter((t) => IDS.includes(t.id))
/** Apply a picker change to every selected task */function apply(tasks: Task[], ids: string[], change: PickChange): Task[] { return tasks.map((t) => { if (!ids.includes(t.id)) return t if (change.kind === 'labels') return { ...t, labels: [...t.labels.filter((l) => !change.remove.includes(l)), ...change.add.filter((l) => !t.labels.includes(l))] } return { ...t, [change.kind]: change.value } })}
/** Three selected tasks on their own copy of the sample data. Each button changes all three at once. */Usage
import { BulkBar, usePick } from '@rchursin/product/tasks'<div className="@container"> <BulkBar count={ids.length} onPick={(kind, anchor) => pick({ kind, ids, anchor })} onDelete={() => remove(ids)} onClear={clearSelection} /></div>API
BulkBar props
| Prop | Type | Default | Description |
|---|---|---|---|
countrequired | number | – | How many tasks are selected. |
onPickrequired | (kind: PickKind, anchor: HTMLElement) => void | – | A property button was pressed; open that picker next to anchor for the whole selection. |
onDeleterequired | () => void | – | Delete was pressed. |
onClear | () => void | – | Shows the clear (×) button. Leave it out to hide the button. |
className | string | – | Extra classes on the bar. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves through the bar’s buttons. |
| EnterSpace | Press a button. A property button opens its picker for the whole selection. |
- A group named “Bulk actions for 3 selected tasks”.
- Every button keeps a name when its word is hidden at narrow widths (the word becomes sr-only), and a tooltip gives its shortcut.
- Property buttons have aria-haspopup="dialog"; focus returns to them when the picker closes.
- It is light on dark in both themes, and focus shows as a 2px outline in the light ink.
- In TasksApp the same keys work on the selection (S, P, A, L, ⇧D, ⌘⌫) and Esc clears it; the bar itself binds no keys.
Guidelines
Do
- Put it in a @container, so its words show when there is room.
- Bind the shortcuts its tooltips promise in the view around it, as TasksApp does.
Don’t
- Don’t show it for one highlighted task; it is for a selection.
- Don’t ask “Are you sure?” before Delete; delete at once and offer Undo.
In use
Related
- Task listThe task list as a data grid: one Tab stop, arrows to move, pickers in the cells, and groups that collapse.
- TasksThe whole task tracker: a keyboard-first list and board, a detail view, bulk edits, and undo for every change.
- Property pickersThe Tasks tool’s pickers for status, priority, assignee, labels and estimate, as a chip, a row or an icon.
- ToastShort past-tense results with at most one action, like “Run archived · Undo”. Read out politely; they wait while hovered or focused.
Source
The component’s own files, exactly as they run on this site.
/** * Bulk actions for a selection, floating at the bottom of the window. The same shortcuts work * on the selection (S, P, A, L, ⇧D, ⌘⌫), and Esc clears it. */import type { MouseEvent } from "react"import { cx } from "../../components/cx"import { Icon, type IconName } from "../icons/Icon"import { comboKeys } from "../lib/hotkeys"import type { PickKind } from "./PickerHost"
const actions: { kind: PickKind; label: string; icon: IconName; key: string }[] = [ { kind: "status", label: "Status", icon: "status-progress", key: "s" }, { kind: "priority", label: "Priority", icon: "priority-high", key: "p" }, { kind: "assignee", label: "Assignee", icon: "user", key: "a" }, { kind: "labels", label: "Labels", icon: "tag", key: "l" }, { kind: "due", label: "Due date", icon: "calendar", key: "shift+d" },]
const barButton = "inline-flex h-8 min-w-8 shrink-0 items-center justify-center gap-1.5 rounded-lg px-1.5 @2xl:px-2 text-[13px] font-medium text-on-inverse/85 hover:bg-on-inverse/12 hover:text-on-inverse focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-on-inverse"