Favorite Fruit
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Basic Select with visible label - the recommended pattern. */
export function SelectBasicDemo() {
  const [value, setValue] = useState("apple");

  return (
    <Select
      label="Favorite Fruit"
      className="w-[200px]"
      value={value}
      onValueChange={(v) => setValue(v ?? "apple")}
      items={{ apple: "Apple", banana: "Banana", cherry: "Cherry" }}
    />
  );
}

安装

桶式导出

import { Select } from "@cloudflare/kumo";

细粒度导入

import { Select } from "@cloudflare/kumo/components/select";

用法

import { Select } from "@cloudflare/kumo";

export default function Example() {
  const [value, setValue] = useState("apple");

  return (
    <Select
      label="Favorite Fruit"
      value={value}
      onValueChange={(v) => setValue(v ?? "apple")}
      items={{ apple: "Apple", banana: "Banana", cherry: "Cherry" }}
    />
  );
}

示例

基础用法

带有可见标签的 Select。提供 label 属性时, Select 会自动在 Field 包装内渲染, 标签显示在其上方。

Favorite Fruit
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Basic Select with visible label - the recommended pattern. */
export function SelectBasicDemo() {
  const [value, setValue] = useState("apple");

  return (
    <Select
      label="Favorite Fruit"
      className="w-[200px]"
      value={value}
      onValueChange={(v) => setValue(v ?? "apple")}
      items={{ apple: "Apple", banana: "Banana", cherry: "Cherry" }}
    />
  );
}

尺寸

使用 size 属性与 Input 的尺寸保持一致(xs、sm、base、lg)。

xs
sm
base
lg
import { Select } from "@cloudflare/kumo";

/** Select trigger sizes (xs/sm/base/lg) matching Input and Combobox. */
export function SelectSizesDemo() {
  return (
    <div className="grid gap-4">
      <div className="flex items-center gap-3">
        <span className="w-10 text-sm text-kumo-subtle">xs</span>
        <Select
          aria-label="Select size xs"
          size="xs"
          className="w-[200px]"
          placeholder="Choose..."
          items={{ a: "Option A", b: "Option B" }}
        />
      </div>
      <div className="flex items-center gap-3">
        <span className="w-10 text-sm text-kumo-subtle">sm</span>
        <Select
          aria-label="Select size sm"
          size="sm"
          className="w-[200px]"
          placeholder="Choose..."
          items={{ a: "Option A", b: "Option B" }}
        />
      </div>
      <div className="flex items-center gap-3">
        <span className="w-10 text-sm text-kumo-subtle">base</span>
        <Select
          aria-label="Select size base"
          size="base"
          className="w-[200px]"
          placeholder="Choose..."
          items={{ a: "Option A", b: "Option B" }}
        />
      </div>
      <div className="flex items-center gap-3">
        <span className="w-10 text-sm text-kumo-subtle">lg</span>
        <Select
          aria-label="Select size lg"
          size="lg"
          className="w-[200px]"
          placeholder="Choose..."
          items={{ a: "Option A", b: "Option B" }}
        />
      </div>
    </div>
  );
}

弹出位置

弹出面板默认优先显示在 side="bottom",该方向空间不足时会自动翻转。 使用 side、align、sideOffset 和 alignOffset 可以显式固定位置, 碰撞处理仍然生效。

side=bottom (default)
side=top
align=end
sideOffset=12
import { Select } from "@cloudflare/kumo";

/**
 * Use `side` and `align` to pin the popup. Placement still flips automatically
 * when the chosen side runs out of room.
 */
export function SelectPlacementDemo() {
  return (
    <div className="grid gap-4 sm:grid-cols-2">
      <Select
        label="side=bottom (default)"
        className="w-[200px]"
        defaultValue="earth"
        items={planets}
      />
      <Select
        label="side=top"
        side="top"
        className="w-[200px]"
        defaultValue="earth"
        items={planets}
      />
      <Select
        label="align=end"
        align="end"
        className="w-[200px]"
        defaultValue="earth"
        items={planets}
      />
      <Select
        label="sideOffset=12"
        sideOffset={12}
        className="w-[200px]"
        defaultValue="earth"
        items={planets}
      />
    </div>
  );
}

