CommandPalette 命令面板

以快捷键唤起的搜索面板,输入后从命令与页面中选择。

'use client'

import { useState } from 'react'
import { Bookmark, Home, Library, LogOut, Moon, PenLine, Search, Settings } from 'lucide-react'
import { Button, CommandPalette, Kbd, Stack, Text, type CommandItems } from '@hina-ui/react'

const items: CommandItems = [
  {
    label: '页面',
    items: [
      { id: 'home', label: '首页', icon: Home, keywords: ['home'] },
      { id: 'library', label: '书架', icon: Library, keywords: ['library'] },
      { id: 'bookmarks', label: '收藏', icon: Bookmark, keywords: ['bookmark'] },
      { id: 'settings', label: '设置', icon: Settings, keywords: ['settings'], kbd: ['⌘', ','] },
    ],
  },
  {
    label: '操作',
    items: [
      { id: 'review', label: '新建书评', icon: PenLine, description: '记录一本刚读完的书' },
      { id: 'theme', label: '切换主题', icon: Moon, kbd: ['⌘', 'D'] },
      { id: 'logout', label: '退出登录', icon: LogOut },
    ],
  },
]

export default function Demo() {
  const [picked, setPicked] = useState('')

  return (
    <Stack gap="md" align="start">
      <CommandPalette items={items} hotkey="mod+j" onSelect={item => setPicked(item.label)}>
        <Button variant="outline" tone="neutral" icon={<Search />}>
          搜索
          <Kbd>⌘J</Kbd>
        </Button>
      </CommandPalette>
      {picked && (
        <Text size="sm" tone="muted">
          已选择:{picked}
        </Text>
      )}
    </Stack>
  )
}
tsx

用法

import { CommandPalette } from '@hina-ui/react'
ts

items 是条目列表。每个条目至少有 id 与 label,可以带 description、keywords、icon、kbd 与 onSelect;带有 label 与 items 的对象是一个分组。children 是触发器。选中条目时先调用该条目的 onSelect,再调用组件的 onSelect,然后关闭面板。

'use client'

import { useState } from 'react'
import { Button, CommandPalette, Stack, Text, type CommandItems } from '@hina-ui/react'

const items: CommandItems = [
  { id: 'home', label: '首页' },
  { id: 'library', label: '书架' },
  { id: 'bookmarks', label: '收藏' },
  { id: 'settings', label: '设置' },
]

export default function Demo() {
  const [picked, setPicked] = useState('')

  return (
    <Stack gap="md" align="start">
      <CommandPalette items={items} onSelect={item => setPicked(item.label)}>
        <Button variant="outline" tone="neutral">
          打开面板
        </Button>
      </CommandPalette>
      {picked && (
        <Text size="sm" tone="muted">
          已选择:{picked}
        </Text>
      )}
    </Stack>
  )
}
tsx

示例

分组与说明

分组各有标题,条目的 description 显示在标签下方。没有查询时分组按给定顺序排列,有查询时含最佳匹配的分组靠前,没有匹配条目的分组不显示。

import { Button, CommandPalette, type CommandItems } from '@hina-ui/react'

const items: CommandItems = [
  {
    label: '最近阅读',
    items: [
      { id: 'book-1', label: '星之继承者', description: '詹姆斯·P·霍根' },
      { id: 'book-2', label: '海伯利安', description: '丹·西蒙斯' },
    ],
  },
  {
    label: '书单',
    items: [
      { id: 'list-1', label: '今年想读', description: '12 本' },
      { id: 'list-2', label: '硬科幻入门', description: '8 本' },
    ],
  },
]

export default function Demo() {
  return (
    <CommandPalette items={items} placeholder="搜索书与书单">
      <Button variant="outline" tone="neutral">
        搜索
      </Button>
    </CommandPalette>
  )
}
tsx

图标与按键提示

icon 显示在标签前,kbd 是显示在行末的按键提示,只用于提示,面板不会替你绑定这些按键。

'use client'

import { Copy, Moon, PenLine, Trash2 } from 'lucide-react'
import { Button, CommandPalette, type CommandItems } from '@hina-ui/react'

