- 前端
- UI组件
- 设计系统
【免费下载链接】coss
coss.com/ui is the official design system of Cal.com
coss是 Cal.com 官方设计系统(位于本仓库apps/ui目录),其Command组件基于 Base UI 的Autocomplete与Dialog原语组合而成,用于实现命令面板(Command Palette)与键盘可导航的操作菜单。本文以 apps/ui/skills/coss/references/primitives/command.md 为骨架,结合 command.tsx 源码与p-command-1、p-command-2两个粒子示例,系统讲解 Command 组件的适用场景、安装方式、完整 API、最小可用模式、分组与快捷键最佳实践以及常见陷阱,读完即可在你的项目中落地一个生产级的命令面板。
何时该用、何时不该用 Command
Command 组件解决的是「快速发现并执行动作」这一交互问题。官方文档给出了清晰的判定标准:
适用场景(When to use)
- 命令面板(Command palette)与可键盘导航的操作菜单;
- 面向高级用户(power-user)的快捷动作发现,以及应用级快捷键工作流。
不适用场景(When NOT to use)
- 列表只是一组没有搜索的简单操作 → 使用 Menu;
- 用户需要从预定义列表中选择 → 使用 Select 或 Combobox;
- 流程是一个数据表单 → 使用 Form。
这条边界非常重要:Command 的定位是「搜索 + 动作执行」,而不是「选择数据」或「录入表单」。选错组件会让交互变得笨重。
安装与依赖
在 coss 的组件注册表中,command被声明为一个registry:ui类型条目(见 registry-ui.ts):
{ dependencies: ["@base-ui/react"], files: [{ path: "ui/command.tsx", type: "registry:ui" }], name: "command", registryDependencies: ["@coss/autocomplete"], type: "registry:ui", }因此可以通过 shadcn CLI 一键安装:
npx shadcn@latest add @coss/command注意:command的registryDependencies声明了@coss/autocomplete,也就是说安装 Command 时它的 Autocomplete 底层依赖会被一并带入。如果选择手动安装(Manual deps),核心第三方依赖是:
npm install @base-ui/react从源码看,command.tsx通过@base-ui/react/dialog的Dialog原语和@/registry/default/ui/autocomplete的Autocomplete*系列组件组合而成,没有直接依赖 cmdk 或其他命令面板库。
Canonical imports(标准导入)
组件拆分为 14 个可独立导入的 API,官方推荐的导入方式如下:
import { Command, CommandCollection, CommandDialog, CommandDialogPopup, CommandDialogTrigger, CommandEmpty, CommandFooter, CommandGroup, CommandGroupLabel, CommandInput, CommandItem, CommandList, CommandPanel, CommandSeparator, CommandShortcut, } from "@/components/ui/command" import { Button } from "@/components/ui/button"结合 command.tsx 源码,可以看清这些 API 的组成:
- 对话框相关:
CommandDialog(即 Base UIDialog.Root)、CommandDialogTrigger、CommandDialogPopup、CommandDialogPortal(即Dialog.Portal)、CommandDialogBackdrop、CommandDialogViewport、CommandCreateHandle(即Dialog.createHandle,用于命令式打开对话框); - 搜索与列表:
Command(包装Autocomplete)、CommandInput(包装AutocompleteInput)、CommandList、CommandEmpty、CommandPanel(结果面板容器); - 分组与条目:
CommandGroup、CommandGroupLabel、CommandCollection(渲染函数式集合)、CommandItem、CommandSeparator、CommandShortcut(快捷键kbd)、CommandFooter(底部操作提示栏)。
其中Command组件值得单独说明:它并不是一个独立的原语,而是对Autocomplete的预设封装:
export function Command({ autoHighlight = "always", keepHighlight = true, ...props }) { return <Autocomplete autoHighlight={autoHighlight} inline keepHighlight={keepHighlight} open {...props} />; }这意味着Command默认强制展开(open)、始终高亮首个条目(autoHighlight="always")、保持高亮(keepHighlight)并使用**内联(inline)**过滤模式——这正是命令面板「输入即过滤、方向键即选择」体验的底层保证。
最小可用模式(Minimal pattern)
官方文档提供了一个开箱即用的最小示例,完整复刻如下:
const items = [ { value: "linear", label: "Linear" }, { value: "figma", label: "Figma" }, { value: "slack", label: "Slack" }, ] <CommandDialog> <CommandDialogTrigger render={<Button variant="outline" />}> Open Command Palette </CommandDialogTrigger> <CommandDialogPopup> <Command items={items}> <CommandInput placeholder="Search..." /> <CommandEmpty>No results found.</CommandEmpty> <CommandList> {(item) => ( <CommandItem key={item.value} value={item.value}> {item.label} </CommandItem> )} </CommandList> </Command> </CommandDialogPopup> </CommandDialog>这段代码展示了几条关键用法:
CommandDialogTrigger的renderprop 可以让任意组件(这里是Button)成为触发器,这是 Base UI 的「render prop 组合」模式;CommandList接收一个渲染函数,函数的入参是items中的每一项;CommandItem的value用于过滤匹配,label用于展示;CommandEmpty在过滤结果为空时展示「No results found.」;- 组件树是「Trigger → Popup → Command(含 Input/Empty/List)」的三层结构。
从源码看,CommandInput还内置了autoFocus、size="lg"与startAddon={<SearchIcon />}(左侧搜索图标),这些是封装好的默认行为,无需额外配置。
分组命令面板:Group / Collection 模式
当命令数量较多时,应当使用分组。官方文档给出了带分组的标准写法:
<Command items={items}> <CommandInput placeholder="Type a command..." /> <CommandEmpty>No results found.</CommandEmpty> <CommandList> <CommandGroup> <CommandGroupLabel>Suggestions</CommandGroupLabel> <CommandCollection> {(item) => ( <CommandItem key={item.value} value={item.value}> {item.label} </CommandItem> )} </CommandCollection> </CommandGroup> </CommandList> </Command>分组模式的关键点是CommandGroup+CommandGroupLabel+CommandCollection三者配合:
CommandGroup声明一组条目的容器;CommandGroupLabel渲染组标题(如 "Suggestions"、"Commands");CommandCollection接收渲染函数,把组的items逐项渲染成CommandItem。
在p-command-1粒子(见 p-command-1.json)中可以看到完整的分组数据模型:Group接口包含value(组名)和items数组,groupedItems将 "Suggestions" 与 "Commands" 两组组合后直接传入Command items={groupedItems};外层CommandList的渲染函数收到(group, index),每组渲染一个CommandGroup,组间用CommandSeparator分隔。这展示了「外层按组渲染、内层按条目渲染」的完整模式。
对话框模式:受控 open 与快捷键唤起
命令面板最常见的形态是放在对话框弹层中。官方文档明确指出:
Use
CommandDialog+CommandDialogTrigger+CommandDialogPopupto wrapCommandin a dialog overlay. Use controlledopen/onOpenChangestate for keyboard-shortcut activation.
即:用CommandDialog三件套包裹Command,并通过受控的open/onOpenChange实现快捷键唤起。p-command-1给出了完整参考实现:
const [open, setOpen] = useState(false); useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "j" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((open) => !open); } }; document.addEventListener("keydown", down); return () => document.removeEventListener("keydown", down); }, []); <CommandDialog onOpenChange={setOpen} open={open}> <CommandDialogTrigger render={<Button variant="outline" />}> Open Command Palette <KbdGroup><Kbd>⌘</Kbd><Kbd>J</Kbd></KbdGroup> </CommandDialogTrigger> ... </CommandDialog>关键细节:
- 在
useEffect中监听全局keydown,捕获⌘/Ctrl + J,preventDefault后切换open状态;CommandDialog的onOpenChange同步状态,实现 Esc 关闭时状态一致; CommandDialogTrigger内部用KbdGroup/Kbd渲染快捷键提示(⌘J);- 点击条目时通过
onClick回调setOpen(false)关闭面板(p-command-1中的handleItemClick); - 弹出层的布局由
CommandDialogBackdrop(bg-black/32 backdrop-blur-sm遮罩)、CommandDialogViewport(全屏固定定位的居中容器)、CommandDialogPopup(max-w-xl圆角卡片)三级组成,视觉与动效由源码中的 Tailwind 类与data-starting-style/data-ending-style数据属性驱动。
进阶:portalProps 与 Portal 转发
官方文档在「Patterns from coss particles」一节特别提到了Portal forwarding:
optional
portalPropsonCommandDialogPopup→ Base UIDialog.Portal(keepMounted,container, …)
结合 command.tsx 的源码实现,CommandDialogPopup的签名是:
export function CommandDialogPopup({ className, children, portalProps, ...props }: CommandDialogPrimitive.Popup.Props & { portalProps?: CommandDialogPrimitive.Portal.Props; })它内部把portalProps展开(spread)到CommandDialogPortal(即 Base UIDialog.Portal)上,同时渲染CommandDialogBackdrop与CommandDialogViewport。完整说明见 portal-props.md,核心用途包括:
keepMounted:让弹层内容在关闭后仍挂载在 DOM 中(对保持输入状态、动画退场有用);container:把弹层内容渲染到指定 DOM 节点(处理层叠上下文、微前端、Shadow DOM 场景);- 以及其他该组件
Portal.Props接受的属性。
同时要注意,portalProps只影响portal 节点;若要调整定位(placement),应使用side、align、sideOffset等 positioner 属性。
进阶:命令式 Dialog 句柄(CommandCreateHandle)
command.tsx还导出了CommandCreateHandle(即 Base UIDialog.createHandle),用于命令式地控制对话框。p-command-2粒子(AI 助手命令面板,见 p-command-2.json)展示了它的用法:
export const commandHandle: ReturnType<typeof CommandCreateHandle> = CommandCreateHandle(); ... <CommandDialog handle={commandHandle} onOpenChange={handleOpenChange} open={open}>CommandCreateHandle()返回一个可重用的句柄对象,传入CommandDialog的handleprop 后,即可在组件外部以命令式方式打开/关闭对话框(该粒子中还结合AbortController处理了 AI 请求的取消与卸载清理)。
进阶:自定义过滤与 AI 搜索(filter prop)
p-command-2展示了超越基本搜索的高级用法:它通过useAutocompleteFilter构造大小写不敏感(sensitivity: "base")的匹配器,然后向Command传入自定义filter:
const { contains } = useAutocompleteFilter({ sensitivity: "base" }); const filterItem = useCallback((itemValue, query) => { const item = itemValue as Item; return ( contains(item.label, query) || contains(item.value, query) || item.keywords?.some((keyword) => contains(keyword, query)) ); }, [contains]); <Command filter={filterItem} items={commandGroups} key={commandResetKeyRef.current}>要点:
- 匹配逻辑可以扩展到
keywords字段(如 "proj" 匹配 "Projects"),实现别名/模糊搜索; - 该粒子还在输入框旁提供了 "Ask AI"(Tab / Enter)入口:无结果时按 Enter 进入 AI 问答模式,展示
Skeleton加载态、mock 回答(dangerouslySetInnerHTML渲染 markdown 转换结果)与参考链接(Button render={<Link/>}); key={commandResetKeyRef.current}用于在状态切换时强制重建Command,重置高亮与过滤状态;Esc 在 AI 模式下被stopPropagation拦截为「返回搜索」而非「关闭面板」。
这一示例说明Command的filterprop 是可插拔的,完全可以把本地过滤替换为远程搜索或 AI 检索逻辑。
用 CommandFooter 呈现键盘操作提示
p-command-1与p-command-2都用CommandFooter渲染底部操作提示条:↑/↓ 导航、↵ 打开、Esc 关闭,通过KbdGroup/Kbd组件组合展示。源码中CommandFooter是一个带顶部边框的 flex 容器(flex items-center justify-between gap-2 rounded-b-... border-t px-5 py-3 text-muted-foreground text-xs),而CommandShortcut则渲染为右对齐的kbd元素(ms-auto ... tracking-widest),用于在条目行尾展示快捷键(如 ⌘L、⌘⇧C)。这是让命令面板对新手也保持「可发现、可学习」的关键 UX 细节。
常见陷阱(Common pitfalls)
官方文档总结了三条高频问题,值得在实现时逐条对照:
- 命令列表缺少清晰分组与动作标签:没有分组的扁平长列表会让用户难以扫读;应使用
CommandGroup+CommandGroupLabel,并为每个动作提供明确的label(与value分开),必要时补充shortcut。 - 把关键破坏性操作直接绑定到命令,缺少确认路径:删除、清空等破坏性动作不应在面板内一键触发,应弹出确认流程(如 AlertDialog)再执行。
- 缺少方向键 / 选择 / Esc 的键盘可访问性检查:命令面板的可用性几乎全部依赖键盘。需要验证
↑/↓导航、Enter选择、Esc关闭/返回,以及autoFocus是否落位到输入框;p-command-2中对 Esc 的自定义拦截(AI 模式返回搜索)就是一个需要刻意处理的边界。
更多参考与粒子示例
- 核心粒子:
p-command-1(p-command-1.json)— 对话框 + 分组动作命令面板;p-command-2(p-command-2.json)— 集成 AI 助手的进阶命令面板。 - 相关引用:
p-autocomplete-1、p-select-1、p-input-group-1分别对应搜索/选择/输入分组的邻近参考模式,安装命令同样是npx shadcn@latest add @coss/xxx并按需选用。 - 源码入口:command.tsx(组件实现)、autocomplete.tsx(Command 的底层原语)、registry-ui.ts(注册表声明)、portal-props.md(portal 转发机制说明)。
综上,coss 的Command组件把 Base UI 的 Dialog 与 Autocomplete 能力收敛为一份开箱即用的命令面板 API,从最小示例到分组、快捷键、AI 搜索、portal 定制均有官方粒子与源码可循。照着本文的模式,你可以在几分钟内构建出具备完整键盘可访问性、分组导航与快捷键唤起的专业命令面板。
- 前端
- UI组件
- 设计系统
【免费下载链接】coss
coss.com/ui is the official design system of Cal.com
相关推荐
gpui-kit Command 命令面板完全指南:构建分组过滤、Action 快捷键提示与键盘导航的 ⌘K 风格命令列表
gpui kit Command 命令面板完全指南:构建分组过滤、Action 快捷键提示与键盘导航的 ⌘K 风格命令列表 命令面板(Command Palet
桌面应用UI组件前端coss Menu 组件实战指南:用 Base UI 构建可访问的 React 下拉菜单
coss Menu 组件实战指南:用 Base UI 构建可访问的 React 下拉菜单 在 kaneo 项目中, Menu 是 coss UI 组件库 htt
企业应用后端前端gpui-kit Command 组件实战指南:构建可搜索、可虚拟化的 ⌘K 命令面板
gpui kit Command 组件实战指南:构建可搜索、可虚拟化的 ⌘K 命令面板 本指南以 gpui kit 组件库的 Command 命令面板为核心,系
桌面应用UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考