对齐到选中项

设置 alignItemWithTrigger 可将弹出面板覆盖在触发器上,使选中项正好位于 其正上方,行为与原生 select 一致。选中项之前的选项渲染在触发器上方, 其余选项渲染在下方,因此弹出面板可以向两个方向延伸。 下面的两个 Select 都在列表中间选中了 Mars, 可以打开对比效果。当空间不足时, 此模式会自动停用并回退到常规的锚定定位。

Anchored (default)

Opens below the trigger

Aligned to selection

Selected option lands on the trigger

import { Select } from "@cloudflare/kumo";

/**
 * `alignItemWithTrigger` overlays the popup on the trigger so the selected
 * option sits directly on top of it, like a native `<select>`. Options before
 * the selection render above the trigger and the rest below. Open the second
 * select — "Mars" is selected mid-list, so the popup extends in both
 * directions. Falls back to normal anchored placement when space runs out.
 */
export function SelectDynamicPlacementDemo() {
  return (
    <div className="flex flex-wrap items-start gap-8">
      <Select
        label="Anchored (default)"
        description="Opens below the trigger"
        className="w-[200px]"
        defaultValue="mars"
        items={planets}
      />
      <Select
        label="Aligned to selection"
        description="Selected option lands on the trigger"
        alignItemWithTrigger
        className="w-[200px]"
        defaultValue="mars"
        items={planets}
      />
    </div>
  );
}

无可见标签

当不需要可见标签时(例如紧凑界面或上下文已明确时), 使用 aria-label 保证无障碍。

import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Select without visible label - use aria-label for accessibility. */
export function SelectWithoutLabelDemo() {
  const [value, setValue] = useState("apple");

  return (
    <Select
      aria-label="Select a fruit"
      className="w-[200px]"
      value={value}
      onValueChange={(v) => setValue(v ?? "apple")}
      items={{ apple: "Apple", banana: "Banana", cherry: "Cherry" }}
    />
  );
}

带描述

Select 与 Field 包装集成, 可在输入框下方显示描述文字。

Issue Type

Choose the category that best describes your issue

import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Select with label and description text. */
export function SelectWithDescriptionDemo() {
  const [value, setValue] = useState<string | null>(null);

  return (
    <Select
      label="Issue Type"
      description="Choose the category that best describes your issue"
      className="w-[280px]"
      value={value}
      onValueChange={(v) => setValue(v as string | null)}
      items={{
        bug: "Bug",
        documentation: "Documentation",
        feature: "Feature",
      }}
    />
  );
}

错误状态

传入 error 属性可显示校验错误。存在错误时, 界面中会用错误信息替代描述。

Issue Type
Please select an issue type
import { Select } from "@cloudflare/kumo";

/** Select with label and validation error. */
export function SelectWithErrorDemo() {
  return (
    <Select
      label="Issue Type"
      error="Please select an issue type"
      className="w-[280px]"
      value={null}
      items={{
        bug: "Bug",
        documentation: "Documentation",
        feature: "Feature",
      }}
    />
  );
}

占位符

使用 placeholder 属性,在未选择任何值时显示文字。使用 renderValue 自定义选中值的显示时,当值为 null 会显示占位符, 而不会调用 renderValue。

<Select
  placeholder="Select a user..."
  value={user}
  renderValue={(user) => user.name} // Only called when user is not null
