Components · Actions
SelectionToolbar
The toolbar that floats at the bottom of a list or conversation while items are selected, with the count and the actions on them, such as forwarding messages or changing tasks in bulk.
import { SelectionToolbar } from "@botiverse/raft-ui/wip";SelectionToolbar floats at the bottom of a list or conversation while several items are selected: the count, then the actions on those items, then × to stop selecting. Messages (Forward) and tasks (bulk actions) share it, in the BatchBar style (Noel; stdrc, 2026-11-10, #wg-task-v2), so selecting reads the same everywhere. Its rules are in Toolbar for selected items. It is a FitToolbar that never wraps.
When to use
Anatomy
In this order:
SelectionToolbarDock: where it floats, centered at the bottom of its positioned parent. The toolbar measures what fits in its width.SelectionToolbar: the floating frame, a Base UI Toolbar (role="toolbar"). Required.SelectionToolbarItem: any other control of the toolbar, such as a thread's select-all checkbox before the count: wrap it (<SelectionToolbarItem><Checkbox /></SelectionToolbarItem>) so the arrow keys reach it; it keeps its own role and keys.SelectionToolbarCount: "3 selected", with the number inSelectionToolbarCountValue. Required.SelectionToolbarHint: an optional short line after the count, such as "1 will be skipped".SelectionToolbarActions: the surface's actions.SelectionToolbarAction: one action, an icon and a label.secondarymakes it the first to move into More;menuadds the chevron of an action that opens a menu;moreItemis what it is in More's menu (by default a menu item with its icon and label that calls itsonClick; give amenuaction aDropdownMenuSubmenu).SelectionToolbarMore: More, after the actions, shown only while some are in it. PutSelectionToolbarMoreItemsin its menu: it lists them, in order.SelectionToolbarClose: ×, last. Required.
The stable MessageMultiSelectToolbar and MessageMultiSelectToolbarActions keep their earlier bar until SelectionToolbar leaves wip, so a change here is not a change to them.
Look
- A floating panel's frame:
radius-container,layer-popover, aline-seamring andshadow-float, as every floating panel (Elevation). It is as tall as a head with a head's air at its ends (header-heightandheader-air): 44px and 5px with a mouse on a desktop, 56px and 6px on a phone and a touch screen. It replaces the composer at the bottom of a column, so it takes a head's measure (Noel). It is one row and never wraps. - The count is
text-calloutbold, 8px on its left (and its right with a mouse), with the number in Space Mono, as counts are. The hint istext-captioninforeground-hint. - With a mouse every button is an
mdghost Button, 34px, 4px apart, with a 14px icon and its label, as the BatchBar spec draws it (Noel); a menu action's 12px chevron follows its label. An action whose label has given way is a 34px square; More and × are too. - On a phone and a touch screen the buttons are a head's bar buttons, 44px (
bar-button-size), side by side, showing their icons alone with no chevrons, as a head's buttons are by touch: at 360px a phone then fits the count, Status, Assignee, Nudge, More and × (Noel, 2026-11-10). - There is no primary button, and no pink: while several items are selected, no action outranks the others (stdrc, 2026-11-10).
Behavior
SelectionToolbarDockfloats it 12px above the bottom of the list or conversation, plus the safe area, as the conversation's other floating controls, so it sits above a phone's bottom navigation, and 20px from the sides, the composer's edge.- When its row doesn't fit the width it floats in, measured in that width and never read from the window, things give way one step at a time (FitToolbar): the hint (then read by screen readers only), the
secondaryactions into More from the end, the labels from the end (so the first action's goes last; each then shows its label as a tooltip), then the other actions into More from the end. The count and × never give way. It never wraps, because a second line would cover the items under it (When a row runs out of room). - Open the menus of a menu action and of More upward:
side="top"onDropdownMenuContent. - With a mouse each button's hit area reaches only half the 4px gap toward its neighbour, so a click between two lands on the nearer; by touch each 44px button is its own tap.
Content
- The count says what is selected, as a number and a word: "3 selected". In a thread with a select-all checkbox, "All 169 selected" when all are (Toolbar for selected items).
- Keep the hint to a few words about what the actions will do to the selection, such as "1 will be skipped". It is the first to give way, so don't put anything there the person needs.
- Write labels in sentence case: Forward, Generate image, Copy link, Copy Markdown. Order the actions by why people select the items, the most wanted first: it keeps its label longest. For messages Forward first, Generate image next (it shares them out too), the two copies last, the last three
secondary. For tasks Status, Assignee, Nudge, then Follow and Delete markedsecondary: they are the first into More when the row is narrow (Noel, 2026-11-10).
Accessibility
- Give
SelectionToolbaranaria-labelthat stays the same while the count changes, such as "Selected message actions". It is one Tab stop: Left and Right move between its controls, skipping those in More (Base UI Toolbar). SelectionToolbarCountis a polite live region: the new count is announced when it changes.- An action showing its icon alone keeps its label as its accessible name and shows it as a tooltip.
SelectionToolbarMoreandSelectionToolbarClosetake anaria-label, such as "More actions" and "Cancel".- End the selection on Escape, as × does, unless a menu is open; then move the focus back into the conversation: to the composer, or where there is none, to the item the selection started on (Moving focus). The toolbar does not do this for you: the selection is yours.
API
SelectionToolbar
| Prop | Type | Default |
|---|---|---|
| boolean | ||
| If `true`, using keyboard navigation will wrap focus to the other end of the toolbar once the end is reached. | boolean | true |
| Allows you to replace the component's HTML element with a different tag, or compose it with another component. | ReactElement | ((props, state) => ReactElement) |
Also takes the attributes of its element.
SelectionToolbarAction
| Prop | Type | Default |
|---|---|---|
| The label. While only the icon shows, the label is its accessible name and its tooltip. | ReactNode | |
| The action's 14px icon. | ReactNode | |
| Busy: shows the spinner and blocks activation (click, Enter, Space and form submission) while keeping the button focusable. A busy button is `aria-disabled`, not natively `disabled`, so the focus a press left on it stays there and is still there when the wait ends. | boolean | |
| The spinner's accessible name while `loading`: say what is happening. | string | |
| The action opens a menu: a 12px chevron follows the label. Open the menu upward. | boolean | false |
| What the action is in More's menu while it is there. By default a `DropdownMenuItem` with its icon and label that calls its `onClick`. Required: its menu in More's (a `DropdownMenuSubmenu` with the same choices). Its `onClick` only opens its own menu, so it has no default, and without one it would vanish into More. | ReactNode | |
| Moves into More before the labels give way, the last first. | boolean | false |
| Whether the button should be focusable when disabled. | boolean | false |
| Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). | boolean | true |
| Allows you to replace the component's HTML element with a different tag, or compose it with another component. | ReactElement | ((props, state) => ReactElement) |
Also takes the attributes of its element.
SelectionToolbarActions
| Prop | Type | Default |
|---|---|---|
| `scroll`: the group takes the rest of its line and scrolls sideways instead of giving way, its items whole, the side that hides more fading. Mark an applied or chosen item `data-active`: it is kept in view. | "scroll" | |
| Groups with the same `wrapLine` wrap onto the same line, in their order, a fixed gap apart; a group without one takes a line of its own. | string | |
| When `true` all toolbar items in the group are disabled. | boolean | false |
| Allows you to replace the component's HTML element with a different tag, or compose it with another component. | ReactElement | ((props, state) => ReactElement) |
Also takes the attributes of its element.
SelectionToolbarClose
| Prop | Type | Default |
|---|---|---|
| Its accessible name, such as "Cancel". | string | |
| Busy: shows the spinner and blocks activation (click, Enter, Space and form submission) while keeping the button focusable. A busy button is `aria-disabled`, not natively `disabled`, so the focus a press left on it stays there and is still there when the wait ends. | boolean | |
| The spinner's accessible name while `loading`: say what is happening. | string | |
| Whether the button should be focusable when disabled. | boolean | false |
| Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). | boolean | true |
| Allows you to replace the component's HTML element with a different tag, or compose it with another component. | ReactElement | ((props, state) => ReactElement) |
Also takes the attributes of its element.
SelectionToolbarCount, SelectionToolbarCountValue, SelectionToolbarDock, SelectionToolbarHint
| Prop | Type | Default |
|---|---|---|
| Allows you to replace the component's HTML element with a different tag, or compose it with another component. | ReactElement | ((props, state) => ReactElement) |
Also takes the attributes of its element.
SelectionToolbarItem
| Prop | Type | Default |
|---|---|---|
| The control, such as `<Checkbox />`. It keeps its own role and keys. | ReactElement<Record<string, unknown>, string | JSXElementConstructor<any>> |
SelectionToolbarMore
| Prop | Type | Default |
|---|---|---|
| Its accessible name, such as "More actions". | string | |
| Busy: shows the spinner and blocks activation (click, Enter, Space and form submission) while keeping the button focusable. A busy button is `aria-disabled`, not natively `disabled`, so the focus a press left on it stays there and is still there when the wait ends. | boolean | |
| The spinner's accessible name while `loading`: say what is happening. | string | |
| Whether the button should be focusable when disabled. | boolean | false |
| Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). | boolean | true |
| Allows you to replace the component's HTML element with a different tag, or compose it with another component. | ReactElement | ((props, state) => ReactElement) |
Also takes the attributes of its element.
Related
Changes
- 0.83.0 · AddedSelectionToolbar (from
@botiverse/raft-ui/wip): the floating toolbar for selected items (messages, tasks), in the BatchBar style.SelectionToolbarDockfloats it 12px above the bottom (plus the safe area) and 20px from the sides;SelectionToolbarCount(with the number inSelectionToolbarCountValue, Space Mono),SelectionToolbarHint,SelectionToolbarActionswithSelectionToolbarAction(secondary,menu,moreItem),SelectionToolbarMorewithSelectionToolbarMoreItemsin its menu,SelectionToolbarItemfor another control such as a select-all checkbox, andSelectionToolbarClose. A floating panel's frame (line-seamring,shadow-float) as tall as a head: 44px withmdbuttons 4px apart with a mouse, 56px with 44px bar buttons side by side showing their icons alone by touch. It is a Base UI Toolbar (one Tab stop, arrow keys inside) and never wraps: when its row doesn't fit the width it floats in, measured, the hint, the secondary actions, the labels (the first action's last) and then the other actions give way, the last first; the count and × stay.