Skip to content

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 cmdk

For 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.

Reference

Propschecked, indicator, wrapperClassName… (14)

Props

CommandItem

CommandItem props
PropType
checkedboolean | 'mixed'

For multi-select lists (a filter): a trailing check that draws in, and aria-checked on the option. 'mixed' is for a "select all" row over a partial selection. Leave undefined for plain commands.

–
indicator'check' | 'checkbox'

How checked shows: a trailing check (default) or a leading checkbox.

default 'check'

'check'
Other props pass through: extends ComponentProps<typeof CommandPrimitive.Item>.

CommandInput

CommandInput props
PropType
wrapperClassNamestring–
expandedboolean

Whether the list is showing. Leave unset when it always is (cmdk's default).

–

CommandLoading

CommandLoading props
PropType
labelstring

Announced politely while the row is shown, and shown when there are no children.

default "Loading…"

"Loading…"
Other props pass through: extends ComponentProps<'div'>.

CommandSeparator

CommandSeparator props
PropType
alwaysRenderboolean–

CommandDialog

CommandDialog props
PropType
titlestring

Names the dialog for screen readers.

default "Command palette"

"Command palette"
descriptionstring

Describes the dialog for screen readers.

–
classNamestring–
showCloseButtonboolean

default false

false
presentationOverlayPresentation
'auto''sheet''popover'

auto (the default) is a dialog under a pointer and a tall bottom sheet on touch; force either one.

default 'auto'

'auto'
autoFocusFieldboolean

Whether 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 primeKeyboard() in that button's tap handler, or iOS won't raise the keyboard for a focus that comes after the tap.

–
childrenRequiredReactNode–
soundboolean

With sound on (YoujProvider’s sound), opening plays open and closing close. false keeps this dialog silent.

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

Motion tokens
MomentTokens
Active rowone SlidingIndicator glides between rows and retargets while a key repeats
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
List heighteases as results come and go, up to --command-list-max
duration.baseease.standard

220ms · cubic-bezier(0.2, 0, 0, 1)

220mscubic-bezier(0.2, 0, 0, 1)
Checkchecked rows, draws in
duration.slowease.emphasized

340ms · 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.enter

220ms · cubic-bezier(0.16, 1, 0.3, 1)

220mscubic-bezier(0.16, 1, 0.3, 1)
Dialog close
duration.fastease.exit

140ms · 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 combobox that controls a listbox. The active row is tracked with aria-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.
  • CommandDialog is a named dialog that focuses the input when it opens and returns focus to its trigger when it closes.
KeyAction
↑ / ↓Moves the active row, wrapping at the ends. Disabled rows are skipped.
Home / EndJumps to the first or last row.
Ctrl+N / Ctrl+PNext and previous row, for Emacs and Vim habits.
EnterRuns the active row.
EscapeCloses CommandDialog.