Command palette
The ⌘K box. Search every record, run an action, go to a page or ask Flo, from anywhere. It grows out of the Ask bar and folds back into it.
Playground
Change the controls to try each item prop. The preview shows the list inline; in the app it sits in the 640px palette. The code under the preview lists only the props you changed.
import { CommandItem } from '@/ui/command';<CommandItem> Add person</CommandItem>Results
Type to filter. Groups come in this order: Flo, records by type, Recent, Actions, Go to. The top match takes the current fill; the arrows move it and Enter opens it. Record rows carry their mark and one subtle meta line.
import { PlusIcon, UsersIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandRecordItem } from '@/ui/command';
import { FloMark } from '@/ui/flo-mark';
import { KbdGroup } from '@/ui/kbd';
// ⌘K or / opens it in the app. Type to filter; Enter opens the top match; Tab hands the text to Flo.
export default function CommandDefault() {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('maya');
return (
<>
<Button onClick={() => setOpen(true)}>Search or ask Flo</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput value={query} onValueChange={setQuery} />
<CommandEmpty>No records match “{query}”.</CommandEmpty>
<CommandList>
<CommandGroup heading="Flo" forceMount>
<CommandItem value={`ask-flo ${query}`} forceMount>
<FloMark size="xsmall" />
Ask Flo about “{query}”
<KbdGroup keys={['Tab']} />
</CommandItem>
</CommandGroup>
<CommandGroup heading="People">
<CommandRecordItem type="candidate" name="Maya Chen" meta="Senior Data Engineer · Halcyon Bank" />
<CommandRecordItem type="candidate" name="Maya Okafor" meta="Head of Talent · Northbridge Talent" />
</CommandGroup>
<CommandGroup heading="Jobs">
<CommandRecordItem type="job" name="Senior Data Engineer" meta="Halcyon Bank · hiring manager Maya Chen" keywords={["maya chen"]} />
</CommandGroup>
<CommandGroup heading="Actions" forceMount>
<CommandItem forceMount>
<PlusIcon />
Add person
</CommandItem>
</CommandGroup>
<CommandGroup heading="Go to" forceMount>
<CommandItem forceMount>
<UsersIcon />
Go to People
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
}States and when things go wrong
Nothing typed shows Recent first. A search still out after 300ms shows a 2px line sweeping under the input; a fast search never does. With no match, one line names the query, outside the list so screen readers hear it once, and items that can't run sit at half opacity.
import { ClockCounterClockwiseIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandRecordItem } from '@/ui/command';
import { FloMark } from '@/ui/flo-mark';
import { Lozenge } from '@/ui/lozenge';
type State = 'recent' | 'searching' | 'none';
// Nothing typed shows Recent first. A search still out after 300ms shows the sweep line; a fast one never does. No match
// says so once, outside the list, and Ask Flo stays, disabled while it isn't built for this.
export default function CommandStates() {
const [state, setState] = useState<State | null>(null);
const query = state === 'searching' ? 'northbridge talent' : state === 'none' ? 'orbital frieght' : '';
return (
<div className="flex flex-wrap gap-100">
<Button onClick={() => setState('recent')}>Nothing typed</Button>
<Button onClick={() => setState('searching')}>Still searching</Button>
<Button onClick={() => setState('none')}>No match</Button>
<CommandDialog open={state !== null} onOpenChange={(open) => !open && setState(null)} shouldFilter={false}>
<CommandInput value={query} loading={state === 'searching'} readOnly />
<CommandEmpty show={state === 'none'}>No records match “{query}”.</CommandEmpty>
<CommandList>
{state === 'recent' && (
<CommandGroup heading="Recent">
<CommandRecordItem type="company" name="Halcyon Bank" />
<CommandRecordItem type="candidate" name="Sara Lund" />
</CommandGroup>
)}
{state === 'searching' && (
<CommandGroup heading="Companies">
<CommandRecordItem type="company" name="Northbridge Talent" />
</CommandGroup>
)}
{state === 'none' && (
<CommandGroup heading="Flo">
<CommandItem disabled>
<FloMark size="xsmall" />
Ask Flo: {query}
<Lozenge>Soon</Lozenge>
</CommandItem>
</CommandGroup>
)}
{state === 'recent' && (
<CommandGroup heading="Go to">
<CommandItem>
<ClockCounterClockwiseIcon />
Go to Inbox
</CommandItem>
</CommandGroup>
)}
</CommandList>
</CommandDialog>
</div>
);
}Motion
In the library the palette uses the Popup motion. In the app it grows out of the Ask bar, which is the Ask bar's own motion. Press Replay to close and open it again.
import { UsersIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { CommandDialog, CommandGroup, CommandInput, CommandItem, CommandList } from '@/ui/command';
// Replay closes the palette, then opens it again, so the 120ms exit and the 160ms entry run back to back.
export default function CommandMotion() {
const [open, setOpen] = useState(false);
const replay = () => {
setOpen(false);
setTimeout(() => setOpen(true), 400);
};
return (
<>
<Button variant="ghost" onClick={replay}>
Replay
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput />
<CommandList>
<CommandGroup heading="Go to">
<CommandItem>
<UsersIcon />
Go to People
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
}