CommandMenu
Filter and invoke grouped actions with pointer or keyboard navigation.
Installation
Import from the Reflow workspace package.
Playground
Open demoimport { CommandMenu } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const commands = [
{ value: 'new', label: 'New project', group: 'Actions' },
{ value: 'settings', label: 'Settings', group: 'Pages' },
{ value: 'search', label: 'Search files', group: 'Actions' },
]
return () => (
<>
<CommandMenu items={commands} filters={['Actions', 'Go to', 'Help']} suggestions={['calendar', 'new-file', 'settings']} />
</>
)
}Basic
Type to filter, press ↓ ↑ to move, Enter to run. Rows come from items, grouped by group.
import { CommandMenu } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const commands = [
{ value: 'new', label: 'New project', group: 'Actions' },
{ value: 'settings', label: 'Settings', group: 'Pages' },
{ value: 'search', label: 'Search files', group: 'Actions' },
]
const runCommand = (value: string) => { console.log('Selected', value) }
return () => (
<>
<CommandMenu items={commands} onSelect={runCommand} />
</>
)
}Descriptions, shortcuts, disabled rows
3 more fields per row: description, shortcut caps, and disabled, which the arrows skip. Type clipboard: keywords match too.
import { CommandMenu } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
let plan = 'free'
const select = (value: string) => { plan = value; handle.update() }
const runCommand = (value: string) => { console.log('Selected', value) }
return () => (
<>
<CommandMenu items={[
{ value: 'new-file', label: 'New file', description: 'Blank document', shortcut: 'mod+n', keywords: ['create'] },
{ value: 'settings', label: 'Settings', shortcut: 'mod+,' },
{ value: 'export', label: 'Export as PDF', description: 'Pro plan', disabled: true },
]} onSelect={runCommand} />
</>
)
}Suggestions
3 suggested rows lead until you type 1 letter, then the groups take over.
import { CommandMenu } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const commands = [
{ value: 'new', label: 'New project', group: 'Actions' },
{ value: 'settings', label: 'Settings', group: 'Pages' },
{ value: 'search', label: 'Search files', group: 'Actions' },
]
const runCommand = (value: string) => { console.log('Selected', value) }
return () => (
<>
<CommandMenu items={commands} suggestions={['calendar', 'new-file', 'settings']} suggestionsLabel="Recent" onSelect={runCommand} />
</>
)
}Tabs
Click a tab or press ← →, and the rows follow. Controls hug the end of the row beneath the field.
import { CommandMenu } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const commands = [
{ value: 'new', label: 'New project', group: 'Actions' },
{ value: 'settings', label: 'Settings', group: 'Pages' },
{ value: 'search', label: 'Search files', group: 'Actions' },
]
const runCommand = (value: string) => { console.log('Selected', value) }
return () => (
<>
<CommandMenu filters={['Actions', 'Go to', 'Help']} items={commands} onSelect={runCommand} />
</>
)
}Filters
A compact, borderless row under the field for selects or ghost buttons; the list divider closes it.
import { CommandMenu, Select } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const options = ['Apple', 'Banana', 'Cherry', 'Mango'].map((label) => ({ value: label.toLowerCase(), label }))
const commands = [
{ value: 'new', label: 'New project', group: 'Actions' },
{ value: 'settings', label: 'Settings', group: 'Pages' },
{ value: 'search', label: 'Search files', group: 'Actions' },
]
const runCommand = (value: string) => { console.log('Selected', value) }
return () => (
<>
<CommandMenu items={commands} controls={<>
<Select variant="ghost" label="Type" options={types} />
<Select variant="ghost" label="Sort" options={sorts} />
</>} onSelect={runCommand} />
</>
)
}Dialog and trigger shortcut
Press the combo anywhere on the page. A pick or Escape closes it.
import { CommandMenu, CommandShortcut } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const commands = [
{ value: 'new', label: 'New project', group: 'Actions' },
{ value: 'settings', label: 'Settings', group: 'Pages' },
{ value: 'search', label: 'Search files', group: 'Actions' },
]
const runCommand = (value: string) => { console.log('Selected', value) }
return () => (
<>
<CommandMenu trigger={<>Open <CommandShortcut keys="mod+p" /></>} shortcut="mod+p" suggestions={['calendar', 'new-file', 'settings']} items={commands} onSelect={runCommand} />
</>
)
}Custom rows
renderItem replaces the row content; the icon and caps stay in your hands.
import { CommandMenu } from '@rtzll/reflow'
import { type Handle } from 'remix/component'
export function Example(handle: Handle) {
const commands = [
{ value: 'new', label: 'New project', group: 'Actions' },
{ value: 'settings', label: 'Settings', group: 'Pages' },
{ value: 'search', label: 'Search files', group: 'Actions' },
]
const runCommand = (value: string) => { console.log('Selected', value) }
return () => (
<>
<CommandMenu items={commands} renderItem={(item) => (
<><span>{item.label}</span><span>{item.group}</span></>
)} onSelect={runCommand} />
</>
)
}API Reference
Public props for the Reflow implementation. Native controls also accept their standard HTML attributes and Remix event mixes.
CommandMenu
| Prop | Type | Default | Description |
|---|---|---|---|
items* | readonly CommandItem[] | — | Items and their content. |
renderItem | ((item: CommandItem) => RemixNode) | — | Replaces the label and description of a row. The icon and shortcut caps stay. |
footer | RemixNode | boolean | — | true shows the key hints that follow the menu; a node replaces them. Omitted, there is no footer. |
suggestions | readonly string[] | — | Command values listed first, under suggestionsLabel, while nothing is typed. |
suggestionsLabel | string | — | Heading of the suggestions group. Defaults to Suggestions. |
controls | RemixNode | — | Compact controls (ghost selects or buttons) in a row under the field, or at the end of the tab row. |
emptyMessage | string | — | Shown when nothing matches. Defaults to No results. |
label | string | Command menu | Accessible name or visible label for the control. |
placeholder | string | Type a command or search… | Hint shown while the value is empty. |
filters | readonly string[] | — | Group names to expose as filters. Left and Right switch filters from the search field. |
trigger | RemixNode | — | Supply a trigger to open the menu in a modal. |
inDialog | boolean | — | Enable dialog focus and dismissal hints when supplying your own dialog. |
shortcut | boolean | string | — | The combo that toggles a modal menu: true or omitted is Mod+K, a string such as mod+p sets another, false binds nothing. |
onSelect | ((value: string) => void) | — | Called when an item is selected. |
CommandItem
| Prop | Type | Default | Description |
|---|---|---|---|
value* | string | — | Identifier passed to onSelect when the command is invoked. |
label* | string | — | Accessible name or visible label for the control. |
description | string | — | Additional explanatory text. |
group | string | — | Label used to group related items. |
keywords | readonly string[] | — | Additional terms matched by search. |
disabled | boolean | — | Prevents interaction and conveys the disabled state. |
shortcut | string | — | Visible keyboard hint. The consumer registers the action shortcut. |
icon | RemixNode | — | Icon content. |