/>
Category
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Select with placeholder text when no value is selected. */
export function SelectPlaceholderDemo() {
  const [value, setValue] = useState<string | null>(null);

  return (
    <Select
      label="Category"
      placeholder="Choose a category..."
      className="w-[200px]"
      value={value}
      onValueChange={(v) => setValue(v as string | null)}
      items={{
        bug: "Bug",
        documentation: "Documentation",
        feature: "Feature",
      }}
    />
  );
}

带提示的标签

使用 labelTooltip 在标签旁添加一个提示图标, 用于提供补充说明。

Priority
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Select with label tooltip for additional context. */
export function SelectWithTooltipDemo() {
  const [value, setValue] = useState<string | null>(null);

  return (
    <Select
      label="Priority"
      labelTooltip="Higher priority issues are addressed first"
      placeholder="Select priority"
      className="w-[200px]"
      value={value}
      onValueChange={(v) => setValue(v as string | null)}
      items={{
        low: "Low",
        medium: "Medium",
        high: "High",
        critical: "Critical",
      }}
    />
  );
}

自定义渲染

使用 renderValue 自定义选中值在触发器按钮中的外观。当处理复杂的 对象数据结构,而不只是简单字符串值时, 这一用法很有用。

Language
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Select with custom rendering for complex option display. */
export function SelectCustomRenderingDemo() {
  const [value, setValue] = useState(languages[0]);

  return (
    <Select
      label="Language"
      className="w-[200px]"
      renderValue={(v) => (
        <span>
          {v.emoji} {v.label}
        </span>
      )}
      value={value}
      onValueChange={(v) => setValue(v as (typeof languages)[0])}
    >
      {languages.map((language) => (
        <Select.Option key={language.value} value={language}>
          {language.emoji} {language.label}
        </Select.Option>
      ))}
    </Select>
  );
}

只有在选中值后才会调用 renderValue 函数。使用 placeholder 定义未选择任何值时的显示内容。

Select 会拿值与 items 比较,以确定哪个选项被选中。 对于对象类型的选项,默认按引用比较,而不是按值比较。 如果希望按值比较对象选项,可以使用 isItemEqualToValue 属性。

<Select
  className="w-[200px]"
  placeholder="Select a language..."
  renderValue={(v) => (
    <span>
      {v.emoji} {v.label}
    </span>
  )}
  value={value}
  onValueChange={(v) => setValue(v)}
  // Provides custom comparison logic
  isItemEqualToValue={(item, value) => item.value === value.value}
>
  {languages.map((language) => (
    <Select.Option key={language.value} value={language}>
      {language.emoji} {language.label}
    </Select.Option>
  ))}
</Select>

加载状态

带加载状态的 Select 组件。加载状态通过 loading 属性 传给组件。

加载状态

从服务器加载(模拟延迟 2 秒)

Assignee
import { Select } from "@cloudflare/kumo";

/** Select in loading state. */
export function SelectLoadingDemo() {
  return <Select aria-label="Loading select" className="w-[200px]" loading />;
}

多选

使用 multiple 属性启用多选。此时值变为选中项的数组。 使用 placeholder 定义空状态,使用 renderValue 自定义选中项如何显示。

<Select
  multiple
  placeholder="Select columns..."
  value={selectedColumns}
  renderValue={(columns) => columns.join(", ")}
  onValueChange={setSelectedColumns}
>
  <Select.Option value="name">Name</Select.Option>
  <Select.Option value="email">Email</Select.Option>
</Select>
Visible Columns
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Multi-select for choosing multiple values. */
export function SelectMultipleDemo() {
  const [value, setValue] = useState<string[]>(["Name", "Location", "Size"]);

  return (
    <Select
      label="Visible Columns"
      className="w-[250px]"
      multiple
      renderValue={(value) => {
        if (value.length > 3) {
          return (
            <span className="line-clamp-1">
              {value.slice(0, 2).join(", ") + ` and ${value.length - 2} more`}
            </span>
          );
        }
        return <span>{value.join(", ")}</span>;
      }}
      value={value}
      onValueChange={(v) => setValue(v as string[])}
    >
      <Select.Option value="Name">Name</Select.Option>
      <Select.Option value="Location">Location</Select.Option>
      <Select.Option value="Size">Size</Select.Option>
      <Select.Option value="Read">Read</Select.Option>
      <Select.Option value="Write">Write</Select.Option>
      <Select.Option value="CreatedAt">Created At</Select.Option>
    </Select>
  );
}

