The header checkbox needs three states: empty, partial, and checked. The indeterminate dash is not optional, and partial always resolves to select-all, never to clear.
Name the number. When select-all only grabs the rows on screen, offer 'Select all 247 matching' instead of a bare 'all' that hides the true scope.
Keep the count honest as context shifts. Change a filter and the label should re-read live (from 247 matching down to 96) so people act on the real set.
Selection is state, not the DOM. Shift-click picks a range, and the selected ids survive paging because they live in application state, not in the visible rows.
For destructive bulk actions, skip the confirm modal. Echo the count, run the action immediately, and offer a 10 second undo with a draining countdown ring.
Do / Don't
Do: Give the header checkbox all three states and let the partial dash resolve to select-all.
Do: Spell out the exact count in the affordance, like 'Select all 247 matching'.
Do: Swap destructive confirm dialogs for an undo window that echoes what you deleted.
Don't: Ship a two-state header checkbox that skips the indeterminate dash.
Don't: Label bulk selection with a vague 'all' that hides how many rows you touched.
Don't: Store the selection in the DOM, where it silently resets the moment someone changes page.
The Design System Blueprint
An 11-page visual guide to consistent, scalable UI: colors, typography, spacing, components, motion.