ComponentsPanel
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"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.
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'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.
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.
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.
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
divwithrole="toolbar", named by the count unless you passaria-label. A separaterole="status"region, always on the page, announces "{count} selected" when the bar appears and when the count changes. Separators arerole="separator". - Labels
- Name every icon-only action. The ร is named by the provider's
clearSelectionstring. - 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
returnFocusTowhen 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. |
| HomeorEnd | Moves to the first or last button. |
| EnterorSpace | 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 |
|---|---|---|---|
countrequired | 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.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--accent-foreground | text |
--border | background |
--foreground | text |
--muted-foreground | text |
--popover | background |
--popover-foreground | text |