Skip to content
Lenso UI

Select 选择器

Select 展示可折叠的选项列表,并允许用户从中选择一项。

用法

import { Select } from "@lenso/ui";

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample, states } from "./select-example";export function Default() {  return <SelectExample label="State" choices={states} />;}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

组件结构

import {Select, Label, Description, Header, ListBox, Separator} from "@lenso/ui";
export default () => (  <Select>    <Label />    <Select.Trigger>      <Select.Value />      <Select.ClearButton />      <Select.Indicator />    </Select.Trigger>    <Description />    <Select.Popover>      <ListBox>        <ListBox.Item>          <Label />          <Description />          <ListBox.ItemIndicator />        </ListBox.Item>        <ListBox.Section>          <Header />          <ListBox.Item>            <Label />          </ListBox.Item>        </ListBox.Section>      </ListBox>    </Select.Popover>  </Select>);

示例

变体

Select 组件支持两种视觉变体:

  • primary(默认)— 带阴影的标准样式,适用于大多数场景
  • secondary — 低强调、无阴影,适合在 Surface 等表面背景上使用

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { exampleStyles, SelectExample } from "./select-example";const choices = [  { value: "option1", label: "Option 1" },  { value: "option2", label: "Option 2" },];export function Variants() {  return (    <div {...stylex.props(exampleStyles.stack)}>      <SelectExample label="Primary variant" choices={choices} variant="primary" />      <SelectExample label="Secondary variant" choices={choices} variant="secondary" />    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

宽度充满

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { exampleStyles, SelectExample } from "./select-example";export function FullWidth() {  return (    <div {...stylex.props(exampleStyles.wide, exampleStyles.stack)}>      <SelectExample        fluid        label="Favorite Animal"        choices={["Cat", "Dog", "Bird"].map((label) => ({ label, value: label.toLowerCase() }))}      />    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

带描述

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample, states } from "./select-example";export function WithDescription() {  return (    <SelectExample label="State" choices={states} description="Select your state of residence" />  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

必填

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { Form } from "@base-ui/react/form";import * as stylex from "@stylexjs/stylex";import { countrySections, exampleStyles, SelectExample, states } from "./select-example";export function Required() {  return (    <Form      {...stylex.props(exampleStyles.field, exampleStyles.stack)}      onSubmit={(event) => {        event.preventDefault();        alert("Form submitted successfully!");      }}    >      <SelectExample fluid required name="state" label="State" choices={states} />      <SelectExample        fluid        required        name="country"        label="Country"        placeholder="Select a country"        choices={countrySections          .flatMap((section) => section.items)          .filter((item) =>            ["usa", "canada", "mexico", "uk", "france", "germany"].includes(item.value),          )}      />      <button type="submit" {...stylex.props(exampleStyles.action)}>        Submit      </button>    </Form>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

禁用

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { countries, exampleStyles, SelectExample, states } from "./select-example";export function Disabled() {  return (    <div {...stylex.props(exampleStyles.stack)}>      <SelectExample disabled label="State" choices={states} defaultValue="california" />      <SelectExample        disabled        multiple        label="Countries to Visit"        choices={countries.slice(0, 6)}        placeholder="Select countries"        defaultValue={["argentina", "japan", "france"]}      />    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

含禁用选项

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample } from "./select-example";const animals = ["Dog", "Cat", "Bird", "Kangaroo", "Elephant", "Tiger"].map((label) => ({  label,  value: label.toLowerCase(),  disabled: label === "Cat" || label === "Kangaroo",}));export function WithDisabledOptions() {  return <SelectExample label="Animal" placeholder="Select an animal" choices={animals} />;}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

多选

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { countries, SelectExample } from "./select-example";export function MultipleSelect() {  return (    <SelectExample      multiple      label="Countries to Visit"      placeholder="Select countries"      choices={countries}    />  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

分组选项

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { countrySections, SelectExample } from "./select-example";export function WithSections() {  return (    <SelectExample      label="Country"      placeholder="Select a country"      choices={countrySections.flatMap((section) => section.items)}      sections={countrySections}    />  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

受控组件

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { useState } from "react";import { controlledStates, exampleStyles, SelectExample } from "./select-example";export function Controlled() {  const [state, setState] = useState<string | null>("california");  return (    <div {...stylex.props(exampleStyles.compactStack)}>      <SelectExample        label="State (controlled)"        choices={controlledStates}        placeholder="Select a state"        value={state}        onValueChange={setState}      />      <p {...stylex.props(exampleStyles.note)}>        Selected: {controlledStates.find((item) => item.value === state)?.label || "None"}      </p>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

受控多选

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { useState } from "react";import { controlledStates, exampleStyles, SelectExample } from "./select-example";export function ControlledMultiple() {  const [selected, setSelected] = useState<string[]>(["california", "texas"]);  return (    <div {...stylex.props(exampleStyles.stack)}>      <SelectExample        multiple        label="States (controlled multiple)"        choices={controlledStates}        placeholder="Select states"        value={selected}        onValueChange={setSelected}      />      <p {...stylex.props(exampleStyles.note)}>        Selected: {selected.length > 0 ? selected.join(", ") : "None"}      </p>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

受控展开状态

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { useState } from "react";import { exampleStyles, SelectExample, states } from "./select-example";export function ControlledOpenState() {  const [open, setOpen] = useState(false);  return (    <div {...stylex.props(exampleStyles.stack)}>      <SelectExample label="State" choices={states} open={open} onOpenChange={setOpen} />      <button type="button" {...stylex.props(exampleStyles.action)} onClick={() => setOpen(!open)}>        {open ? "Close" : "Open"} Select      </button>      <p {...stylex.props(exampleStyles.note)}>Select is {open ? "open" : "closed"}</p>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

异步加载

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** * HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 * Modified: abortable native fetch and scroll pagination replace RAC collection/load-more. */import { Spinner } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useCallback, useEffect, useRef, useState } from "react";import { exampleStyles, SelectExample } from "./select-example";interface PokemonPage {  next: string | null;  results: { name: string }[];}export function AsynchronousLoading() {  const [pokemon, setPokemon] = useState<{ name: string }[]>([]);  const [loading, setLoading] = useState(false);  const [error, setError] = useState(false);  const cursor = useRef<string | null>("https://pokeapi.co/api/v2/pokemon");  const controller = useRef<AbortController | null>(null);  const load = useCallback(async () => {    if (!cursor.current || controller.current) return;    const request = new AbortController();    controller.current = request;    setLoading(true);    setError(false);    try {      const response = await fetch(cursor.current, { signal: request.signal });      if (!response.ok) throw new Error(`Pokemon request failed (${response.status})`);      const page: PokemonPage = await response.json();      cursor.current = page.next;      setPokemon((previous) => [...previous, ...page.results]);    } catch {      if (!request.signal.aborted) setError(true);    } finally {      if (controller.current === request) {        controller.current = null;        setLoading(false);      }    }  }, []);  useEffect(() => {    void load();    return () => {      controller.current?.abort();      controller.current = null;    };  }, [load]);  return (    <SelectExample      label="Pick a Pokemon"      placeholder="Select a Pokemon"      choices={pokemon.map(({ name }) => ({ value: name, label: name }))}      onPopoverScroll={(event) => {        const element = event.currentTarget;        if (element.scrollHeight - element.scrollTop - element.clientHeight < 48) void load();      }}      footer={        <div {...stylex.props(exampleStyles.loading)}>          {loading && (            <>              <Spinner size="sm" />              <span {...stylex.props(exampleStyles.note)}>Loading more...</span>            </>          )}          {!loading && cursor.current && (            <button              type="button"              {...stylex.props(exampleStyles.action)}              onClick={() => void load()}            >              {error ? "Retry loading" : "Load more"}            </button>          )}        </div>      }    />  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

自定义指示器

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { ChevronsExpandVertical } from "@gravity-ui/icons";import { SelectExample, states } from "./select-example";export function CustomIndicator() {  return (    <SelectExample      label="State"      choices={states}      indicator={<ChevronsExpandVertical width={12} height={12} aria-hidden="true" />}    />  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

带清除按钮

Select.Trigger 本身是 button,因此不能再嵌套 <button> 来实现清除。请在触发器内组合 Select.ClearButton — 它渲染为非 button 控件,且不会打开菜单。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { useState } from "react";import { SelectExample, states } from "./select-example";export function WithClearButton() {  const [value, setValue] = useState<string | null>("california");  return (    <SelectExample      label="State"      choices={states}      value={value}      onValueChange={setValue}      clear={value === null ? undefined : () => setValue(null)}    />  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

由于 ARIA 将 button 的子节点视为纯展示内容,清除控件无法获得焦点,因此它带有 aria-hidden,仅作为指针操作的可视入口。当组合了 Select.ClearButton 时,触发器同时支持按 Backspace 或 Delete 清除,这是键盘与屏幕阅读器用户的清除方式。两种方式都会调用 onClear。

自定义展示值

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { Avatar, AvatarImage, AvatarFallback } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { exampleStyles, SelectExample } from "./select-example";const users = [  { id: "1", name: "Bob", email: "[email protected]", color: "blue" },  { id: "2", name: "Fred", email: "[email protected]", color: "green" },  { id: "3", name: "Martha", email: "[email protected]", color: "purple" },  { id: "4", name: "John", email: "[email protected]", color: "red" },  { id: "5", name: "Jane", email: "[email protected]", color: "orange" },];function UserAvatar({ user, small = false }: { user: (typeof users)[number]; small?: boolean }) {  return (    <Avatar size="sm" xstyle={small ? exampleStyles.smallAvatar : undefined}>      <AvatarImage        src={`https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/${user.color}.jpg`}        alt=""      />      <AvatarFallback>{user.name.charAt(0)}</AvatarFallback>    </Avatar>  );}export function CustomValue() {  return (    <SelectExample      label="User"      placeholder="Select a user"      choices={users.map((user) => ({        value: user.id,        label: user.name,        content: (          <div {...stylex.props(exampleStyles.row)}>            <UserAvatar user={user} />            <div {...stylex.props(exampleStyles.details)}>              <span>{user.name}</span>              <span {...stylex.props(exampleStyles.note)}>{user.email}</span>            </div>          </div>        ),      }))}      valueContent={(value) => {        const user = users.find((item) => item.id === value);        return user ? (          <span {...stylex.props(exampleStyles.row)}>            <UserAvatar user={user} small />            <span>{user.name}</span>          </span>        ) : (          "Select a user"        );      }}    />  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

渲染函数

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample, states } from "./select-example";export function RenderFunction() {  return <SelectExample customRender label="State" choices={states} />;}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

表面样式

在 Surface 内使用时,请使用 variant="secondary",以应用适合表面背景的低强调变体。

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { Form } from "@base-ui/react/form";import * as stylex from "@stylexjs/stylex";import { countrySections, exampleStyles, SelectExample, states } from "./select-example";export function OnSurface() {  return (    <div {...stylex.props(exampleStyles.surface)}>      <Form        {...stylex.props(exampleStyles.stack)}        onSubmit={(event) => {          event.preventDefault();          alert("Form submitted successfully!");        }}      >        <SelectExample          fluid          required          name="state"          label="State"          choices={states}          variant="secondary"        />        <SelectExample          fluid          required          name="country"          label="Country"          placeholder="Select a country"          choices={countrySections            .flatMap((section) => section.items)            .filter((item) =>              ["usa", "canada", "mexico", "uk", "france", "germany"].includes(item.value),            )}          variant="secondary"        />        <button type="submit" {...stylex.props(exampleStyles.action)}>          Submit        </button>      </Form>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

自定义样式

Tailwind CSS

此预览复用英文版适配,不代表中文源示例已完成本地实现。

"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample } from "./select-example";export function CustomStyles() {  return (    <SelectExample      custom      label="Plan"      placeholder="Pick a plan"      variant="secondary"      choices={[        { value: "free", label: "Free" },        { value: "pro", label: "Pro" },      ]}    />  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

全局 CSS

若要自定义组件类,可使用 @layer components 指令。了解更多。

@layer components {  .select {    @apply flex flex-col gap-1;  }
  .select__trigger {    @apply rounded-lg border border-border bg-surface p-2;  }
  .select__value {    @apply text-current;  }
  .select__indicator {    @apply text-muted;  }
  .select__popover {    @apply rounded-lg border border-border bg-surface p-2;  }}

样式参考

HeroUI 遵循 BEM 方法论,确保组件变体与状态可复用且易于自定义。

CSS 类

Select 组件使用以下 CSS 类(查看源码样式):

基础类 [!toc]

  • .select - Select 根容器
  • .select__trigger - 打开下拉的触发按钮
  • .select__value - 当前显示的值或占位符
  • .select__clear-button - 触发器内的可选清除控件
  • .select__indicator - 下拉指示图标
  • .select__popover - 弹出层容器

变体类 [!toc]

  • .select--primary - Primary 变体,带阴影(默认)
  • .select--secondary - Secondary 变体,无阴影,适合在 Surface 上使用

状态类 [!toc]

  • .select[data-invalid="true"] - 无效状态
  • .select__trigger[data-focus-visible="true"] - 触发器聚焦状态
  • .select__trigger[data-disabled="true"] - 触发器禁用状态
  • .select__value[data-placeholder="true"] - 占位符状态
  • .select__clear-button[data-empty="true"] - 无选中项时隐藏清除控件
  • .select__indicator[data-open="true"] - 展开时的指示器状态

交互状态

该组件同时支持 CSS 伪类与 data 属性,以提供更灵活的状态控制:

  • 悬停:触发器上的 :hover 或 [data-hovered="true"]
  • 聚焦:触发器上的 :focus-visible 或 [data-focus-visible="true"]
  • 禁用:Select 上的 :disabled 或 [data-disabled="true"]
  • 展开:指示器上的 [data-open="true"]

API 参考

Select

Prop类型默认值描述
placeholderstring'Select an item'Select 为空时显示的占位符文本。
selectionMode"single" | "multiple""single"启用单选或多选。
isOpenboolean-设置菜单是否打开(受控)。
defaultOpenboolean-设置菜单默认是否打开(非受控)。
onOpenChange(isOpen: boolean) => void-展开状态变化时的事件处理函数。
disabledKeysIterable<Key>-禁用条目的 key。
isDisabledboolean-Select 是否禁用。
valueKey | Key[] | null-当前值(受控)。
defaultValueKey | Key[] | null-默认值(非受控)。
onChange(value: Key | Key[] | null) => void-值变化时的事件处理函数。
onClear() => void-清除选择时的事件处理函数。
isRequiredboolean-用户输入是否必填。
isInvalidboolean-Select 的值是否无效。
namestring-输入框名称,用于提交 HTML 表单。
autoCompletestring-描述自动完成行为类型。
fullWidthbooleanfalseSelect 是否占满容器宽度。
variant"primary" | "secondary""primary"视觉变体。primary 为默认带阴影样式。secondary 为低强调、无阴影变体,适合在 Surface 上使用。
classNamestring-额外的 CSS 类。
childrenReactNode | RenderFunction-Select 内容或渲染函数。
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, SelectRenderProps>-使用自定义渲染函数覆盖默认 DOM 元素。

Select.Trigger

Prop类型默认值描述
classNamestring-额外的 CSS 类。
childrenReactNode | RenderFunction-触发器内容或渲染函数。

Select.Value

Prop类型默认值描述
classNamestring-额外的 CSS 类。
childrenReactNode | RenderFunction-值区域内容或渲染函数。
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, SelectValueRenderProps>-使用自定义渲染函数覆盖默认 DOM 元素。

Select.Indicator

Prop类型默认值描述
classNamestring-额外的 CSS 类。
childrenReactNode-自定义指示器内容。

Select.ClearButton

Prop类型默认值描述
classNamestring-额外的 CSS 类。
childrenReactNode-自定义内容,替代默认图标。
onClick(e: MouseEvent) => void-点击清除控件时的事件处理函数。

Select.ClearButton 渲染为 span,因此可以放在 Select.Trigger(button)内部而不会形成嵌套 button。选择为空时会自动隐藏。

Select.Popover

Prop类型默认值描述
placement"bottom" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top" | "top left" | "top right" | "top start" | "top end" | "left" | "left top" | "left bottom" | "start" | "start top" | "start bottom" | "right" | "right top" | "right bottom" | "end" | "end top" | "end bottom""bottom"弹出层相对触发器的位置。
classNamestring-额外的 CSS 类。
childrenReactNode-子内容。

RenderProps

对 Select.Value 使用渲染函数时,会提供以下值:

Prop类型描述
defaultChildrenReactNode默认渲染的值。
isPlaceholderboolean是否为占位符状态。
stateSelectStateSelect 的状态。
selectedItemsNode[]当前已选中的条目。

无障碍

Select 组件实现 ARIA 列表框模式,并提供:

  • 完整的键盘导航支持
  • 选择变化时的屏幕阅读器播报
  • 合理的焦点管理
  • 禁用状态支持
  • 输入首字母快速定位(typeahead)
  • 与 HTML 表单的集成

更多信息见 React Aria Select 文档。

相关组件