Command

Command palette / fuzzy search menu.

Usage

The command-palette pattern. Bits owns the filtering, the scoring and arrow-key navigation, and it keeps focus in the Input while the highlight moves — so the user types and navigates without ever leaving the field. To open it in a modal, wrap the whole thing in a Dialog.

Try typing trash — it matches Delete, which does not contain the word.

Viewport is required

Command.Viewport goes inside List and wraps the content. It is not optional.

Bits takes the Input's aria-controls from the Viewport's id, and uses the Viewport as the insertion element when sorting filtered items. Omit it and the combobox is invalid ARIA — axe reports aria-required-attr, because a role="combobox" with aria-expanded must say what it controls.

This is not hypothetical: our own a11y suite caught a Command built without one, which is why it is called out here rather than left as a detail in the API table.

Naming the field and the list

There are two names to set, and the prop names do not make that obvious:

  • label on Root names the search field. It renders a visually hidden <label> that the Input references.
  • aria-label on List names the list. Bits defaults it to "Suggestions...", which says nothing about what the suggestions are — override it.

We deliberately leave Bits' default in place rather than silently substituting our own, so the behaviour matches their documentation. Setting both is on you.

Keywords and filtering

value is what the query matches against. Add keywords for the words a user would actually type but that are not in your label — trash on a Delete item, preferences on Settings. This is the difference between a palette people use and one they abandon because it never finds anything.

A group whose items are all filtered out is hidden along with its heading, so you never get a heading over nothing. Set shouldFilter=false when you filter server-side and render only what you want shown.

Async results

Use Loading while a request is in flight, not Empty. "No results" and "still loading" are different answers, and showing the first one during a fetch tells the user to stop typing when they should wait.

Always render one or the other — a palette that goes silently blank reads as broken.

Announcing results

Bits filters the list and announces nothing. Without Command.Status, a screen reader user types and the list shrinks in silence — they never learn whether they have forty matches or none.

Add it once, inside Root
<Command.Root label="Command palette">
  <Command.Input bind:value={search} />

  <!-- Once, anywhere inside Root. Pass a label in your language. -->
  <Command.Status label={(n) => (n === 1 ? '1 resultado' : `${n} resultados`)} />

  <Command.List aria-label="Commands">
    <Command.Viewport>…</Command.Viewport>
  </Command.List>
</Command.Root>

It is a visually hidden role="status" region. Hidden because the count is already on screen — the results are right there — so duplicating it visibly would be noise for everyone else.

Pass label. The default is English, and a count is exactly the kind of string that needs your language and plural rules.

delay is not a performance tweak. Without it, typing "button" fires six announcements — "48 results", "12 results", "4 results" — and the user hears a torrent instead of an answer. Each keystroke restarts the wait, so they get one answer once they stop typing. The default is 500ms.

It stays silent until the search is non-empty: the count of an unfiltered list is not news when a palette opens, and it falls silent again if the search is cleared.

Combobox needs the same thing and gets no component, deliberately — Bits does not filter there, you do, so you already know the count. Render your own region with it. There is nothing this library can add that you do not already have.

Props

Command.Root

PropTypeDefault
value The highlighted item's value. Bindable.string''
class Extra classes merged onto the root.string
child bits-ui Snippet<[{ props: Record<string, unknown>; }]> | undefined
children bits-ui Snippet<[]> | undefined
columns bits-ui The number of columns in a grid layout.number | null | undefinednull
disableInitialScroll bits-ui Whether to disable scrolling the selected item into view on initial mount. When `true`, prevents automatic scrolling when the command menu first renders and selects its first item, but still allows scrolling on subsequent selections.boolean | undefinedfalse
disablePointerSelection bits-ui Optionally set to `true` to disable selection via pointer events.boolean | undefined
filter bits-ui A custom filter function for whether each command item should match the query. It should return a number between `0` and `1`, with `1` being a perfect match, and `0` being no match, resulting in the item being hidden entirely. By default, it will use the `computeCommandScore` function exported by this package to compute the score.((value: string, search: string, keywords?: string[] | undefined) => number) | undefined
id bits-ui string | undefined
label bits-ui An accessible label for the command menu. Not visible & only used for screen readers.string | undefined
loop bits-ui Optionally set to `true` to enable looping through the items when the user reaches the end of the list using the keyboard.boolean | undefined
onStateChange bits-ui A function that is called when the command state changes.((state: Readonly<CommandState>) => void) | undefined
onValueChange bits-ui A function that is called when the selected command menu item changes. It receives the new value as an argument.((value: string) => void) | undefined
ref bits-ui HTMLElement | null | undefined
shouldFilter bits-ui Optionally set to `false` to turn off the automatic filtering and sorting. If `false`, you must conditionally render valid items yourself.boolean | undefined
vimBindings bits-ui Set this prop to `false` to disable the option to use ctrl+n/j/p/k (vim style) navigation.boolean | undefinedtrue

Command.Input

PropTypeDefault
value The search query. Bindable.string''
class Extra classes merged onto the input.string
child bits-ui Snippet<[{ props: Record<string, unknown>; }]> | undefined
children bits-ui Snippet<[]> | undefined
id bits-ui string | undefined
ref bits-ui HTMLElement | null | undefined

Command.List

PropTypeDefault
class Extra classes merged onto the list.string
child bits-ui Snippet<[{ props: Record<string, unknown>; }]> | undefined
children bits-ui Snippet<[]> | undefined
id bits-ui string | undefined
ref bits-ui HTMLElement | null | undefined

Command.Item

PropTypeDefault
class Extra classes merged onto the item.string
child bits-ui Snippet<[{ props: Record<string, unknown>; }]> | undefined
children bits-ui Snippet<[]> | undefined
disabled bits-ui Whether the item is disabled.boolean | undefinedfalse
forceMount bits-ui Whether to always mount the item regardless of filtering logic.boolean | undefined
id bits-ui string | undefined
keywords bits-ui A list of keywords that will be used to filter the item.string[] | undefined
onSelect bits-ui A callback that is fired when the item is selected, either via click or keyboard selection.(() => void) | undefined
ref bits-ui HTMLElement | null | undefined
value bits-ui A unique value for this item that will be used when filtering and ranking the items. If not provided, an attempt will be made to use the `textContent` of the item. If the `textContent` is dynamic, you will need to provide a stable unique value for the item.string | undefined

Command.Status

PropTypeDefault
label Builds the announced text from the result count. Pass your own. The default is English, and a count is exactly the kind of string that needs the app's language and plural rules — `1 result` versus `1 resultado` versus languages with more than two plural forms.(count: number) => string(count: number) => count === 0 ? 'No results' : count === 1 ? '1 result' : `${count} results`
delay Milliseconds to wait after the last keystroke before announcing. Not a performance tweak. Without it, typing "button" fires six announcements — "48 results", "12 results", "4 results" — and a screen reader user hears a torrent instead of an answer. Long enough to let typing settle, short enough to arrive while the search is still the subject.number500
class Extra classes merged onto the region.string

Command.Item

Input, List, Group, GroupHeading, Empty, Separator and Loading take class plus their native attributes. Viewport, GroupItems and LinkItem are re-exported from Bits unchanged.