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.
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.
- 1Select all
Shown while anything is left to select. Selects every visible row.
- 2Inverse
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.
- 3None
Only in the partial state. Empties the selection. Called None, never Clear — one name for one action.
- 4Unselect 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.
Tick and untick to watch the cluster change shape. Nothing else drives it.
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([])}
/>Only Select all. Inverse would select everything (which Select all already does) and None would empty an empty selection.
The full cluster. This is the only state where Inverse and None can do something the other controls cannot.
Unselect all, alone. The cluster collapses rather than showing three controls where one is a no-op and another is a synonym.
The handlers act on what is shown and keep the selection the search is hiding. This is how FilterBar consumes it.
{/* 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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| total | number | — | How many rows are VISIBLE, after any search or filter. At 0 the component renders nothing. |
| selected | number | — | How many of those visible rows are selected. Together with total it decides the state. |
| onAll | () => void | — | Select every visible row. |
| onInverse | () => void | — | Swap selected and unselected among the visible rows. |
| onNone | () => void | — | Empty the selection. Also backs Unselect all in the complete state. |
| className | string | — | Merged 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
- 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 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.