Components › Forms & input › Select › Check menu

Check menu

canon

The one menu for toggling a set of options (columns, tiles, categories): checkbox on the right, optional search, hidden items below a line, reorder by drag or Alt+↑/↓; bottom sheet with Hotovo on a phone.

AD 30 Sep 2026: canon again. The one menu for toggling a set of OPTIONS (column visibility, tile layout, category filter) with optional search, hidden-below-divider and reorder; RecordSelect multi stays the picker of RECORDS. Select has no multiple mode, so the old replacedBy was void. approvedAt after the stories show search, hidden items, keyboard reorder (Alt+↑↓) and the phone sheet. Registry repaired 17 Oct 2026 (review F1): apps/web/src/ds/check-menu.css no longer exists; reorder styling is in ds/reorder.css (step F4). Registry repaired 17 Oct 2026 (review F2): removed classes with no CSS definition and no TSX use on main: .cm-reorder, .cm-dragging, .cm-drop-line, .cm-sheet. Registry repaired 17 Oct 2026 (review F4/F11): attached css reorder.css.

Used in 4 files · 4 areas · 4× — components/activity, components/mail, components/record, components/segment

Keyboard

  • Trigger supplied by the caller (button): Enter/Space open and close the menu through the anchor span's click.
  • On open, focus moves to the search field when there is one, otherwise to the first row.
  • ↑/↓, Home, End move focus between rows (canonical Popover behaviour). They do nothing while focus is in the search field — Tab moves from the search to the rows.
  • Enter or Space toggles the focused row; the menu stays open and focus stays on the row, so repeated toggling works. Keyboard activation (click.detail === 0) is never swallowed by the after-drag guard.
  • With onReorder: Alt+↑ / Alt+↓ moves the focused row one place. Focus stays on the moved row, so the key can be repeated. At the edge nothing moves, but the position is still announced. Popover ignores arrows with a modifier, so Alt+arrow never moves focus. Reorder is off while a query is typed.
  • Esc closes the menu and returns focus to the trigger; the key is consumed, so an enclosing dialog stays open.
  • Phone sheet: Hotovo (full width, 44px) and a tap on the scrim close it; no keyboard needed.

ARIA

  • Shell: role="menu" + aria-label (ariaLabel); the phone sheet uses the same role and label, and shows the label as its title.
  • Rows: button[role="menuitemcheckbox"] + aria-checked; the checkbox and icon are decorative.
  • Search: input with aria-label = the placeholder.
  • The group line is role="separator".
  • Reorder announcement: a visually hidden role="status" aria-live="polite" region next to the trigger (outside the menu, which may contain only items) says "{label}: pozice {pos} z {total}" after every Alt+arrow.
  • Draggable rows carry draggable and data-check-item; the insertion line is aria-hidden.
  • The trigger must set aria-haspopup="menu" and aria-expanded={open} itself.

Focus

  • Trigger and rows: foundation ring outline: 2px solid var(--accent); outline-offset: 1px.
  • Focus returns to the trigger on Esc; a click outside leaves focus where the user clicked.

Contrast

Computed from packages/ui/src/tokens/tokens.css:

  • Row label --ink-900 on --surface-100 = 17.59:1 (light), 14.53:1 (dark).
  • Icon --fg-secondary = 3.93:1 / 6.2:1 (non-text).
  • Checkbox on --accent = 4.44:1 (light) / 3.83:1 (dark); off border --border-strong 1.38:1 / 1.55:1.
  • Empty state --fg-muted on --surface-100 — secondary text.
  • Dragged row opacity: 0.45 ≈ 2.9:1 (light) — transient.

Known limits

  • Drag and drop is mouse only; on touch the order is changed elsewhere (e.g. the column ⋮ menu). Keyboard reorder (Alt+↑/↓) covers the desktop without a mouse.
  • The off-state checkbox border is a hairline (below 3:1); the state is carried by aria-checked.
  • Trigger ARIA depends on the caller.
  • The component is app-side; the catalog shows markup, not the live component.

Source: docs/select/check-menu.a11y.md · GitLab