const items: CommandItems = [
  { id: 'review', label: '新建书评', icon: PenLine, kbd: ['⌘', 'N'] },
  { id: 'copy', label: '复制链接', icon: Copy, kbd: ['⌘', 'C'] },
  { id: 'theme', label: '切换主题', icon: Moon, kbd: ['⌘', 'D'] },
  { id: 'delete', label: '删除书评', icon: Trash2, disabled: true },
]

export default function Demo() {
  return (
    <CommandPalette items={items}>
      <Button variant="outline" tone="neutral">
        操作
      </Button>
    </CommandPalette>
  )
}
tsx

全局快捷键

hotkey 接受 mod+k 这样的组合,mod 对应 Mac 的 ⌘ 与其他平台的 Ctrl,还可以加上 shift 与 alt。面板打开时再按一次会关闭。

也可以按⌘⇧P打开

import { Button, CommandPalette, Inline, Kbd, Text, type CommandItems } from '@hina-ui/react'

const items: CommandItems = [
  { id: 'home', label: '首页' },
  { id: 'library', label: '书架' },
  { id: 'settings', label: '设置' },
]

export default function Demo() {
  return (
    <Inline gap="md" align="center">
      <CommandPalette items={items} hotkey="mod+shift+p">
        <Button variant="outline" tone="neutral">
          搜索
        </Button>
      </CommandPalette>
      <Text size="sm" tone="muted">
        也可以按
        <Kbd>⌘</Kbd>
        <Kbd>⇧</Kbd>
        <Kbd>P</Kbd>
        打开
      </Text>
    </Inline>
  )
}
tsx

受控

open 可受控。省略 children 时不渲染触发器,面板只能从外部打开。

已关闭

'use client'

import { useState } from 'react'
import { Button, CommandPalette, Inline, Text, type CommandItems } from '@hina-ui/react'

const items: CommandItems = [
  { id: 'home', label: '首页' },
  { id: 'library', label: '书架' },
  { id: 'settings', label: '设置' },
]

export default function Demo() {
  const [open, setOpen] = useState(false)

  return (
    <Inline gap="md" align="center">
      <Button variant="outline" tone="neutral" onClick={() => setOpen(true)}>
        从外部打开
      </Button>
      <Text size="sm" tone="muted">
        {open ? '已打开' : '已关闭'}
      </Text>
      <CommandPalette open={open} onOpenChange={setOpen} items={items} />
    </Inline>
  )
}
tsx

自定义过滤

search 可受控。设置 ignoreFilter 后面板不再自行过滤,条目列表完全由调用方决定,例如向服务端搜索。

'use client'

import { useState } from 'react'
import { Button, CommandPalette, type CommandItems } from '@hina-ui/react'

const books = [
  { id: 'b1', label: '星之继承者', tags: ['科幻', '硬科幻'] },
  { id: 'b2', label: '海伯利安', tags: ['科幻', '太空歌剧'] },
  { id: 'b3', label: '基地', tags: ['科幻', '经典'] },
  { id: 'b4', label: '三体', tags: ['科幻', '中文'] },
]

export default function Demo() {
  const [search, setSearch] = useState('')
  const query = search.trim()
  const items: CommandItems = books
    .filter(book => !query || book.tags.some(tag => tag.includes(query)))
    .map(book => ({ id: book.id, label: book.label, description: book.tags.join('、') }))

  return (
    <CommandPalette
      search={search}
      onSearchChange={setSearch}
      items={items}
      ignoreFilter
      placeholder="按标签搜索"
    >
      <Button variant="outline" tone="neutral">
        按标签搜索
      </Button>
    </CommandPalette>
  )
}
tsx

内联

设置 inline 后面板不再包进浮层,直接渲染在文档流里,适合嵌在页面中而不是由快捷键唤起。内联形态不注册全局快捷键,选中条目之后面板停在原地。

页面
首页
书架
收藏
设置⌘,
操作
新建书评记录一本刚读完的书
切换主题⌘D
'use client'

import { Bookmark, Home, Library, Moon, PenLine, Settings } from 'lucide-react'
import { CommandPalette, type CommandItems } from '@hina-ui/react'

