Select 选择器

从列表中选择一项。

'use client'

import { useState } from 'react'
import { Select, type SelectValue } from '@hina-ui/react'

const types = [
  { value: 'gal', label: 'Galgame' },
  { value: 'ln', label: '轻小说' },
  { value: 'manga', label: '漫画' },
  { value: 'anime', label: '动画' },
]

export default function Demo() {
  const [type, setType] = useState<SelectValue>('gal')

  return (
    <Select
      value={type}
      onValueChange={setType}
      options={types}
      aria-label="作品类型"
      className="w-64"
    />
  )
}
tsx

用法

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

下拉选择框由触发器与浮层列表组成。options 提供选项,value / onValueChange 绑定选中的值。每个选项是 { value, label },可以附带 description 与 disabled;带 options 且不带 value 的项是分组。触发器与输入框共用同一副输入面。

当前值:无

'use client'

import { useState } from 'react'
import { Select, Stack, Text, type SelectValue } from '@hina-ui/react'

const statuses = [
  { value: 'wish', label: '想看' },
  { value: 'doing', label: '在看' },
  { value: 'done', label: '看过' },
  { value: 'dropped', label: '抛弃' },
]

export default function Demo() {
  const [status, setStatus] = useState<SelectValue>(null)

  return (
    <Stack className="w-56">
      <Select
        value={status}
        onValueChange={setStatus}
        options={statuses}
        placeholder="选择收藏状态"
        aria-label="收藏状态"
      />
      <Text tone="muted">当前值:{status ?? '无'}</Text>
    </Stack>
  )
}
tsx

示例

清除

clearable 在有值且未禁用时显示清除按钮。点击后以 null 调用 onValueChange,再调用 onClear,焦点回到触发器,列表保持关闭。

'use client'

import { useState } from 'react'
import { Select, type SelectValue } from '@hina-ui/react'

const works = [
  { value: 'summer-pockets', label: 'Summer Pockets' },
  { value: 'clannad', label: 'CLANNAD' },
  { value: 'rewrite', label: 'Rewrite' },
]

export default function Demo() {
  const [work, setWork] = useState<SelectValue>('clannad')

  return (
    <Select
      value={work}
      onValueChange={setWork}
      options={works}
      clearable
      aria-label="作品"
      className="w-72"
    />
  )
}
tsx

分组

分组项带 label 与 options,可以与普通选项混排。

'use client'

import { useState } from 'react'
import { Select, type SelectValue } from '@hina-ui/react'

const sorts = [
  { value: 'latest', label: '最近更新' },
  {
    label: '按评分',
    options: [
      { value: 'rating-desc', label: '评分从高到低' },
      { value: 'rating-asc', label: '评分从低到高' },
    ],
  },
  {
    label: '按发售',
    options: [
      { value: 'release-desc', label: '最新发售' },
      { value: 'release-asc', label: '最早发售' },
    ],
  },
]

export default function Demo() {
  const [sort, setSort] = useState<SelectValue>('latest')

  return (
    <Select
      value={sort}
      onValueChange={setSort}
      options={sorts}
      aria-label="排序"
      className="w-56"
    />
  )
}
tsx

定制内容

renderOption 属性定制列表中每一项的内容,renderValue 属性定制触发器里显示的内容,两者都能拿到当前选项。

组件从 options 推断完整选项类型,渲染函数中的 option 保留额外字段及其类型;value / onValueChange 仍绑定 value。可用 SelectOption<{ icon: LucideIcon }> 声明额外字段,示例直接通过 option.icon 读取图标。

'use client'

import { useState } from 'react'
import { BookOpen, Clapperboard, Gamepad2, type LucideIcon } from 'lucide-react'
import { Inline, Select, Stack, Text, type SelectOption, type SelectValue } from '@hina-ui/react'

const types: SelectOption<{ icon: LucideIcon }>[] = [
  { icon: Gamepad2, value: 'gal', label: 'Galgame', description: '视觉小说与冒险游戏' },
  { icon: BookOpen, value: 'ln', label: '轻小说', description: '文库本与网络连载' },
  { icon: Clapperboard, value: 'anime', label: '动画', description: 'TV、剧场版与 OVA' },
]

export default function Demo() {
  const [type, setType] = useState<SelectValue>('gal')

  return (
    <Select
      value={type}
      onValueChange={setType}
      options={types}
      aria-label="作品类型"
      className="w-64"
      renderValue={({ option }) => (
        <Inline as="span" gap="sm" wrap={false}>
          <option.icon />
          {option.label}
        </Inline>
      )}
      renderOption={({ option }) => (
        <Inline as="span" gap="sm" wrap={false}>
          <option.icon />
          <Stack as="span" gap="none" className="min-w-0">
            <Text as="span">{option.label}</Text>
            <Text as="span" size="xs" tone="muted">
              {option.description}
            </Text>
          </Stack>
        </Inline>
      )}
    />
  )
}
tsx

尺寸

三档尺寸与输入框相同。

import { Select, Stack } from '@hina-ui/react'

const options = [
  { value: 'gal', label: 'Galgame' },
  { value: 'ln', label: '轻小说' },
]

export default function Demo() {
  return (
    <Stack className="w-56">
      <Select size="sm" options={options} value="gal" aria-label="小号" />
      <Select size="md" options={options} value="gal" aria-label="中号" />
      <Select size="lg" options={options} value="gal" aria-label="大号" />
    </Stack>
  )
}
tsx

形态

primary 直接放在页面底色上,带边框与阴影;secondary 放在卡片等表面内,只有一层浅色底。