更多示例

Author

Select the primary author for this document

import { useState } from "react";
import { Select, Text } from "@cloudflare/kumo";

/** Select with complex object values and custom option rendering. */
export function SelectComplexDemo() {
  const [value, setValue] = useState<(typeof authors)[0] | null>(null);

  return (
    <Select
      label="Author"
      description="Select the primary author for this document"
      placeholder="Select an author"
      className="w-[200px]"
      onValueChange={(v) => setValue(v as (typeof authors)[0] | null)}
      value={value}
      isItemEqualToValue={(item, value) => item?.id === value?.id}
      renderValue={(author) => author.name}
    >
      {authors.map((author) => (
        <Select.Option key={author.id} value={author}>
          <div className="flex w-[300px] items-center justify-between gap-2">
            <Text>{author.name}</Text>
            <Text variant="secondary">{author.title}</Text>
          </div>
        </Select.Option>
      ))}
    </Select>
  );
}

禁用选项

选项可以通过 disabled 属性禁用。 禁用选项呈灰色显示,无法被选中。

Deployment Region
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Select with disabled options that cannot be selected. */
export function SelectDisabledOptionsDemo() {
  const [value, setValue] = useState<Region | null>(null);

  return (
    <Select
      label="Deployment Region"
      placeholder="Choose a region..."
      className="w-[250px]"
      value={value}
      onValueChange={(v) => setValue(v as Region | null)}
      isItemEqualToValue={(item, val) => item.value === val.value}
    >
      {regions.map((region) => (
        <Select.Option
          key={region.value}
          value={region}
          disabled={region.disabled}
        >
          {region.label}
        </Select.Option>
      ))}
    </Select>
  );
}

禁用项目(通过 items 属性)

items 对象映射属性既接受带 disabled 的描述对象, 也接受普通的字符串值。

Plan
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Select using the items prop with disabled descriptors. */
export function SelectDisabledItemsDemo() {
  const [value, setValue] = useState<string | null>("free");

  return (
    <Select
      label="Plan"
      className="w-[200px]"
      value={value}
      onValueChange={(v) => setValue(v as string | null)}
      items={{
        free: "Free",
        pro: "Pro",
        business: { label: "Business", disabled: true },
        enterprise: { label: "Enterprise", disabled: true },
      }}
    />
  );
}

分组选项

使用 Select.Group、Select.GroupLabel 和 Select.Separator, 用带标签的标题和视觉分隔线组织选项。

Food
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Select with grouped options organized under labeled headers. */
export function SelectGroupedDemo() {
  const [value, setValue] = useState<Food | null>(null);

  return (
    <Select
      label="Food"
      placeholder="Pick a food..."
      className="w-[220px]"
      value={value}
      onValueChange={(v) => setValue(v as Food | null)}
      isItemEqualToValue={(item, val) => item.value === val.value}
    >
      <Select.Group>
        <Select.GroupLabel>Fruits</Select.GroupLabel>
        {foods.fruits.map((food) => (
          <Select.Option key={food.value} value={food}>
            {food.label}
          </Select.Option>
        ))}
      </Select.Group>
      <Select.Separator />
      <Select.Group>
        <Select.GroupLabel>Vegetables</Select.GroupLabel>
        {foods.vegetables.map((food) => (
          <Select.Option key={food.value} value={food}>
            {food.label}
          </Select.Option>
        ))}
      </Select.Group>
    </Select>
  );
}

分组与禁用选项

