Command
Search and choose commands or options inline, in a popover or in a dialog.
import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandLoading, CommandSeparator, CommandShortcut, ListOption, OptionNote } from '@youj/react/command'
Installation & usage
npm i @youj/react cmdkFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Requires cmdk, an optional dependency. Import it from @youj/react/command.
Use Command to search a list of actions or options. Place it inline, in a Popover, or in CommandDialog. Use Command Palette for an app-wide shortcut with nested pages and back navigation. For short lists, use Select or Dropdown Menu.
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from '@youj/react/command'
export function CommandMenu() {
return (
<Command label="Command menu" className="rounded-xl border">
<CommandInput placeholder="Type a command or search…" />
<CommandList>
<CommandEmpty>No commands match.</CommandEmpty>
<CommandGroup heading="Project">
<CommandItem onSelect={deploy}>
Deploy to production
<CommandShortcut>⌘⇧D</CommandShortcut>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Go to">
<CommandItem onSelect={openInbox}>Inbox</CommandItem>
</CommandGroup>
</CommandList>
</Command>
)
}Command is cmdk, restyled. Typing filters and ranks the rows and keeps the first match active. onSelect runs on click and on Enter. A row's value is what the search matches (its text by default), and keywords add more terms. label names the search field for screen readers ("Search" by default). Arrow keys wrap around from the last row to the first.
Description
A command menu with grouped actions and shortcuts; choosing one reports what ran below.
A status filter: Command inside a Popover, with checked rows you can toggle.
Command in a Popover makes a picker or a faceted filter, the shape a data table toolbar uses. Give rows a checked state and each gets a trailing check that draws itself in, plus aria-checked for assistive technology, so the list works as a multi-select.
Results that arrive from a request, with a loading row while it runs.
When results come from a request, turn off cmdk's filtering with shouldFilter={false}, control the search, and show a CommandLoading row while the request runs. CommandEmpty waits while a loading row is shown, since nothing having arrived yet isn't the same as no results.
A command palette in a dialog, opened from a button.
CommandDialog puts a Command in a Dialog that hangs from the top third of the screen, so the input never moves as the list grows and shrinks. It's named "Command palette" for screen readers unless you pass a title. This one opens from a button: this site already uses ⌘K (Ctrl K on Windows and Linux) for its own search. For the full palette, with pages and a global shortcut, use Command Palette.
Code
command-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propschecked, indicator, wrapperClassName… (14)
Props
CommandItem
| Prop | Type | |
|---|---|---|
checked | boolean | 'mixed'For multi-select lists (a filter): a trailing check that draws in, and
aria-checked on the option. | – |
indicator | 'check' | 'checkbox'How default 'check' | 'check' |
Other props pass through: extends ComponentProps<typeof CommandPrimitive.Item>. | ||
CommandInput
| Prop | Type | |
|---|---|---|
wrapperClassName | string | – |
expanded | booleanWhether the list is showing. Leave unset when it always is (cmdk's default). | – |
CommandLoading
| Prop | Type | |
|---|---|---|
label | stringAnnounced politely while the row is shown, and shown when there are no children. default "Loading…" | "Loading…" |
Other props pass through: extends ComponentProps<'div'>. | ||
CommandSeparator
| Prop | Type | |
|---|---|---|
alwaysRender | boolean | – |
CommandDialog
| Prop | Type | |
|---|---|---|
title | stringNames the dialog for screen readers. default "Command palette" | "Command palette" |
description | stringDescribes the dialog for screen readers. | – |
className | string | – |
showCloseButton | booleandefault false | false |
presentation | OverlayPresentation'auto''sheet''popover'
default 'auto' | 'auto' |
autoFocusField | booleanWhether the search field takes focus on open. By default it does when
opened from a keyboard or a mouse, and not when opened by a touch, so the
on-screen keyboard doesn't cover the list before it is seen. Pass true
when the dialog is opened to type in (a search button); on iOS also call
| – |
childrenRequired | ReactNode | – |
sound | booleanWith sound on ( default true | true |
Command passes its props to cmdk's root: label, shouldFilter, filter, value and onValueChange for the active row, and loop, which is on here. CommandList, CommandGroup (heading), CommandEmpty and CommandShortcut take cmdk's or plain element props.
TouchInside a bottom sheet (a Combobox, a MultiSelect or a CommandDialog…
Touch
Inside a bottom sheet (a Combobox, a MultiSelect or a CommandDialog on touch) the parts restyle themselves: the search row becomes a filled search field that stays at the top while the list scrolls under it, rows are 48px with 16px text, and CommandShortcut hints are hidden. The highlight only shows while the field has focus, when Enter would pick it; otherwise a row darkens under the finger.
On a touch device CommandDialog is a tall bottom sheet instead of a dialog in the middle of the screen. Its height stays the same as results change, and while the on-screen keyboard is up the sheet sits on top of it, so the search field stays visible. Pull it down, tap outside it or press Escape to close it. It opens with the list in view and the keyboard down. If it opens from a search button, where people expect to type right away, pass autoFocusField. iOS only raises the keyboard for a focus that happens during the tap itself, and the field appears a moment later, so also call primeKeyboard() from @youj/react in that button's handler before you open the dialog.
presentation on CommandDialog chooses: auto (the default) is a dialog under a mouse or trackpad and a sheet on touch, sheet always uses the sheet, and popover keeps the dialog on touch too.
MotionActive row, list height, check… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Active rowone SlidingIndicator glides between rows and retargets while a key repeats | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
List heighteases as results come and go, up to --command-list-max | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
Checkchecked rows, draws in | duration.slowease.emphasized340ms · cubic-bezier(0.6, 0, 0.1, 1) | 340mscubic-bezier(0.6, 0, 0.1, 1) | |
Dialog openCommandDialog, rise and scale (yj-dialog) | duration.baseease.enter220ms · cubic-bezier(0.16, 1, 0.3, 1) | 220mscubic-bezier(0.16, 1, 0.3, 1) | |
| Dialog close | duration.fastease.exit140ms · cubic-bezier(0.4, 0, 1, 1) | 140mscubic-bezier(0.4, 0, 1, 1) |
Reduced motion
The highlight jumps to the active row, the list snaps to its new height, checks appear drawn and the dialog appears at once.
While you type, the highlight stays still: filtering keeps the first result active, so only the list around it changes height. The list is capped at 20rem by default and scrolls past that. Change the cap with the --command-list-max custom property on CommandList, as the examples above do.
Reduced motion
The active row's highlight jumps instead of gliding, and the list snaps to its new height. Checks appear already drawn, and CommandDialog opens and closes at once. Filtering, announcements and keyboard behavior don't change.
AccessibilityKeyboard, ARIA, focus
Accessibility
- The input is a
comboboxthat controls alistbox. The active row is tracked witharia-activedescendant, so focus stays in the input while you move through the results. - Groups are
groups named by their headings. Separators, the loading row and the highlight are hidden from assistive technology, because a listbox may only contain options and groups. - The list keeps a status region beside it. It announces "no results" and the loading label, which the listbox itself can't.
CommandDialogis a named dialog that focuses the input when it opens and returns focus to its trigger when it closes.
| Key | Action |
|---|---|
↑ / ↓ | Moves the active row, wrapping at the ends. Disabled rows are skipped. |
Home / End | Jumps to the first or last row. |
Ctrl+N / Ctrl+P | Next and previous row, for Emacs and Vim habits. |
Enter | Runs the active row. |
Escape | Closes CommandDialog. |