const items: CommandItems = [
  {
    label: '页面',
    items: [
      { id: 'home', label: '首页', icon: Home },
      { id: 'library', label: '书架', icon: Library },
      { id: 'bookmarks', label: '收藏', icon: Bookmark },
      { id: 'settings', label: '设置', icon: Settings, kbd: ['⌘', ','] },
    ],
  },
  {
    label: '操作',
    items: [
      { id: 'review', label: '新建书评', icon: PenLine, description: '记录一本刚读完的书' },
      { id: 'theme', label: '切换主题', icon: Moon, kbd: ['⌘', 'D'] },
    ],
  },
]

export default function Demo() {
  return <CommandPalette inline items={items} className="max-w-sm" />
}
tsx

虚拟滚动

virtualize 按需渲染可见范围附近的条目,与 VirtualList 共用测量与滚动底层。默认关闭;可传 { estimateSize, overscan } 调整预估行高和两侧预渲染数量,行高会按实际内容测量。键盘导航覆盖完整数据,禁用项会跳过。 搜索仍处理完整数据。 命令离开渲染范围后会卸载,持久状态应按命令 id 保存在外部。

已选: —

'use client'

import { useState } from 'react'
import { CommandPalette, Stack, Text } from '@hina-ui/react'

const items = Array.from({ length: 10000 }, (_, index) => ({
  id: String(index),
  label: `条目 ${String(index + 1).padStart(5, '0')}`,
  keywords: [`id-${index}`],
}))

export default function Demo() {
  const [selected, setSelected] = useState('—')

  return (
    <Stack gap="sm" className="w-96 max-w-full">
      <CommandPalette
        items={items}
        virtualize={{ estimateSize: 36, overscan: 6 }}
        inline
        aria-label="一万项"
        onSelect={item => setSelected(item.label)}
      />
      <Text size="sm" tone="muted">
        已选: {selected}
      </Text>
    </Stack>
  )
}
tsx

行为

  • 打开后焦点落在输入框,首个条目自动高亮;输入时列表即时过滤,标签中匹配的片段以强调色标出。
  • 匹配按标签全等、标签开头、标签包含、关键词、说明的顺序排列;有查询时,含最佳匹配的分组排在前面。超出可见高度的条目在列表内滚动。
  • 方向键移动高亮,回车选中高亮项,鼠标悬停也会移动高亮。
  • 按 Esc、点击遮罩或选中条目都会关闭面板,关闭时清空搜索内容。
  • 面板打开期间页面停止滚动,焦点限制在面板内,关闭后回到触发器。
  • 宽屏上面板停靠在视口上部,窄屏上贴顶并占满宽度。

无障碍

  • 面板是对话框,无障碍名取 label,默认为界面语言中的“命令面板”。
  • 输入框通过 aria-activedescendant 指向当前高亮的条目;列表使用 listbox 与 option 角色,分组带有各自的名称。
  • 图标对辅助技术隐藏,按键提示以 kbd 元素呈现。

API

Props

属性
类型
默认值
说明
items
CommandItems
—
必填。条目与分组
virtualize
VirtualizeOptions
false
虚拟滚动;预估行高按内容,overscan 6
placeholder
string
取自界面语言
输入框的占位文字
label
string
取自界面语言
面板的无障碍名
hotkey
string
—
全局快捷键,例如 mod+k
ignoreFilter
boolean
false
不自行过滤,条目列表由调用方决定
inline
boolean
false
渲染为内联面板,不使用浮层
className
string
—
追加至面板的类名

受控状态

名称
类型
说明
open
boolean
面板是否打开
search
string
输入框中的搜索词

回调

回调
参数
说明
onSelect
(item: CommandItem)
选中条目时触发

内容属性

属性
说明
children
触发器

类型

字段
类型
说明
id
string
必填。条目的唯一标识
label
string
必填。标签
description
string
标签下方的说明,也参与匹配
keywords
string[]
参与匹配但不显示的关键词
icon
Component
标签前的图标
kbd
string[]
行末的按键提示
disabled
boolean
不可选中
onSelect
() => void
选中时调用

分组是 { label: string; items: CommandItem[] },CommandItems 是条目与分组的数组。

type VirtualizeOptions = boolean | { estimateSize?: number; overscan?: number }
ts