import { Card, Select, Stack } from '@hina-ui/react'

const options = [
  { value: 'gal', label: 'Galgame' },
  { value: 'ln', label: '轻小说' },
]

export default function Demo() {
  return (
    <Stack className="w-56">
      <Select options={options} placeholder="直接放在页面上" aria-label="页面上的选择框" />
      <Card>
        <Select
          variant="secondary"
          options={options}
          placeholder="放在卡片内"
          aria-label="卡片内的选择框"
        />
      </Card>
    </Stack>
  )
}
tsx

状态

invalid 标出校验未通过,disabled 禁用整个选择框,选项上的 disabled 只禁用该项。选项为空时列表显示提示。

import { Select, Stack } from '@hina-ui/react'

const options = [
  { value: 'gal', label: 'Galgame' },
  { value: 'ln', label: '轻小说' },
  { value: 'manga', label: '漫画', disabled: true },
]

export default function Demo() {
  return (
    <Stack className="w-56">
      <Select invalid options={options} placeholder="请选择类型" aria-label="校验未通过" />
      <Select disabled options={options} value="gal" aria-label="已禁用" />
      <Select options={[]} placeholder="没有可选项" aria-label="空列表" />
    </Stack>
  )
}
tsx

在表单中

放进 FormField 后,标签指向触发器,说明与错误信息由字段渲染并关联到它;校验规则、校验时机与提交交给 Form。

'use client'

import { useState } from 'react'
import * as v from 'valibot'
import { Button, Form, FormField, Select, Text } from '@hina-ui/react'

const categories = [
  { label: '游戏', value: 'game' },
  { label: '小说', value: 'novel' },
  { label: '漫画', value: 'manga' },
]

const schema = v.object({
  category: v.pipe(v.string('请选择分类'), v.nonEmpty('请选择分类')),
})

export default function Demo() {
  const [values, setValues] = useState({ category: null as string | number | null | undefined })
  const [saved, setSaved] = useState('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    setSaved(JSON.stringify(data))
  }

  return (
    <Form values={values} rules={schema} className="w-80" onSubmit={save}>
      {({ submitting }) => (
        <>
          <FormField name="category" label="分类" required>
            <Select
              value={values.category}
              onValueChange={category => setValues({ ...values, category })}
              options={categories}
            />
          </FormField>
          <Button type="submit" loading={submitting} className="self-start">
            保存
          </Button>
          {saved && (
            <Text tone="muted" size="sm">
              已保存:{saved}
            </Text>
          )}
        </>
      )}
    </Form>
  )
}
tsx

虚拟滚动

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

已选: 7890

'use client'

import { useState } from 'react'
import { Select, Stack, Text, type SelectValue } from '@hina-ui/react'

const options = Array.from({ length: 10000 }, (_, index) => ({
  value: index,
  label: `条目 ${String(index + 1).padStart(5, '0')}`,
  disabled: index % 97 === 0,
}))

export default function Demo() {
  const [selected, setSelected] = useState<SelectValue>(7890)

  return (
    <Stack gap="sm" className="w-56 max-w-full">
      <Select
        value={selected}
        onValueChange={setSelected}
        options={options}
        virtualize={{ estimateSize: 36, overscan: 6 }}
        aria-label="一万项"
      />
      <Text size="sm" tone="muted">
        已选: {selected}
      </Text>
    </Stack>
  )
}
tsx

行为

  • 浮层贴着触发器展开,宽度与触发器相同,列表超出高度时在浮层内滚动。
  • 打开期间页面锁定滚动,点击外部或者按 Esc 关闭。
  • 支持方向键、Home、End 以及输入首字母快速定位。
  • 选中后浮层关闭,焦点回到触发器。

无障碍

  • 触发器为 role="combobox",列表为 role="listbox",选项为 role="option" 并带 aria-selected。
  • 应当配合 label 元素或者 aria-label 提供名称。invalid 同时设置 aria-invalid。

API

Props

T extends SelectOption 从 options 推断,默认是 SelectOption。

属性
类型
默认值
说明
value
string | number | null
—
选中的值
options
SelectItems<T>
—
选项,见下方类型
virtualize
VirtualizeOptions
false
虚拟滚动;预估行高按内容,overscan 6
clearable
boolean
false
是否显示清除按钮
placeholder
string
语言包
无值时显示的文字
open
boolean
false
浮层是否打开,可受控
variant
'primary' | 'secondary'
'primary'
形态
size
'sm' | 'md' | 'lg'
'md'
尺寸
invalid
boolean
false
是否校验未通过
name
string
—
原生表单字段名
required
boolean
false
设置 name 后启用原生必填校验
autoComplete
string
—
原生表单自动填充提示
disabled
boolean
false
是否禁用
className
string
—
追加至根元素的类名

内容属性

属性
参数
说明
renderValue
{ option: T }
触发器里显示的内容
renderOption
{ option: T }
列表中每一项的内容

回调

回调
参数
说明
onValueChange
value: string | number | null
选中值变化
onOpenChange
open: boolean
浮层开合变化
onClear
—
清除选中值

类型

type SelectOption<T extends object = object> = {
  value: string | number
  label: string
  description?: string
  disabled?: boolean
} & T

interface SelectOptionGroup<T extends SelectOption = SelectOption> {
  label: string
  options: T[]
}

type SelectItems<T extends SelectOption = SelectOption> = Array<T | SelectOptionGroup<T>>
ts
type VirtualizeOptions = boolean | { estimateSize?: number; overscan?: number }
ts