Overlays
Command
A filterable list of actions — the ⌘K surface.
Usage
import { Command } from '@misoto22/design'Notes
A filterable list of actions — the ⌘K surface.
Built on cmdk rather than on a Select or a menu, because the interaction is neither: the list is filtered as you type, the highlighted row moves with the arrow keys while focus STAYS in the input, and Enter runs the highlighted row. That is the ARIA combobox pattern, and it is the part nobody should hand-roll — aria-activedescendant moving without focus moving is precisely where a home-made palette stops working with a screen reader.
Anatomy
| Element | Description |
|---|---|
| Inputrequired | CommandInput, the combobox, with the search glyph and a hairline under it. A hard 52px tall rather than a --control-h token, so it does not move with data-density. |
| Listrequired | CommandList, capped at 18rem inline and raised to 26rem inside CommandDialog, scrolling on its own six-pixel hairline bar. |
| Item | A row: an icon ELEMENT, the label, an optional meta note and a shortcut printed as a Kbd. The highlighted row takes --accent-muted and a leading accent rule, because a fill alone is hard to catch while scrolling. |
| Empty | CommandEmpty. cmdk renders an empty state only if one exists in the tree, so without it an unmatched filter leaves the input over a blank strip. |
| Footer | CommandFooter and its CommandHint rows. A palette is a keyboard surface whose keys are invisible; this is the only place they get printed. |
Best practices
Do
- Reach for CommandDialog, not the bare Command. The root is an inline bordered box with no scrim, no focus trap and no Escape handling of its own, so a palette built from it alone stays open until something else closes it.
- Always render CommandEmpty. cmdk shows an empty state only when one is present, so the palette that omits it answers a filter matching nothing with an input above a blank strip and no explanation.
- Pass CommandItem an icon element — icon={<Search size={16} />} — when the glyph needs its own size or colour, and the component — icon={Search} — when it does not. Both spellings work here and in DropdownMenuItem; they used to be the reverse of each other one import apart.
- Bind ⌘K yourself and then print it: nothing in this component listens for a key, so the palette has no shortcut until the app adds a keydown handler, and a CommandFooter is where the reader finds out it exists.
Don’t
- Do not pass value to CommandItem unless the value is what a reader would type — cmdk filters on value first and only falls back to the row’s own text when there is none, so an id passed as the value makes the visible label unsearchable. Pass the label’s words as keywords when the id has to stay the value, which is what SearchableMenu does.
- CommandDialog renders with hideTitle and showClose={false}, so the palette has no visible heading and no visible close: Escape and the scrim are the only exits and neither announces itself. Print Escape in a CommandFooter rather than assuming it is known.
- Do not expect the palette to follow data-density — CommandInput is a hard 52px, not a --control-h token, so its field stays put while every other control in the app shrinks under compact.
Examples
inline
The palette's parts, laid out inline so all of them are visible at once: the combobox input, the filtered list with its groups and separator, and the footer that prints the keys. Reach for CommandDialog rather than this bare root in an application — the root is an inline bordered box with no scrim, no focus trap and no Escape handling of its own, so a palette built from it stays open until something else closes it. Always render CommandEmpty: cmdk shows an empty state only when one exists in the tree, so a palette without one answers an unmatched filter with a blank strip. And note the icon prop takes an ELEMENT here, the reverse of DropdownMenuItem one import away.
dialog
The palette as it is nearly always used: modal, above centre, opened by a key this example binds itself. Nothing in the component listens for a key — the shortcut exists only once the app adds a handler — and CommandDialog renders with hideTitle and showClose={false}, so there is no visible heading and no visible close. Escape and the scrim are the only exits and neither announces itself, which is why the shortcut is printed on the page here and why a real palette carries a CommandFooter. The handler bails on defaultPrevented so it does not fight another palette already bound to the same keys.
or press ⌘ K
values and keywords
cmdk filters on an item's value and only falls back to the row's own text when there is none — so passing an internal id as value makes the visible label unsearchable, which is the single most common way a palette ships broken. Every value here is what a reader would actually type, and the second half of each row is a keywords list for the words they might type instead: revert for a rollback, teammate for people. meta is the quiet note at the end of the row, for what kind of thing it is — not a description, because a palette that prints a sentence per row stops being scannable at about six.
Parts
Composed at the call site rather than configured through props, so a layout this component did not anticipate is still expressible.
CommandInput
The filter field. Carries the search icon and the combobox semantics.
Takes no props of its own.
CommandList
Takes no props of its own.
CommandEmpty
Shown when the filter matches nothing. Say what would match, not "no results".
Takes no props of its own.
CommandFooter
The key-hint strip along the bottom.
A palette is a keyboard surface whose keys are invisible: nothing on screen says the arrows move the row or that Enter runs it, and a reader who reaches for the mouse has been failed by the design rather than by themselves.
Takes no props of its own.
CommandHint
One key-and-verb pair inside a CommandFooter.
| Prop | Type | Default | Description |
|---|---|---|---|
| childrenrequired | ReactNode | What they do — a verb, lowercase, no sentence. | |
| keysrequired | string[] | The keys this hint describes, printed as `Kbd` chips. |
Also accepts everything in Omit<ComponentProps<'span'>, 'children'>. Those are forwarded to the underlying element and are not listed row by row.
CommandGroup
Takes no props of its own.
CommandSeparator
A divider between groups.
Marked presentational, because ARIA permits a listbox to contain only option and group — and the library renders this as role="separator", which puts a critical aria-required-children violation inside every palette that uses one. The grouping is already announced by the groups themselves, so removing the divider from the accessibility tree loses nothing.
Takes no props of its own.
CommandItem
| Prop | Type | Default | Description |
|---|---|---|---|
| icon | RemixiconComponentType | ReactNode | A leading glyph. Either spelling — `icon={<Settings size={16} />}` passes the element, `icon={Settings}` passes the component and this sizes it. It is what makes a long list scannable — the eye sorts by shape before it reads, and forty identical rows of text defeat that. | |
| meta | ReactNode | A quiet note at the end of the row — what kind of thing this is, or its current state. Not a description: a palette that prints a sentence per row stops being scannable at about six of them. | |
| shortcut | string | A shortcut printed at the end of the row. |
Also accepts everything in ComponentProps<typeof CommandPrimitive.Item>. Those are forwarded to the underlying element and are not listed row by row.
CommandDialog
The palette in a modal, which is how it is nearly always used.
The dialog's own padding is removed: a palette is edge-to-edge, and its input is the first thing focus lands on.
| Prop | Type | Default | Description |
|---|---|---|---|
| childrenrequired | ReactNode | — | |
| labelrequired | string | Names the palette for assistive tech. | |
| onOpenChangerequired | (open: boolean) => void | — | |
| openrequired | boolean | — | |
| inputLabel | string | Names the search field when it should read differently from the dialog — a "Search records" field inside a "Library search" palette. Defaults to `label`. An `aria-label` on `CommandInput` cannot do this: the field is labelled by reference, and a reference outranks an `aria-label`. | |
| onCloseAutoFocus | DialogContentProps['onCloseAutoFocus'] | Runs as the dialog hands focus back on close. The default returns it to a `DialogTrigger`, and a palette opened by a shortcut has none, so focus falls to the page. Prevent the default and focus what opened it. | |
| onEscapeKeyDown | DialogContentProps['onEscapeKeyDown'] | Runs before Escape closes the dialog. Prevent the default to keep it open, as a nested view stepping back does. | |
| onOpenAutoFocus | DialogContentProps['onOpenAutoFocus'] | Runs as the dialog moves focus in. Prevent the default to place focus yourself. | |
| shouldFilter | boolean | Pass `false` when the host has already filtered the rows — a server search or an index of its own. The palette would otherwise filter them a second time against the input and hide results the host meant to show. |
Keyboard
| Key | Does |
|---|---|
| ↑↓ | Moves the highlight. Focus stays in the input, so what you typed stays editable. |
| Enter | Runs the highlighted item. |
| Escape | Closes the palette. |
Accessibility
- The list filters as you type, the highlight moves with the arrow keys, and focus stays in the input. That last part is the ARIA combobox pattern and the part a home-made palette gets wrong.