Command

Searchable command palette for quick actions.

import { Command, useCommandState } from 'ui-lab-components/command';
"use client";
 
import { useMemo, useState, type CSSProperties } from 'react';
import { Icon } from 'ui-lab-components/icon';
import { Button } from 'ui-lab-components/button';
import { Command, type CommandItemData, useCommandContext, useCommandState } from 'ui-lab-components/command';
 
const commandData: Omit<CommandItemData, 'action'>[] = [
  { id: 'projects', label: 'Open project', description: 'Browse your workspace projects', category: 'Navigate', shortcut: '⌘P', icon: <Icon id="folder" size={16} /> },
  { id: 'settings', label: 'Settings', description: 'Manage workspace preferences', category: 'Navigate', shortcut: '⌘,', icon: <Icon id="gear" size={16} /> },
  { id: 'create', label: 'New document', description: 'Create a blank document', category: 'Create', shortcut: '⌘N', icon: <Icon id="file-plus" size={16} /> },
];
 
function Results() {
  const { filteredItems } = useCommandContext();
  return (
    <Command.List empty-message="No matching commands. Try a different search.">
      {filteredItems.length > 0 ? (
        <Command.Groups
          renderCategory={(category) => category ? <Command.Category>{category}</Command.Category> : null}
          renderItem={(cmd) => (
            <Command.Item value={cmd.id} textValue={cmd.label} action={cmd.action} icon={cmd.icon} hint={cmd.shortcut}>
              <div className="min-w-0">
                <Command.Label>{cmd.label}</Command.Label>
                <Command.Description>{cmd.description}</Command.Description>
              </div>
            </Command.Item>
          )}
        />
      ) : null}
    </Command.List>
  );
}
 
export default function Example() {
  const [container, setContainer] = useState<HTMLDivElement | null>(null);
  const [, setSelected] = useState('');
  const commands = useMemo(() => commandData.map((cmd) => ({ ...cmd, action: () => setSelected(cmd.label) })), []);
  const command = useCommandState({ defaultOpen: true });
 
  return (
    <div className="w-full">
      <div ref={setContainer} className="[&>[data-slot=overlay]]:bg-transparent! [&>[data-slot=overlay]]:backdrop-filter-none!" style={{ position: 'relative', isolation: 'isolate', overflow: 'hidden', width: '100%', height: 520 }}>
        <div className="flex h-full items-center justify-center">
          <Command.Trigger state={command} asChild>
            <Button>Open Palette</Button>
          </Command.Trigger>
        </div>
        <Command
          state={command}
          items={commands}
          container={container}
          style={{ '--origin': '50% 6%', '--width': '28rem', '--height': '28rem', height: '28rem', minHeight: '28rem' } as CSSProperties}
        >
          <Command.Input autoFocus={false} placeholder="Search commands..." />
          <Results />
          <Command.Footer actions={{ left: [{ label: 'Navigate', hint: ['↑', '↓'] }, { label: 'Select', hint: 'Enter' }], right: [{ label: 'Close', hint: 'Esc' }] }} />
        </Command>
      </div>
    </div>
  );
}
 

command/01-basic-command

An always-visible floating palette scoped to its preview, with grouped commands, icons, descriptions, shortcuts, keyboard navigation, and selection feedback.

Loading interactive preview
'use client';
 
import React from 'react';
import { Icon } from 'ui-lab-components/icon';
import { Command, type CommandItemData, useCommandContext } from 'ui-lab-components/command';
 
const commandData: Omit<CommandItemData, 'action'>[] = [
  { id: 'search', label: 'Search documents', description: 'Find anything in your workspace', category: 'Workspace', shortcut: '⌘F', icon: <Icon id="search" size={16} /> },
  { id: 'projects', label: 'Browse projects', description: 'Jump to an active project', category: 'Workspace', shortcut: '⌘P', icon: <Icon id="folder" size={16} /> },
  { id: 'create', label: 'Create document', description: 'Start with a blank page', category: 'Actions', shortcut: '⌘N', icon: <Icon id="file-plus" size={16} /> },
  { id: 'invite', label: 'Invite teammate', description: 'Bring someone into your workspace', category: 'Actions', icon: <Icon id="users" size={16} /> },
  { id: 'settings', label: 'Workspace settings', category: 'Settings', shortcut: '⌘,', icon: <Icon id="gear" size={16} /> },
];
 