将分组、分隔线和禁用选项与信息提示结合使用, 可以清晰地区分可用与不可用的选择。

Server Region
import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Grouped select with disabled options and info tooltips. */
export function SelectGroupedWithDisabledDemo() {
  const [value, setValue] = useState<ServerRegion | null>(null);

  return (
    <Select
      label="Server Region"
      placeholder="Select a region..."
      className="w-[260px]"
      value={value}
      onValueChange={(v) => setValue(v as ServerRegion | null)}
      isItemEqualToValue={(item, val) => item.value === val.value}
    >
      <Select.Group>
        <Select.GroupLabel>Available</Select.GroupLabel>
        {serverRegions.available.map((region) => (
          <Select.Option key={region.value} value={region}>
            {region.label}
          </Select.Option>
        ))}
      </Select.Group>
      <Select.Separator />
      <Select.Group>
        <Select.GroupLabel>Unavailable</Select.GroupLabel>
        {serverRegions.unavailable.map((region) => (
          <Select.Option key={region.value} value={region} disabled>
            {region.label}
          </Select.Option>
        ))}
      </Select.Group>
    </Select>
  );
}

长列表(滚动测试)

包含大量选项的 Select 组件,用于测试弹出面板的滚动行为。弹出面板应平滑滚动, 不会出现反弹或过度滚动的问题。

Long List Select

Tests scrolling behavior with many options

import { useState } from "react";
import { Select } from "@cloudflare/kumo";

/** Select with a long list to test popup scrolling behavior. */
export function SelectLongListDemo() {
  const [value, setValue] = useState<LongListItem | null>(null);

  return (
    <Select
      label="Long List Select"
      description="Tests scrolling behavior with many options"
      placeholder="Choose an option..."
      className="w-[220px]"
      value={value}
      onValueChange={(v) => setValue(v as LongListItem | null)}
      isItemEqualToValue={(item, val) => item.value === val.value}
    >
      {longListItems.map((item) => (
        <Select.Option key={item.value} value={item}>
          {item.label}
        </Select.Option>
      ))}
    </Select>
  );
}

API 参考

Select

PropTypeDefaultDescription
align"start" | "center" | "end"-

如何相对于指定边对齐弹出层。

alignItemWithTriggerboolean-

定位器是否与触发器重叠,以使选中项文本与触发器的值文本对齐。这仅适用于鼠标输入,并在空间不足时自动禁用。

alignOffsetnumber | OffsetFunction-

沿对齐轴的额外偏移量(像素)。也接受一个函数,该函数返回偏移量,用以读取锚点和定位元素及其所在边和对齐方式的尺寸。

该函数接受一个包含以下属性的 data 对象参数:

  • data.anchor:锚点元素的尺寸,包含 width 和 height 属性。
  • data.positioner:定位元素的尺寸,包含 width 和 height 属性。
  • data.side:定位元素相对于锚点元素对齐的边。
  • data.align:定位元素相对于指定边的对齐方式。
anchorReactNode-

用于定位弹出层的参照元素。默认情况下,弹出层将相对于触发器进行定位。

arrowPaddingnumber-

箭头与弹出层边缘之间需保持的最小距离。

用于防止箭头元素超出弹出层的圆角。

collisionAvoidanceCollisionAvoidance-

确定定位弹出层时如何处理碰撞。

side 控制首选放置轴(top/bottom 或 left/right)上的溢出:

  • 'flip':当请求的边适合时保持;否则尝试相反方向(top 和 bottom,或 left 和 right)。
  • 'shift':不改变边;保持请求的边并在裁剪边界内移动弹出层,使其保持可见。
  • 'none':不修正侧轴溢出。

align 控制对齐轴(start/center/end)上的溢出:

  • 'flip':保持侧边,但当请求的对齐溢出时交换 start 和 end。
  • 'shift':保持侧边和请求的对齐,然后沿对齐轴微调弹出层以适配。
  • 'none':不修正对齐轴溢出。

