Layout
Command Menu
A dialog with a search field that opens a page or runs a command.
Last command: none
import * as React from 'react'
import { Button, CommandMenu, type CommandMenuGroup } from 'ferry-ui'
import { CreditCard, FolderKanban, LayoutDashboard, Plus, Search, UserPlus, Users } from 'lucide-react'
export default function CommandMenuHero() {
const [open, setOpen] = React.useState(false)
const [last, setLast] = React.useState('none')
// Navigation first, actions last. In a real app, a navigation item has an `href`.
const groups: CommandMenuGroup[] = [
{
id: 'pages',
label: 'Go to',
items: [
{ id: 'overview', label: 'Overview', icon: <LayoutDashboard />, onSelect: () => setLast('Go to Overview') },
{ id: 'projects', label: 'Projects', icon: <FolderKanban />, onSelect: () => setLast('Go to Projects') },
{ id: 'members', label: 'Members', icon: <Users />, onSelect: () => setLast('Go to Members') },
{ id: 'billing', label: 'Billing', icon: <CreditCard />, onSelect: () => setLast('Go to Billing') },
],
},
{
id: 'actions',
label: 'Actions',
items: [
{ id: 'new-project', label: 'New project', icon: <Plus />, onSelect: () => setLast('New project') },
{ id: 'invite', label: 'Invite member', icon: <UserPlus />, onSelect: () => setLast('Invite member') },
],
},
]
return (
<div className="flex flex-col items-center gap-3">
<Button icon={<Search />} onClick={() => setOpen(true)}>
Open the command menu
</Button>
<p className="text-[13px] text-foreground-light">Last command: {last}</p>
<CommandMenu open={open} onOpenChange={setOpen} groups={groups} />
</div>
)
}The search of this site is a command menu. Press ⌘ K or Ctrl K to open it.
NavGroup[] is a valid value for groups. See NavItem.The menu has one part. Its groups prop gives the commands.
import { CommandMenu } from 'ferry-ui'
<CommandMenu open={open} onOpenChange={setOpen} groups={groups} />An item with href opens a page through the link component of the app. An item with onSelect runs an action. The menu then closes.
const groups: CommandMenuGroup[] = [
{
id: 'pages',
label: 'Go to',
items: [{ id: 'invoices', label: 'Invoices', icon: <Receipt />, href: '/invoices' }],
},
{
id: 'actions',
label: 'Actions',
items: [{ id: 'new-invoice', label: 'New invoice', icon: <Plus />, onSelect: openNewInvoice }],
},
]Set shortcut. The menu then opens and closes on ⌘ K or Ctrl K, with no state in your code. A letter sets a different key: shortcut="j".
<CommandMenu groups={groups} shortcut />An item accepts more fields than a NavItem. For example, keepOpen keeps the menu open after the command runs. The API reference lists these fields.
import * as React from 'react'
import { Badge, Button, CommandMenu, useModKey, type CommandMenuGroup } from 'ferry-ui'
import { CreditCard, FolderKanban, KeyRound, Plus, Rows3 } from 'lucide-react'
export default function CommandMenuItemOptions() {
const [open, setOpen] = React.useState(false)
const [compact, setCompact] = React.useState(false)
const mod = useModKey()
const groups: CommandMenuGroup[] = [
{
id: 'pages',
label: 'Go to',
items: [
// `keywords`: the query "payment" finds this item.
{ id: 'billing', label: 'Billing', icon: <CreditCard />, hint: 'Plan and invoices', keywords: ['payment'] },
{ id: 'projects', label: 'Projects', icon: <FolderKanban />, badge: <Badge variant="info">New</Badge> },
],
},
{
id: 'actions',
label: 'Actions',
items: [
// `shortcut` shows the keys. Your code binds them.
{ id: 'new-project', label: 'New project', icon: <Plus />, shortcut: `${mod} N` },
{
id: 'compact',
label: 'Compact rows',
icon: <Rows3 />,
hint: compact ? 'On' : 'Off',
keepOpen: true,
onSelect: () => setCompact((value) => !value),
},
{ id: 'api-key', label: 'New API key', icon: <KeyRound />, hint: 'Admins only', disabled: true },
],
},
]
return (
<>
<Button onClick={() => setOpen(true)}>Open the command menu</Button>
<CommandMenu open={open} onOpenChange={setOpen} groups={groups} />
</>
)
}Set shouldFilter to false. The menu then shows each item that you pass. Get the results in onSearchChange. While the request is in progress, set loading.
import * as React from 'react'
import { Button, CommandMenu, type CommandMenuGroup } from 'ferry-ui'
import { FileText } from 'lucide-react'
const INVOICES = [
{ id: 'INV-2041', customer: 'Northwind Trading', amount: '$4,200.00' },
{ id: 'INV-2042', customer: 'Acme', amount: '$860.00' },
{ id: 'INV-2043', customer: 'Globex Logistics', amount: '$12,940.50' },
{ id: 'INV-2044', customer: 'Umbrella Health', amount: '$7,020.00' },
]
export default function CommandMenuRemote() {
const [open, setOpen] = React.useState(false)
const [results, setResults] = React.useState(INVOICES)
const [loading, setLoading] = React.useState(false)
const timer = React.useRef<number | undefined>(undefined)
// A timer plays the role of the server. The menu calls this function with "" each time it opens.
const search = (query: string) => {
window.clearTimeout(timer.current)
setLoading(true)
timer.current = window.setTimeout(() => {
const text = query.trim().toLowerCase()
setResults(INVOICES.filter((invoice) => `${invoice.id} ${invoice.customer}`.toLowerCase().includes(text)))
setLoading(false)
}, 500)
}
const groups: CommandMenuGroup[] = [
{
id: 'invoices',
label: 'Invoices',
items: results.map((invoice) => ({
id: invoice.id,
label: `${invoice.id} · ${invoice.customer}`,
icon: <FileText />,
hint: invoice.amount,
})),
},
]
return (
<>
<Button onClick={() => setOpen(true)}>Search invoices</Button>
<CommandMenu
open={open}
onOpenChange={setOpen}
groups={groups}
shouldFilter={false}
onSearchChange={search}
loading={loading}
loadingMessage="Search in progress…"
emptyMessage="No invoice matches."
placeholder="Search invoices by number or customer…"
/>
</>
)
}The children show after the groups. Use CommandGroup and CommandItem for a row with your own content. A custom row does not close the menu. Close the menu in the onSelect of the row.
import * as React from 'react'
import {
Avatar,
AvatarFallback,
Button,
CommandGroup,
CommandItem,
CommandMenu,
CommandSeparator,
toast,
type CommandMenuGroup,
} from 'ferry-ui'
import { FolderKanban } from 'lucide-react'
// In a real app, this item has an `href` or an `onSelect`.
const GROUPS: CommandMenuGroup[] = [
{ id: 'pages', label: 'Go to', items: [{ id: 'projects', label: 'Projects', icon: <FolderKanban /> }] },
]
const MEMBERS = [
{ name: 'Maya Chen', initials: 'MC', email: 'maya@example.com' },
{ name: 'Sam Lee', initials: 'SL', email: 'sam@example.com' },
]
export default function CommandMenuCustomRows() {
const [open, setOpen] = React.useState(false)
return (
<>
<Button onClick={() => setOpen(true)}>Open the command menu</Button>
<CommandMenu open={open} onOpenChange={setOpen} groups={GROUPS}>
<CommandSeparator />
<CommandGroup heading="Members">
{MEMBERS.map((member) => (
<CommandItem
key={member.email}
value={`${member.name} ${member.email}`}
onSelect={() => {
// A custom row does not close the menu. Close it in your code.
setOpen(false)
toast(`${member.name} selected`)
}}
>
<Avatar size="sm">
<AvatarFallback>{member.initials}</AvatarFallback>
</Avatar>
<span className="flex min-w-0 flex-col">
<span className="truncate text-foreground">{member.name}</span>
<span className="truncate text-[12px] text-foreground-lighter">{member.email}</span>
</span>
</CommandItem>
))}
</CommandGroup>
</CommandMenu>
</>
)
}title and description name the dialog for screen readers. They do not show.closeLabel and externalLabel set two more texts for screen readers. Use them to translate these texts.CommandMenu accepts only the props of this table.
| Prop | Type | Default |
|---|---|---|
groupsRequired | CommandMenuGroup[] | - |
Sections of commands, separated by hairlines while idle. | ||
open | boolean | - |
Controlled open state. Pair with | ||
defaultOpen | boolean | false |
Initial open state when uncontrolled. Defaults to | ||
shortcut | string | boolean | false |
Lets the menu bind its own global shortcut and toggle itself: | ||
onOpenChange | ((open: boolean) => void) | - |
Called when the menu opens or closes (Escape, outside click, after running a command). | ||
placeholder | string | Type a command or search… |
Placeholder of the search field. Defaults to | ||
title | string | Command menu |
Accessible dialog title (visually hidden). Defaults to | ||
description | string | Search for a page or run a command |
Accessible dialog description (visually hidden). Defaults to | ||
emptyMessage | ReactNode | No results found. |
Shown when no command matches the query. Defaults to | ||
loading | boolean | false |
Shows a loading row above the groups and hides the empty message, e.g. while remote results for the current query are fetched. | ||
loadingMessage | ReactNode | Loading… |
Text of the loading row. Defaults to | ||
onSearchChange | ((query: string) => void) | - |
Called when the query changes (and with | ||
shouldFilter | boolean | true |
Set | ||
onItemSelect | ((item: CommandMenuItem) => void) | - |
Called after any command runs (after its own | ||
linkComponent | LinkComponent | - |
Router-aware link used for | ||
closeLabel | string | - |
Accessible name of the close (X) button. Defaults to | ||
externalLabel | string | (opens in a new tab) |
Screen-reader text appended to the name of | ||
children | ReactNode | - |
Extra content appended inside the list, after the groups: | ||
className | string | - |
Extra classes for the dialog panel (e.g. to change its width). | ||
A CommandMenuItem has the fields of a NavItem and these fields. The menu ignores active.
| Field | Type | Role |
|---|---|---|
hint | ReactNode | Secondary text on the right. |
shortcut | ReactNode | A key hint on the right. The menu does not bind the key. |
keywords | string[] | More words that the search matches. |
value | string | The text that the search matches. The default is label. It must be unique. |
keepOpen | boolean | Keeps the menu open after the command runs. |
A CommandMenuGroup is a NavGroup with CommandMenuItem items. The menu does not show a group with no item.