Raft Design System

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.

ExperimentalAlso called BatchBar, Toolbar for selected items, Multi-select toolbar, Bulk actions
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.

selection-toolbar/selection-toolbar

When to use

SituationUse
Actions on several selected messages or tasksSelectionToolbar
Selected rows in a list with a header row (Activity)ActivityInbox's selection toolbar, inline in the header, which can wrap because it covers nothing
Actions on text the person selectedTextSelectionToolbar
Actions on one message under the pointerThe message's hover toolbar (MessageItem)
Actions on one rowThe row's own menu (DropdownMenu)

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 in SelectionToolbarCountValue. 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. secondary makes it the first to move into More; menu adds the chevron of an action that opens a menu; moreItem is what it is in More's menu (by default a menu item with its icon and label that calls its onClick; give a menu action a DropdownMenuSubmenu).
  • SelectionToolbarMore: More, after the actions, shown only while some are in it. Put SelectionToolbarMoreItems in 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.

message-item/compositions/selection-showcase

Look

  • A floating panel's frame: radius-container, layer-popover, a line-seam ring and shadow-float, as every floating panel (Elevation). It is as tall as a head with a head's air at its ends (header-height and header-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-callout bold, 8px on its left (and its right with a mouse), with the number in Space Mono, as counts are. The hint is text-caption in foreground-hint.
  • With a mouse every button is an md ghost 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

  • SelectionToolbarDock floats 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 secondary actions 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" on DropdownMenuContent.
  • 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 marked secondary: they are the first into More when the row is narrow (Noel, 2026-11-10).

Accessibility

  • Give SelectionToolbar an aria-label that 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).
  • SelectionToolbarCount is 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.
  • SelectionToolbarMore and SelectionToolbarClose take an aria-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

ExperimentalThe floating toolbar for selected items (messages, tasks): the count, an optional hint, the actions on the selection, More while some are out of the row, then ×. One row at any width: what doesn't fit gives way, measured in the width it floats in, and never wraps. A `role="toolbar"`, one Tab stop with the arrow keys inside: give it an `aria-label`.
PropTypeDefault
disabledboolean
loopFocusIf `true`, using keyboard navigation will wrap focus to the other end of the toolbar once the end is reached.booleantrue
renderAllows 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

ExperimentalOne action on the selection: a 14px icon and a label, an `md` ghost button with a mouse on a desktop, a 44px bar button showing its icon alone on a phone and a touch screen.
PropTypeDefault
children (required)The label. While only the icon shows, the label is its accessible name and its tooltip.ReactNode
icon (required)The action's 14px icon.ReactNode
loadingBusy: 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
loadingLabelThe spinner's accessible name while `loading`: say what is happening.string
menuThe action opens a menu: a 12px chevron follows the label. Open the menu upward.booleanfalse
moreItemWhat 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
secondaryMoves into More before the labels give way, the last first.booleanfalse
focusableWhenDisabledWhether the button should be focusable when disabled.booleanfalse
nativeButtonWhether 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>`).booleantrue
renderAllows 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

ExperimentalThe actions on the selection, in one row.
PropTypeDefault
overflow`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"
wrapLineGroups 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
disabledWhen `true` all toolbar items in the group are disabled.booleanfalse
renderAllows 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

Experimental×, last: ends the selection, as Escape does.
PropTypeDefault
aria-label (required)Its accessible name, such as "Cancel".string
loadingBusy: 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
loadingLabelThe spinner's accessible name while `loading`: say what is happening.string
focusableWhenDisabledWhether the button should be focusable when disabled.booleanfalse
nativeButtonWhether 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>`).booleantrue
renderAllows 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

ExperimentalWhat the actions act on ("3 selected"), first. Announced when it changes. Put the number in `SelectionToolbarCountValue`.
PropTypeDefault
renderAllows 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

ExperimentalAny other control of the toolbar, such as a thread's select-all checkbox before the count, so the arrow keys reach it. It takes the toolbar's focus handling only: the control keeps its role and its own keys.
PropTypeDefault
children (required)The control, such as `<Checkbox />`. It keeps its own role and keys.ReactElement<Record<string, unknown>, string | JSXElementConstructor<any>>

SelectionToolbarMore

ExperimentalMore, after the actions, shown only while some are in it. Render it as a `DropdownMenuTrigger` whose menu opens upward (`side="top"`), with `SelectionToolbarMoreItems` in the menu.
PropTypeDefault
aria-label (required)Its accessible name, such as "More actions".string
loadingBusy: 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
loadingLabelThe spinner's accessible name while `loading`: say what is happening.string
focusableWhenDisabledWhether the button should be focusable when disabled.booleanfalse
nativeButtonWhether 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>`).booleantrue
renderAllows 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.

Changes

  • 0.83.0 · AddedSelectionToolbar (from @botiverse/raft-ui/wip): the floating toolbar for selected items (messages, tasks), in the BatchBar style. SelectionToolbarDock floats it 12px above the bottom (plus the safe area) and 20px from the sides; SelectionToolbarCount (with the number in SelectionToolbarCountValue, Space Mono), SelectionToolbarHint, SelectionToolbarActions with SelectionToolbarAction (secondary, menu, moreItem), SelectionToolbarMore with SelectionToolbarMoreItems in its menu, SelectionToolbarItem for another control such as a select-all checkbox, and SelectionToolbarClose. A floating panel's frame (line-seam ring, shadow-float) as tall as a head: 44px with md buttons 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.