function Results() {
  const { filteredItems } = useCommandContext();
  return (
    <Command.List empty-message="No commands found. Try ‘project’ or ‘create’.">
      {filteredItems.length > 0 ? (
        <Command.Groups
          renderCategory={(category) => category ? <Command.Category>{category}</Command.Category> : null}
          renderItem={(cmd) => (
            <Command.Item value={cmd.id} textValue={cmd.label} action={cmd.action} icon={cmd.icon} hint={cmd.shortcut}>
              <div className="min-w-0">
                <Command.Label>{cmd.label}</Command.Label>
                {cmd.description && <Command.Description>{cmd.description}</Command.Description>}
              </div>
            </Command.Item>
          )}
        />
      ) : null}
    </Command.List>
  );
}
 
export default function Example() {
  const [container, setContainer] = React.useState<HTMLDivElement | null>(null);
  const [, setSelected] = React.useState('');
  const commands = React.useMemo(() => commandData.map((cmd) => ({ ...cmd, action: () => setSelected(cmd.label) })), []);
 
  return (
    <div className="w-full">
      <div ref={setContainer} className="[&>[data-slot=overlay]]:bg-transparent! [&>[data-slot=overlay]]:backdrop-filter-none!" style={{ position: 'relative', isolation: 'isolate', overflow: 'hidden', width: '100%', height: 520 }}>
        <Command
          open
          container={container}
          items={commands}
          style={{ '--origin': '50% 6%', '--width': '28rem', '--height': '28rem', height: '28rem', minHeight: '28rem' } as React.CSSProperties}
        >
          <Command.Input autoFocus={false} placeholder="Search your workspace..." />
          <Results />
          <Command.Footer actions={{ left: [{ label: 'Navigate', hint: ['↑', '↓'] }], right: [{ label: 'Select', hint: 'Enter' }] }} />
        </Command>
      </div>
    </div>
  );
}
 

command/02-sidebar-panel

Command.Root used as a headless primitive — no dialog, portal, or trigger — dropped inline into a Card for a sidebar-style quick switcher.

Loading interactive preview
'use client';
 
import React from 'react';
import { Icon } from 'ui-lab-components/icon';
import { Command, type CommandItemData, useCommandContext } from 'ui-lab-components/command';
import { Card } from 'ui-lab-components/card';
import { List } from 'ui-lab-components/list';
import { Scroll } from 'ui-lab-components/scroll';
import { Flex } from 'ui-lab-components/flex';
import { Button } from 'ui-lab-components/button';
import { Input } from 'ui-lab-components/input';
import { Label } from 'ui-lab-components/label';
 
const initialProjects = [
  { id: 'WEB-101', label: 'Website refresh', description: 'In progress · Maya Chen · 18 issues', keywords: ['marketing', 'design', 'website'] },
  { id: 'APP-102', label: 'Mobile onboarding', description: 'In review · Alex Rivera · 8 issues', keywords: ['mobile', 'activation', 'onboarding'] },
  { id: 'API-103', label: 'Public API v2', description: 'Planning · Sam Patel · 12 issues', keywords: ['developer', 'platform', 'api'] },
];
 
function Results({ current }: { current: string }) {
  const { filteredItems, groupedItems, focusedKey, setFocusedKey, actionRef, scrollableRef } = useCommandContext();
 
  React.useEffect(() => {
    filteredItems.forEach((item) => actionRef.current.set(item.id, item.action));
    return () => {
      filteredItems.forEach((item) => actionRef.current.delete(item.id));
    };
  }, [filteredItems, actionRef]);
 
  return (
    <Scroll viewport-ref={scrollableRef} max-height="100%" fade-y className="min-h-0 overflow-hidden px-2 pb-2">
      {filteredItems.length ? (
        <List layout="spaced" aria-label="Workspace navigation" highlighted={focusedKey as string | null}>
          {groupedItems.flatMap(({ category, items }) => [
            ...(category ? [
              <List.Header key={`category-${category}`} className="px-3 pt-4 pb-2 text-sm text-foreground-400">{category}</List.Header>,
            ] : []),
            ...items.map((item) => (
              <List.Item
                key={item.id}
                value={item.id}
                interactive
                selected={current === item.id}
                aria-current={current === item.id ? 'page' : undefined}
                onClick={() => item.action()}
                onFocus={() => setFocusedKey(item.id)}
                onKeyDown={(event) => {
                  if (event.key === 'Enter' || event.key === ' ') {
                    event.preventDefault();
                    item.action();
                  }
                }}
              >
                <List.Media>{item.icon}</List.Media>
                <Flex direction="column" gap={1} className="min-w-0">
                  <List.Title>{item.label}</List.Title>
                  <List.Desc>{item.description}</List.Desc>
                </Flex>
              </List.Item>
            )),
          ])}
        </List>
      ) : (
        <Flex className="px-3 py-6 text-sm text-foreground-300">No matches. Try a project name or ID.</Flex>
      )}
    </Scroll>
  );
}
 
