# SearchableMenu A menu of actions you can type into. - Group: Overlays - Import: `import { SearchableMenu } from '@misoto22/folio'` - Page: https://ui.misoto22.com/components/searchable-menu/ - Related: dropdown-menu, command, combobox ## When to reach for it A DropdownMenu past about a dozen rows stops being scannable, and nesting submenus makes it worse. This is the same list with a filter over it. Not a Command palette: that is page-level and modal; this is anchored to a control. ## Anatomy - **Trigger** (required) — A pill button built here rather than a Button: --control-h-sm tall, hairline bordered, with a chevron. Its accessible name is label, not the children you passed as its text. - **Panel** (required) — A Popover with its padding removed and a flat 16rem width, so the filter and the list run edge to edge inside it. Not modal — this is a Popover, not a Dialog. - **Filter** (required) — The Command input. Focus lands here on open and stays there; the highlight moves under it through aria-activedescendant. - **Rows** — One per MenuAction, as listbox options. shortcut prints as a Kbd at the end of the row, destructive paints it --danger, and selecting one closes the menu before running onSelect. - **Empty state** — emptyMessage, shown when the filter matches nothing. Say what would match rather than “no results”. ## Best practices ### Do - Give an action keywords for the word a reader reaches for that the label does not print — “download” for Export, “bin” for Delete. The label’s own text is lifted into the filter for you; a label built only from elements prints none, and development warns SEARCHABLE_MENU_LABEL_UNREADABLE rather than shipping a row nothing matches. - Make label the trigger’s visible text. It is set as aria-label on the trigger and overrides the children, so a button reading “Status” under a label of “Row actions” is announced as something the reader cannot say aloud (WCAG 2.5.3). - Keep actions that share a group next to each other in the array: groups are built by walking the list and extending only the LAST one, so the same group name appearing again after other rows produces a second heading with identical text. ### Don’t - It is a Popover underneath, so it is not modal: the page behind still scrolls while the filter is open and the panel is re-anchored as it does. A surface that should hold the page still is CommandDialog. - className lands on the TRIGGER, not on the panel — the panel is a flat 16rem with no prop that widens it, so a long label wraps to a second line instead of the box growing to take it. - The trigger is --control-h-sm — 36px comfortable, 30px under data-density="compact" — which is below the 44px pointer target (WCAG 2.5.5), and it is not the system Button, so variant and size do not reach it. ## Accessibility - Inside a bounded frame — a device preview, an embedded console — wrap the subtree in ``. The panel then renders into that element and collides with its edges instead of the viewport’s, and inherits the `dir` and `data-density` set there. - The rows are options inside a listbox rather than menuitems, because filtering requires it — the highlight moves through aria-activedescendant while focus stays in the input, and a menu cannot do that. - The trade is deliberate: a menu that cannot be filtered is worse for the reader than a listbox that runs actions. ## Keyboard - Enter / Space — Opens the menu. - ↑ / ↓ — Moves the highlight while focus stays in the filter. - Enter — Runs the highlighted action. - Escape — Closes without running anything. ## SearchableMenu A menu of actions you can type into. The fourth corner of a square the system otherwise had three of, and the distinction is worth stating because reaching for the wrong one is easy: | | few options | many options | |---|---|---| | **sets a value** | `Select` | `Combobox` | | **runs an action** | `DropdownMenu` | `SearchableMenu` | A `DropdownMenu` past about a dozen rows stops being scannable, and the usual response — nesting submenus — makes it worse. This is the same list with a filter over it. Not the same thing as `Command`: that is a page-level palette, usually modal and usually bound to ⌘K. This is anchored to a control, like the menu it replaces. The rows are `option`s inside a listbox rather than `menuitem`s, because that is what the filtering pattern requires — the highlight moves through `aria-activedescendant` while focus stays in the input, and a menu cannot do that. The trade is deliberate: a menu that cannot be filtered is worse for the reader than a listbox that runs actions. ### Props - `actions` (required) — `MenuAction[]`. - `children` (required) — `ReactNode`. The trigger's text. - `label` (required) — `string`. Names the menu for assistive tech. - `searchPlaceholder` — `string` default `'Filter…'`. - `emptyMessage` — `string` default `'Nothing matches.'`. - `align` — `'start' | 'center' | 'end'` default `'start'`. - `className` — `string`. ## Example — a filtered menu ```tsx import { SearchableMenu, type MenuAction } from '@misoto22/folio'
Actions

{last ? `ran: ${last}` : 'nine actions, one filter'}

``` ## Example — searchable keywords ```tsx import { SearchableMenu, Text, type MenuAction } from '@misoto22/folio'
Release actions {last ? `ran: ${last}` : 'four opaque ids, matched by their keywords'}
``` ## Example — groups in order ```tsx import { SearchableMenu, type MenuAction } from '@misoto22/folio'
Collection actions

{last ? `ran: ${last}` : 'two groups, written adjacent'}

```