CommandPalette 命令面板
@lark-ui/core
import { useState } from "react";
import { CommandPalette, Button } from "@lark-ui/core";

export function CommandPaletteBasicDemo() {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");
  const [selectedItem, setSelectedItem] = useState<string | null>(null);

  const handleSelect = (item: CommandItem) => {
    setSelectedItem(item.title);
    setOpen(false);
    setSearch("");
  };

  // Filter groups based on search
  const filteredGroups = filterGroupsWithItems(sampleGroups, search);

  return (
    <div className="flex flex-col items-start gap-4">
      <Button onClick={() => setOpen(true)}>打开命令面板</Button>
      {selectedItem && (
        <p className="text-sm text-lark-subtle">
          最近选择: <span className="text-lark-default">{selectedItem}</span>
        </p>
      )}

      <CommandPalette.Root
        open={open}
        onOpenChange={setOpen}
        items={filteredGroups}
        value={search}
        onValueChange={setSearch}
        itemToStringValue={(group) => group.label}
        onSelect={(item, { newTab }) => {
          console.log("Selected:", item.title, newTab ? "(new tab)" : "");
          handleSelect(item);
        }}
        getSelectableItems={getSelectableItems}
      >
        <CommandPalette.Input placeholder="输入命令或搜索内容…" />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(group: CommandGroup) => (
              <CommandPalette.Group key={group.id} items={group.items}>
                <CommandPalette.GroupLabel>
                  {group.label}
                </CommandPalette.GroupLabel>
                <CommandPalette.Items>
                  {(item: CommandItem) => (
                    <CommandPalette.Item
                      key={item.id}
                      value={item}
                      onClick={() => handleSelect(item)}
                    >
                      <span className="flex items-center gap-3">
                        {item.icon && (
                          <span className="text-lark-subtle">{item.icon}</span>
                        )}
                        <span>{item.title}</span>
                      </span>
                    </CommandPalette.Item>
                  )}
                </CommandPalette.Items>
              </CommandPalette.Group>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>未找到命令</CommandPalette.Empty>
        </CommandPalette.List>
        <CommandPalette.Footer>
          <span className="flex items-center gap-2">
            <kbd className="rounded border border-lark-hairline bg-lark-base px-1.5 py-0.5 text-[10px]">
              ↑↓
            </kbd>
            <span>导航</span>
          </span>
          <span className="flex items-center gap-2">
            <kbd className="rounded border border-lark-hairline bg-lark-base px-1.5 py-0.5 text-[10px]">
              ↵
            </kbd>
            <span>选择</span>
          </span>
        </CommandPalette.Footer>
      </CommandPalette.Root>
    </div>
  );
}

安装

统一导出

import { CommandPalette } from "@lark-ui/core";

按需导出

import { CommandPalette } from "@lark-ui/core/components/command-palette";

用法

CommandPalette 是基于 Base UI 的 Autocomplete 原语构建的复合组件。它为命令面板界面提供无障碍的键盘导航和可自定义的样式。

import { useState } from "react";
import { CommandPalette } from "@lark-ui/core";

interface Item {
  id: string;
  title: string;
}

const items: Item[] = [
  { id: "1", title: "Create Project" },
  { id: "2", title: "Open Settings" },
];

export default function Example() {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");

  return (
    <>
      <button onClick={() => setOpen(true)}>Open</button>
      <CommandPalette.Root
        open={open}
        onOpenChange={setOpen}
        items={items}
        value={search}
        onValueChange={setSearch}
        itemToStringValue={(item) => item.title}
        getSelectableItems={(items) => items}
      >
        <CommandPalette.Input placeholder="Search..." />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(item) => (
              <CommandPalette.Item
                key={item.id}
                value={item}
                onClick={() => setOpen(false)}
              >
                {item.title}
              </CommandPalette.Item>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>No results</CommandPalette.Empty>
        </CommandPalette.List>
      </CommandPalette.Root>
    </>
  );
}

键盘导航

内置键盘导航,无需额外配置:

↑+↓在选项之间移动高亮
Enter选中当前高亮的选项
⌘/Ctrl+Enter以 newTab: true 方式选中
Escape关闭弹层

示例

带分组的选项

用标签把相关命令归为一组。

import { useState } from "react";
import { CommandPalette, Button } from "@lark-ui/core";

export function CommandPaletteBasicDemo() {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");
  const [selectedItem, setSelectedItem] = useState<string | null>(null);

  const handleSelect = (item: CommandItem) => {
    setSelectedItem(item.title);
    setOpen(false);
    setSearch("");
  };

  // Filter groups based on search
  const filteredGroups = filterGroupsWithItems(sampleGroups, search);

  return (
    <div className="flex flex-col items-start gap-4">
      <Button onClick={() => setOpen(true)}>打开命令面板</Button>
      {selectedItem && (
        <p className="text-sm text-lark-subtle">
          最近选择: <span className="text-lark-default">{selectedItem}</span>
        </p>
      )}

      <CommandPalette.Root
        open={open}
        onOpenChange={setOpen}
        items={filteredGroups}
        value={search}
        onValueChange={setSearch}
        itemToStringValue={(group) => group.label}
        onSelect={(item, { newTab }) => {
          console.log("Selected:", item.title, newTab ? "(new tab)" : "");
          handleSelect(item);
        }}
        getSelectableItems={getSelectableItems}
      >
        <CommandPalette.Input placeholder="输入命令或搜索内容…" />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(group: CommandGroup) => (
              <CommandPalette.Group key={group.id} items={group.items}>
                <CommandPalette.GroupLabel>
                  {group.label}
                </CommandPalette.GroupLabel>
                <CommandPalette.Items>
                  {(item: CommandItem) => (
                    <CommandPalette.Item
                      key={item.id}
                      value={item}
                      onClick={() => handleSelect(item)}
                    >
                      <span className="flex items-center gap-3">
                        {item.icon && (
                          <span className="text-lark-subtle">{item.icon}</span>
                        )}
                        <span>{item.title}</span>
                      </span>
                    </CommandPalette.Item>
                  )}
                </CommandPalette.Items>
              </CommandPalette.Group>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>未找到命令</CommandPalette.Empty>
        </CommandPalette.List>
        <CommandPalette.Footer>
          <span className="flex items-center gap-2">
            <kbd className="rounded border border-lark-hairline bg-lark-base px-1.5 py-0.5 text-[10px]">
              ↑↓
            </kbd>
            <span>导航</span>
          </span>
          <span className="flex items-center gap-2">
            <kbd className="rounded border border-lark-hairline bg-lark-base px-1.5 py-0.5 text-[10px]">
              ↵
            </kbd>
            <span>选择</span>
          </span>
        </CommandPalette.Footer>
      </CommandPalette.Root>
    </div>
  );
}

简单扁平列表

对于较简单的场景,可直接使用不分组的扁平选项数组。

import { useState } from "react";
import { CommandPalette, Button } from "@lark-ui/core";

export function CommandPaletteSimpleDemo() {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");

  return (
    <div>
      <Button onClick={() => setOpen(true)}>打开简易面板</Button>

      <CommandPalette.Root
        open={open}
        onOpenChange={setOpen}
        items={simpleItems}
        value={search}
        onValueChange={setSearch}
        itemToStringValue={(item) => item.title}
        onSelect={(item) => {
          console.log("Selected:", item.title);
          setOpen(false);
        }}
        getSelectableItems={(items) => items}
      >
        <CommandPalette.Input placeholder="搜索操作…" />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(item: SimpleItem) => (
              <CommandPalette.Item
                key={item.id}
                value={item}
                onClick={() => {
                  console.log("Clicked:", item.title);
                  setOpen(false);
                }}
              >
                {item.title}
              </CommandPalette.Item>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>未找到操作</CommandPalette.Empty>
        </CommandPalette.List>
      </CommandPalette.Root>
    </div>
  );
}

加载状态

获取结果期间显示加载动画。

import { useState } from "react";
import { CommandPalette, Button } from "@lark-ui/core";

// With loading state
export function CommandPaletteLoadingDemo() {
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [search, setSearch] = useState("");

  const handleOpen = () => {
    setOpen(true);
    setLoading(true);
    // Simulate loading
    setTimeout(() => setLoading(false), 1500);
  };

  // Filter groups based on search
  const filteredGroups = filterGroupsWithItems(sampleGroups, search);

  return (
    <div>
      <Button onClick={handleOpen}>打开(带加载状态)</Button>

      <CommandPalette.Root
        open={open}
        onOpenChange={setOpen}
        items={loading ? [] : filteredGroups}
        value={search}
        onValueChange={setSearch}
        itemToStringValue={(group) => group.label}
        getSelectableItems={getSelectableItems}
      >
        <CommandPalette.Input placeholder="搜索…" />
        <CommandPalette.List>
          {loading ? (
            <CommandPalette.Loading />
          ) : (
            <>
              <CommandPalette.Results>
                {(group: CommandGroup) => (
                  <CommandPalette.Group key={group.id} items={group.items}>
                    <CommandPalette.GroupLabel>
                      {group.label}
                    </CommandPalette.GroupLabel>
                    <CommandPalette.Items>
                      {(item: CommandItem) => (
                        <CommandPalette.Item
                          key={item.id}
                          value={item}
                          onClick={() => setOpen(false)}
                        >
                          <span className="flex items-center gap-3">
                            {item.icon && (
                              <span className="text-lark-subtle">
                                {item.icon}
                              </span>
                            )}
                            <span>{item.title}</span>
                          </span>
                        </CommandPalette.Item>
                      )}
                    </CommandPalette.Items>
                  </CommandPalette.Group>
                )}
              </CommandPalette.Results>
              <CommandPalette.Empty>未找到结果</CommandPalette.Empty>
            </>
          )}
        </CommandPalette.List>
      </CommandPalette.Root>
    </div>
  );
}

禁用浏览器自动填充

传入 autoComplete、autoCorrect、spellCheck 以及 data-* 等标准 HTML 输入属性,可抑制浏览器和密码管理器的自动填充浮层。

import { useState } from "react";
import { CommandPalette, Button } from "@lark-ui/core";

/** 演示在命令面板(CommandPalette)输入框上关闭浏览器自动补全和拼写检查。 */
export function CommandPaletteNoAutocompleteDemo() {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");

  const filteredGroups = filterGroupsWithItems(sampleGroups, search);

  return (
    <div className="flex flex-col items-start gap-4">
      <Button onClick={() => setOpen(true)}>打开面板(禁用自动补全)</Button>

      <CommandPalette.Root
        open={open}
        onOpenChange={setOpen}
        items={filteredGroups}
        value={search}
        onValueChange={setSearch}
        itemToStringValue={(group) => group.label}
        onSelect={(item) => {
          console.log("Selected:", item.title);
          setOpen(false);
          setSearch("");
        }}
        getSelectableItems={getSelectableItems}
      >
        <CommandPalette.Input
          placeholder="搜索命令…"
          autoComplete="off"
          autoCorrect="off"
          autoCapitalize="none"
          spellCheck={false}
          data-1p-ignore="true"
          data-lpignore="true"
        />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(group: CommandGroup) => (
              <CommandPalette.Group key={group.id} items={group.items}>
                <CommandPalette.GroupLabel>
                  {group.label}
                </CommandPalette.GroupLabel>
                <CommandPalette.Items>
                  {(item: CommandItem) => (
                    <CommandPalette.Item
                      key={item.id}
                      value={item}
                      onClick={() => {
                        setOpen(false);
                        setSearch("");
                      }}
                    >
                      <span className="flex items-center gap-3">
                        {item.icon && (
                          <span className="text-lark-subtle">{item.icon}</span>
                        )}
                        <span>{item.title}</span>
                      </span>
                    </CommandPalette.Item>
                  )}
                </CommandPalette.Items>
              </CommandPalette.Group>
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>未找到命令</CommandPalette.Empty>
        </CommandPalette.List>
      </CommandPalette.Root>
    </div>
  );
}

带面包屑的 ResultItem

使用 ResultItem 展示包含面包屑、图标以及可选文本高亮的富内容选项。

import { useState } from "react";
import { CommandPalette, Button } from "@lark-ui/core";

export function CommandPaletteResultItemDemo() {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");

  return (
    <div>
      <Button onClick={() => setOpen(true)}>打开(带 ResultItem)</Button>

      <CommandPalette.Root
        open={open}
        onOpenChange={setOpen}
        items={searchResults}
        value={search}
        onValueChange={setSearch}
        itemToStringValue={(item) => item.title}
        getSelectableItems={(items) => items}
      >
        <CommandPalette.Input placeholder="搜索文档…" />
        <CommandPalette.List>
          <CommandPalette.Results>
            {(item: SearchResult) => (
              <CommandPalette.ResultItem
                key={item.id}
                value={item}
                title={item.title}
                breadcrumbs={item.breadcrumbs}
                icon={item.icon}
                onClick={() => {
                  console.log("Navigate to:", item.title);
                  setOpen(false);
                }}
              />
            )}
          </CommandPalette.Results>
          <CommandPalette.Empty>未找到页面</CommandPalette.Empty>
        </CommandPalette.List>
        <CommandPalette.Footer>
          <span className="flex items-center gap-2">
            <kbd className="rounded border border-lark-hairline bg-lark-base px-1.5 py-0.5 text-[10px]">
              ↑↓
            </kbd>
            <span>导航</span>
          </span>
          <span className="flex items-center gap-2">
            <kbd className="rounded border border-lark-hairline bg-lark-base px-1.5 py-0.5 text-[10px]">
              ⌘↵
            </kbd>
            <span>在新标签页打开</span>
          </span>
        </CommandPalette.Footer>
      </CommandPalette.Root>
    </div>
  );
}

组件构成

CommandPalette.Root

组合 Dialog 与 Panel 的主容器。管理打开状态和自动补全(Autocomplete)功能。

CommandPalette.Dialog

模态弹层容器。与 Panel 搭配使用可切换内容(例如逐级下钻导航)。

CommandPalette.Panel

不含弹层的自动补全面板。放在 Dialog 内使用,可切换内容而无需重新挂载。

CommandPalette.Input

带自动聚焦和键盘处理的搜索输入框。

CommandPalette.List

结果的可滚动容器。

CommandPalette.Results

用于遍历选项/分组的 render 属性迭代器。

CommandPalette.Group

按类别分组的容器。

CommandPalette.GroupLabel

分组内的区块标题文字。

CommandPalette.Items

用于遍历分组内选项的 render 属性迭代器。

CommandPalette.Item

基础可选项。

CommandPalette.ResultItem

包含面包屑、图标和文本高亮的富内容选项。

CommandPalette.HighlightedText

根据匹配位置渲染带高亮片段的文本。

CommandPalette.Empty

没有匹配结果时的空状态。

CommandPalette.Loading

加载动画状态。

CommandPalette.Footer

用于放置快捷键提示或其他内容的底部区域。

API 参考

CommandPalette.Root 属性

interface CommandPaletteRootProps<TGroup, TItem> {
  // Dialog state
  open: boolean;
  onOpenChange: (open: boolean) => void;
  onBackdropClick?: (e: React.MouseEvent) => void;

  // Autocomplete
  items: TGroup[];
  value?: string;
  onValueChange?: (value: string) => void;
  itemToStringValue?: (item: TGroup) => string;
  filter?: (item: TGroup, query: string) => boolean;
  onItemHighlighted?: (item: TGroup | undefined, details: {...}) => void;

  // Selection
  onSelect?: (item: TItem, options: { newTab: boolean }) => void;
  getSelectableItems?: (items: TGroup[]) => TItem[];

  children: React.ReactNode;
}

CommandPalette.ResultItem 属性

interface CommandPaletteResultItemProps<T> {
  value: T;
  title: string;
  breadcrumbs?: string[];
  titleHighlights?: [number, number][];
  breadcrumbHighlights?: [number, number][][];
  description?: string;
  icon?: React.ReactNode;
  onClick: (e: React.MouseEvent) => void;
  showArrow?: boolean; // default: true
  external?: boolean; // shows external link icon
  nonInteractive?: boolean;
}