Organisms

Selection bar

What a list says back once rows are ticked: how many are held, what can be done to them, and a way out. It appears only while a selection exists, floats over the list it belongs to, and disappears the moment the selection is emptied. Figma calls it Data Table / Multiple action.

Updated Aug 19, 2026 by Juan Pablo Turina

Live preview

3 selected
import { SelectionBar } from "@/components/organisms/selection-bar";

<SelectionBar
  count={selected.length}
  actions={[
    { label: "Replace primary provider", onSelect: replacePrimary },
    { label: "Replace fallback provider", onSelect: replaceFallback },
  ]}
  onClear={() => setSelected([])}
/>

Anatomy

One row, three blocks separated by vertical rules: the count, the actions, the way out. Padding is 16/24, the gap between blocks is 24 and between actions 16, the radius is 8 and the elevation is shadow-lg — it is floating over content, so it needs to read as a layer.

3 selected
  1. 1
    Count

    text-sm regular foreground: '3 selected'. It is a statement about the selection, not a heading, so it carries no weight of its own.

  2. 2
    Separator

    A vertical rule, 20px tall. There is one on each side of the actions, which is what keeps the count and the way out from reading as more actions.

  3. 3
    Actions

    Outline sm Buttons, gap-4. Every one of them is outline — see the usage notes on why none is primary.

  4. 4
    Overflow kebab

    Past maxVisible (3 by default) the remaining actions collapse into the kit's canonical kebab: DotsThreeOutline fill in a bare button, always visible, wrapped in a Tooltip.

  5. 5
    Clear

    XCircle at 24px, weight fill. It is a Phosphor glyph — not a circle drawn by hand with an X placed inside it, which is how three surfaces had built it.

Common configurations

Three shapes cover everything seen so far: the Figma's two actions, a single destructive action, and more actions than fit — where the kebab takes over. The fourth block is the docking recipe, because the bar does not position itself.

The Figma: two actions

Routing's condition-set list. Two outline buttons, a separator on each side, and the clear glyph.

3 selected
import { SelectionBar } from "@/components/organisms/selection-bar";

<SelectionBar
  count={selected.length}
  actions={[
    { label: "Replace primary provider", onSelect: replacePrimary },
    { label: "Replace fallback provider", onSelect: replaceFallback },
  ]}
  onClear={() => setSelected([])}
/>
A single destructive action

The merchant-ID table. Delete is outline like everything else — the warning is in the confirmation Dialog it opens, not in the bar.

2 selected
<SelectionBar
  count={selected.length}
  actions={[{ label: "Delete", icon: Trash, onSelect: confirmDelete }]}
  onClear={() => setSelected([])}
/>
More actions than fit

The routing canvas list view has four. The first three stay as buttons and the rest move into the kebab, so the bar keeps its width no matter how many actions a screen has.

7 selected
<SelectionBar
  count={selected.length}
  actions={[
    { label: "Replace primary provider", onSelect: replacePrimary },
    { label: "Replace fallback provider", onSelect: replaceFallback },
    { label: "Add risk profile", onSelect: addRisk },
    { label: "Remove risk profile", onSelect: removeRisk },
  ]}
  onClear={() => setSelected([])}
/>
Docked over a list (recipe)

The canonical way to float it: a sticky wrapper that does not swallow clicks on the list behind it, with the bar itself taking the pointer events back.

3 selected
{/* The wrapper does not swallow clicks on the list behind it; the bar
    takes the pointer events back. */}
<div className="pointer-events-none sticky bottom-6 z-10 flex justify-center px-6">
  <SelectionBar
    className="pointer-events-auto"
    count={selected.length}
    actions={actions}
    onClear={() => setSelected([])}
  />
</div>

Import

Copy this import. SelectionBar is self-contained — pass the count, the actions, and how to clear. It renders nothing when the count is 0, so it can stay mounted.

Self-contained — count, actions, onClear. It composes the kit Button, Separator, DropdownMenu and Tooltip; no provider needed.
import { SelectionBar } from "@/components/organisms/selection-bar";

Props

Two required inputs (count and onClear) plus the list of actions. Everything the Figma fixes — padding, radius, elevation, the separators, the 24px XCircle — is canonical and not exposed.

PropTypeDefaultDescription
countnumberHow many rows are selected. At 0 the component renders nothing, so it can stay mounted.
actionsSelectionBarAction[]{ label, onSelect?, icon?, disabled? }. Read in order; every one renders as an outline sm Button.
maxVisiblenumber3How many actions stay as buttons. The rest collapse into the overflow kebab.
label(count: number) => stringn => `${n} selected`The count sentence. Override to say what is selected ('3 accounts selected') or to count visible rows.
onClear() => voidEmpties the selection. Required — the bar always offers a way out of itself.
classNamestringMerged via cn(). Use it to dock the bar (pointer-events-auto inside a sticky wrapper); padding, radius and elevation are canonical.

When to use

  • Any list or table whose rows can be ticked, the moment the first one is.
  • Inside a sheet body as well as under a page-level table — it is the same bar in both.
  • When the actions apply to the selection as a set: replace, assign, remove, skip.

When not to use

  • As a page toolbar with filters and a search field — that is FilterBar / SearchToolbar.
  • For an editor's publish state and its single action — that is PublishBar.
  • For per-row actions: those belong in the row's own kebab, which stays available while a selection is held.

Usage

Do
  • Keep every action outline. The bar is already the emphasis; a blue button inside it competes with the bar instead of standing out of it.
  • Let actions past maxVisible collapse into the kebab rather than growing the bar until it spans the table.
  • Always offer the way out. A selection a user cannot drop is a mode they are stuck in.
  • Count what is on screen: if a filter is hiding selected rows, say so rather than letting 'all' mean rows nobody has seen.
Don't
  • Don't paint a destructive action red here. The tone belongs to the confirmation step; the bar stays neutral.
  • Don't draw the clear control as a circle with an X inside it — it is the XCircle glyph at 24px, fill.
  • Don't render an empty bar at zero selected. Nothing selected is not an empty bar, it is no bar.
  • Don't dock it by giving the component its own position — docking is context, and the recipe below is the canonical one.

Related

Cross-links to atoms and patterns you may reach for next.

  • TableWhere the selection is made. Its pinned checkbox column is the control this bar answers to.
  • Filter barThe page toolbar above the list. It also carries a Select all / Inverse / None cluster.
  • Publish barThe other bar in the kit. That one is chrome for an editor; this one only exists while a selection does.
  • Dropdown menuWhat the overflow kebab opens.