fallbackAxisSide 控制当首选轴无法适配时垂直于轴上的回退行为:

  • 'start':允许垂直回退并首先尝试逻辑起始侧(在 LTR 中 top 先于 bottom,或 left 先于 right)。
  • 'end':允许垂直回退并首先尝试逻辑结束侧(在 LTR 中 bottom 先于 top,或 right 先于 left)。
  • 'none':不进行垂直轴回退。

当 side 为 'shift' 时,显式设置 align 仅支持 'shift' 或 'none'。如果省略 align,默认值为 'flip'。

collisionBoundaryBoundary-

一个元素或矩形,用于限定弹出层所在的区域。

collisionPaddingPadding-

与碰撞边界边缘保持的额外间距。

disableAnchorTrackingboolean-

是否禁用弹窗对其定位锚点的布局偏移追踪。

positionMethod"absolute" | "fixed"-

确定使用哪个 CSS position 属性。

side"top" | "bottom" | "left" | "right" | "inline-end" | "inline-start"-

弹出层对齐锚元素的哪一侧。可能会自动更改以避免碰撞。

sideOffsetnumber | OffsetFunction-

锚点与弹出层之间的距离(像素)。也接受一个函数,该函数返回距离以读取锚点和定位器元素的尺寸,以及其侧边和对齐方式。

该函数接受一个 data 对象参数,具有以下属性:

  • data.anchor:锚点元素的尺寸,具有 width 和 height 属性。
  • data.positioner:定位器元素的尺寸,具有 width 和 height 属性。
  • data.side:定位器对齐到的锚点元素的一侧。
  • data.align:定位器相对于指定侧边的对齐方式。
stickyboolean-

锚元素滚动出视口后,是否将弹出层保持在视口内。

classNamestring-

通过 cn() 合并的额外 CSS 类。

renderReactNode-

替换触发元素,同时保留 Select 行为。

size"xs" | "sm" | "base" | "lg""base"

选择触发器的大小。与 Input 组件大小匹配。

labelReactNode-

选择框的标签内容。提供后,将启用 Field 包装器,并在选择框上方显示可见标签。如需在无可见标签时实现无障碍,请改用 aria-label。

hideLabelboolean--
placeholderstring-

未选择任何值时所显示的占位文本。

loadingboolean-

当 true 时,在所选值的位置显示骨架加载器。

disabledboolean-

选择器是否被禁用。

requiredboolean-

选择器是否为必填项。当为 false 时,显示“(可选)”文本。

labelTooltipReactNode-

通过信息图标显示在标签旁边的 Tooltip 内容。

valueT-

当前选中的值(受控模式)。

childrenReactNode-

Select.Option 元素,用于在下拉框中渲染。

descriptionReactNode-

显示在选择框下方的帮助文本。

errorstring | object-

错误消息字符串或带有 match 键的验证错误对象。

onValueChange(value: T) => void-

选择变化时的回调

defaultValueT-

非受控模式的初始值。

renderValue(value: T) => ReactNode-

一个函数,返回用于在触发器中格式化选中值的 ReactNode。使用对象值时必填。空状态使用 placeholder。

itemsRecord<string, string> | Array<{ label: ReactNode; value: T }>-

弹窗中渲染的项目数据结构。接受纯对象映射({ key: "Label" })或 { label, value } 数组用于对象/复杂值。

isItemEqualToValue(item: T, value: T) => boolean-

用于比较项目的自定义相等函数。当值为对象时需要,因为对象标识(===)在多次渲染中无法匹配。

Select.Option

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Select.Group

使用可访问的 role="group" 将相关选项分组。 可配合 Select.GroupLabel 提供可见标题。

Select.GroupLabel

Select.Group 的可见标题。 自动与其父组关联,保证无障碍。

Select.Separator

选项组之间的视觉分隔线。渲染为 role="separator"。