Patterns

Selection actions

The link cluster beside a list you can pick from — the controls that change the selection wholesale. One rule, three states, and nothing else: nothing selected shows Select all; a partial selection shows Select all · Inverse · None; a complete one shows Unselect all. A control that cannot do anything is not shown.

Updated Aug 20, 2026 by Juan Pablo Turina

Anatomy

Three link Buttons at most, in a row. What renders is decided entirely by the ratio of selected to total, which is why the component takes numbers rather than booleans — a caller cannot express a state the rule does not have.

  1. 1
    Select all

    Shown while anything is left to select. Selects every visible row.

  2. 2
    Inverse

    Only in the partial state. Drops what was selected and picks up what was not. With a complete selection it would mean the same as None, which is why it goes away there.

  3. 3
    None

    Only in the partial state. Empties the selection. Called None, never Clear — one name for one action.

  4. 4
    Unselect all

    Replaces the whole cluster once everything is selected. Select all has nothing left to do, so it is not left on screen doing nothing.

The three states

There is no variant prop and no way to compose a fourth shape. These are all of them, and which one renders is a function of two numbers.

Try it

Tick and untick to watch the cluster change shape. Nothing else drives it.

Country
import { SelectionActions } from "@/components/patterns/selection-actions";

<SelectionActions
  total={visible.length}
  selected={visibleSelected.length}
  onAll={() => setSelected(visible.map((r) => r.id))}
  onInverse={() => setSelected(visible.filter((r) => !selected.includes(r.id)).map((r) => r.id))}
  onNone={() => setSelected([])}
/>
Nothing selected

Only Select all. Inverse would select everything (which Select all already does) and None would empty an empty selection.

Some selected

The full cluster. This is the only state where Inverse and None can do something the other controls cannot.

Everything selected

Unselect all, alone. The cluster collapses rather than showing three controls where one is a no-op and another is a synonym.

Scoped to a searchable list

The handlers act on what is shown and keep the selection the search is hiding. This is how FilterBar consumes it.

Country
{/* A searchable list: act on what is shown, keep what the search is hiding. */}
const visibleValues = options.map((o) => o.value);
const visibleSelected = selected.filter((v) => visibleValues.includes(v));
const hiddenSelected = selected.filter((v) => !visibleValues.includes(v));

<SelectionActions
  total={options.length}
  selected={visibleSelected.length}
  onAll={() => onSet([...hiddenSelected, ...visibleValues])}
  onInverse={() =>
    onSet([...hiddenSelected, ...visibleValues.filter((v) => !selected.includes(v))])
  }
  onNone={() => onSet(hiddenSelected)}
/>

Import

Copy this import. Pass the two counts and the three handlers; the component owns which controls appear.

Self-contained — two counts and three handlers. It composes the kit Button (link, sm); no provider needed.
import { SelectionActions } from "@/components/patterns/selection-actions";

Props

Two numbers decide everything that renders. total is what is VISIBLE, not what the list holds — see the usage notes.

PropTypeDefaultDescription
totalnumberHow many rows are VISIBLE, after any search or filter. At 0 the component renders nothing.
selectednumberHow many of those visible rows are selected. Together with total it decides the state.
onAll() => voidSelect every visible row.
onInverse() => voidSwap selected and unselected among the visible rows.
onNone() => voidEmpty the selection. Also backs Unselect all in the complete state.
classNamestringMerged via cn() onto the row.

When to use

  • Above or beside any multi-select list: a filter panel, a picker, a table with a checkbox column.
  • Whenever a user may want to act on most of a list rather than clicking rows one by one.
  • Next to a search field, where the visible set changes as the user types.

When not to use

  • To act ON a selection that already exists (delete, replace, assign) — that is SelectionBar.
  • On a single-select list, where selecting all is meaningless.
  • As a general link row — these four labels are the whole vocabulary.

Usage

Do
  • Pass the VISIBLE totals. On a searchable list the selection can hold rows the filter is hiding.
  • Scope the handlers to the visible rows too, and keep the hidden selection intact — the labels promise what is on screen.
  • Let the rule decide what renders. Read the state, do not re-derive it with your own conditions.
Don't
  • Don't leave Select all on screen once everything is selected — it is a control that cannot do anything.
  • Don't keep Inverse in the complete state, where it means the same as None.
  • Don't rename None to Clear, or Unselect all to Deselect all. One name per action.
  • Don't pass the full list length while showing a filtered list — that is how 'all' comes to mean rows nobody has seen.

Related

Cross-links to the atoms this molecule composes and sibling patterns.

  • Filter barThe kit's multi-select filter panel. It uses this cluster, scoped to the options the search is showing.
  • Selection barThe other half of selecting: this one changes the selection, that one acts on it.
  • CheckboxThe per-row control these links move in bulk.
  • TableIts pinned checkbox column is the canonical per-row selection.