export default function Example() {
  const [projects, setProjects] = React.useState(initialProjects);
  const [current, setCurrent] = React.useState('WEB-101');
  const [creating, setCreating] = React.useState(false);
  const [name, setName] = React.useState('');
  const [error, setError] = React.useState('');
  const projectNameId = React.useId();
  const commands = React.useMemo<CommandItemData[]>(() => [
    { id: 'my-issues', label: 'My issues', description: '6 assigned to you · 2 due today', category: 'Workspace', icon: <Icon id="inbox" size={18} aria-hidden="true" />, keywords: ['assigned', 'tasks'], action: () => setCurrent('my-issues') },
    { id: 'all-projects', label: 'All projects', description: `${projects.length} projects across your team`, category: 'Workspace', icon: <Icon id="layers" size={18} aria-hidden="true" />, keywords: ['team', 'browse'], action: () => setCurrent('all-projects') },
    ...projects.map((project) => ({
      ...project,
      description: `${project.id} · ${project.description}`,
      category: 'Recent projects',
      icon: <Icon id="folder" size={18} aria-hidden="true" />,
      keywords: [project.id, ...project.keywords],
      action: () => setCurrent(project.id),
    })),
  ], [projects]);
  const currentLabel = commands.find((item) => item.id === current)?.label;
 
  function createProject(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const label = name.trim();
    if (!label) {
      setError('Enter a project name.');
      return;
    }
    if (projects.some((project) => project.label.toLowerCase() === label.toLowerCase())) {
      setError('A project with this name already exists.');
      return;
    }
    const id = `PRJ-${104 + projects.length - initialProjects.length}`;
    setProjects((previous) => [{ id, label, description: 'Planning · Maya Chen', keywords: [label] }, ...previous]);
    setCurrent(id);
    setCreating(false);
    setName('');
    setError('');
  }
 
  return (
    <Card className="grid h-[28rem] w-[28rem] max-w-[calc(100%-2rem)] grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden bg-background-900 p-0">
      <Card.Header className="border-b border-background-800 px-5 py-4">
        <Flex align-center gap={3}>
          <Flex layout="center" className="h-9 w-9 shrink-0 rounded-sm bg-background-700 text-foreground-50 font-semibold" aria-hidden="true">F</Flex>
          <Flex direction="column" gap={1}>
            <strong>Forma</strong>
            <Flex className="text-sm text-foreground-300">Product team · 12 members</Flex>
          </Flex>
        </Flex>
      </Card.Header>
      <Command.Root items={commands} className="grid min-h-0 grid-rows-[auto_minmax(0,1fr)_auto]">
        <Flex className="px-3 pt-3 pb-1">
          <Command.Input aria-label="Search workspace" placeholder="Search projects or jump to…" autoFocus={false} />
        </Flex>
        <Results current={current} />
        <Command.Footer
          className="px-5 py-3 text-foreground-400"
          actions={{ left: [{ label: 'Navigate', hint: ['↑', '↓'] }], right: [{ label: 'Open', hint: 'Enter' }] }}
        />
      </Command.Root>
      <Card.Footer className="border-t border-background-800 px-3 py-3">
        <Flex direction="column" gap={3}>
          {creating ? (
            <form onSubmit={createProject}>
              <Flex direction="column" gap={3}>
                <Flex direction="column" gap={2}>
                  <Label htmlFor={projectNameId} error={Boolean(error)}>Project name</Label>
                  <Input id={projectNameId} value={name} onChange={(event) => { setName(event.target.value); setError(''); }} placeholder="e.g. Customer portal" autoFocus required maxLength={60} error={Boolean(error)} aria-invalid={Boolean(error)} aria-describedby={error ? `${projectNameId}-error` : undefined} />
                  {error && <Flex id={`${projectNameId}-error`} role="alert" className="text-sm text-danger-400">{error}</Flex>}
                </Flex>
                <Flex layout="actions" gap={2}>
                  <Button variant="ghost" onPress={() => { setCreating(false); setName(''); setError(''); }}>Cancel</Button>
                  <Button variant="primary" type="submit" disabled={!name.trim()}>Create project</Button>
                </Flex>
              </Flex>
            </form>
          ) : (
            <Button variant="ghost" icon={<Icon id="plus" size={16} />} className="w-full justify-start" onPress={() => setCreating(true)}>Create project</Button>
          )}
          <Flex role="status" className="sr-only">Viewing {currentLabel}</Flex>
        </Flex>
      </Card.Footer>
    </Card>
  );
}
 

command/03-inline-completion

Command.Root wired to a textarea through useCompletion: typing / filters commands, arrows and Enter drive the list while focus stays in the field.

Loading interactive preview
'use client';
 
import { useMemo, useRef, useState, type KeyboardEvent, type RefObject } from 'react';
import { Command, type CommandItemData, useCommandContext, useCompletion } from 'ui-lab-components/command';
import { Icon } from 'ui-lab-components/icon';
import { Input } from 'ui-lab-components/input';
import { List } from 'ui-lab-components/list';
import { Scroll } from 'ui-lab-components/scroll';
 
// Only keyboard moves scroll the active row into view; hover-driven scrolling would chase the pointer.
function Suggestions({ keyboardRef }: { keyboardRef: RefObject<boolean> }) {
  const { filteredItems, focusedKey, setFocusedKey } = useCommandContext();
  if (!filteredItems.length) return null;
  return (
    <div className="absolute inset-x-0 top-full z-10 mt-2 rounded-sm border border-background-700 bg-background-800 px-1">
      <Scroll max-height="14rem" fade-y padding-y={1}>
        <List aria-label="Commands" layout="spaced" highlighted={keyboardRef.current ? (focusedKey as string | null) : null}>
          {filteredItems.map((item) => (
            <List.Item
              key={item.id}
              value={item.id}
              interactive
              data-highlighted={item.id === focusedKey ? 'true' : 'false'}
              onPointerEnter={() => {
                keyboardRef.current = false;
                setFocusedKey(item.id);
              }}
              onMouseDown={(event) => event.preventDefault()}
              onClick={() => item.action()}
              className="gap-2 px-2 py-1.5 text-sm"
            >
              <List.Media>{item.icon}</List.Media>
              <List.Title>{item.description}</List.Title>
              <span className="ml-auto pl-6 text-foreground-400">/{item.label}</span>
            </List.Item>
          ))}
        </List>
      </Scroll>
    </div>
  );
}
 
export default function Example() {
  const [value, setValue] = useState('');
  const completion = useCompletion({ value, onValueChange: setValue, triggers: ['/'], atStart: true });
  const keyboardRef = useRef(false);
  const commands = useMemo<CommandItemData[]>(
    () =>
      [
        { id: 'review', description: 'Review changes', icon: <Icon id="branch" size={16} /> },
        { id: 'test', description: 'Write tests', icon: <Icon id="check" size={16} /> },
        { id: 'debug', description: 'Debug an error', icon: <Icon id="code" size={16} /> },
        { id: 'explain', description: 'Explain this file', icon: <Icon id="file" size={16} /> },
        { id: 'search', description: 'Search the codebase', icon: <Icon id="search" size={16} /> },
        { id: 'refactor', description: 'Refactor a component', icon: <Icon id="folder-code" size={16} /> },
        { id: 'perf', description: 'Profile performance', icon: <Icon id="gauge" size={16} /> },
        { id: 'notes', description: 'Summarize the session', icon: <Icon id="notes" size={16} /> },
        { id: 'agent', description: 'Hand off to an agent', icon: <Icon id="agent" size={16} /> },
        { id: 'settings', description: 'Adjust model settings', icon: <Icon id="sliders" size={16} /> },
      ].map((command) => ({ ...command, label: command.id, action: () => completion.complete(`/${command.id} `) })),
    [completion.complete],
  );
 
  return (
    <Command.Root items={commands} completion={completion} className="relative">
      <Input
        ref={completion.fieldProps.ref}
        aria-label="Message"
        placeholder="Type / for commands"
        value={value}
        onChange={(event) => setValue(event.target.value)}
        onSelect={completion.fieldProps.onSelect}
        onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
          keyboardRef.current = event.key === 'ArrowDown' || event.key === 'ArrowUp';
          completion.fieldProps.onKeyDown(event);
        }}
      />
      {completion.open && <Suggestions keyboardRef={keyboardRef} />}
    </Command.Root>
  );
}