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} // 仅在 user 不为 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 会把值与选项进行比较以找出选中项。对于对象选项,默认按引用 比较对象是否相同,而不是按值比较。如果希望按值比较对象选项, 可以使用 isItemEqualToValue 属性。

<Select
  className="w-[200px]"
  placeholder="Select a language..."
  renderValue={(v) => (
    <span>
      {v.emoji} {v.label}
    </span>
  )}
  value={value}
  onValueChange={(v) => setValue(v)}
  // 提供自定义比较逻辑
  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

属性类型默认值说明
align"start" | "center" | "end"-How to align the popup relative to the specified side.
alignItemWithTriggerboolean-Whether the positioner overlaps the trigger so the selected item's text is aligned with the trigger's value text. This only applies to mouse input and is automatically disabled if there is not enough space.
alignOffsetnumber | OffsetFunction-Additional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side.
anchorReactNode-An element to position the popup against. By default, the popup will be positioned against the trigger.
arrowPaddingnumber-Minimum distance to maintain between the arrow and the edges of the popup. Use it to prevent the arrow element from hanging out of the rounded corners of a popup.
collisionAvoidanceCollisionAvoidance-Determines how to handle collisions when positioning the popup. `side` controls overflow on the preferred placement axis (`top`/`bottom` or `left`/`right`): - `'flip'`: keep the requested side when it fits; otherwise try the opposite side (`top` and `bottom`, or `left` and `right`). - `'shift'`: never change side; keep the requested side and move the popup within the clipping boundary so it stays visible. - `'none'`: do not correct side-axis overflow. `align` controls overflow on the alignment axis (`start`/`center`/`end`): - `'flip'`: keep side, but swap `start` and `end` when the requested alignment overflows. - `'shift'`: keep side and requested alignment, then nudge the popup along the alignment axis to fit. - `'none'`: do not correct alignment-axis overflow. `fallbackAxisSide` controls fallback behavior on the perpendicular axis when the preferred axis cannot fit: - `'start'`: allow perpendicular fallback and try the logical start side first (`top` before `bottom`, or `left` before `right` in LTR). - `'end'`: allow perpendicular fallback and try the logical end side first (`bottom` before `top`, or `right` before `left` in LTR). - `'none'`: do not fallback to the perpendicular axis. When `side` is `'shift'`, explicitly setting `align` only supports `'shift'` or `'none'`. If `align` is omitted, it defaults to `'flip'`.
collisionBoundaryBoundary-An element or a rectangle that delimits the area that the popup is confined to.
collisionPaddingPadding-Additional space to maintain from the edge of the collision boundary.
disableAnchorTrackingboolean-Whether to disable the popup from tracking any layout shift of its positioning anchor.
positionMethod"absolute" | "fixed"-Determines which CSS `position` property to use.
side"top" | "bottom" | "left" | "right" | "inline-end" | "inline-start"-Which side of the anchor element to align the popup against. May automatically change to avoid collisions.
sideOffsetnumber | OffsetFunction-Distance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side.
stickyboolean-Whether to maintain the popup in the viewport after the anchor element was scrolled out of view.
classNamestring-Additional CSS classes merged via `cn()`.
renderReactNode-Replaces the trigger element while preserving Select behavior.
size"xs" | "sm" | "base" | "lg""base"Size of the select trigger. Matches Input component sizes.
labelReactNode-Label content for the select. When provided, enables the Field wrapper with a visible label above the select. For accessibility without a visible label, use `aria-label` instead.
hideLabelboolean--
placeholderstring-Placeholder text shown when no value is selected.
loadingboolean-When `true`, shows a skeleton loader in place of the selected value.
disabledboolean-Whether the select is disabled.
requiredboolean-Whether the select is required. When `false`, shows "(optional)" text.
labelTooltipReactNode-Tooltip content displayed next to the label via an info icon.
valueT-Currently selected value (controlled mode).
childrenReactNode-`Select.Option` elements to render in the dropdown.
descriptionReactNode-Helper text displayed below the select.
errorstring | object-Error message string or validation error object with `match` key.
onValueChange(value: T) => void-Callback when selection changes
defaultValueT-Initial value for uncontrolled mode
renderValue(value: T) => ReactNode-A function that returns a ReactNode to format the selected value in the trigger. Required when using object values. Use `placeholder` for the empty state.
itemsRecord<string, string> | Array<{ label: ReactNode; value: T }>-Data structure of items rendered in the popup. Accepts a plain object map (`{ key: "Label" }`) or an array of `{ label, value }` for object/complex values.
isItemEqualToValue(item: T, value: T) => boolean-Custom equality function for comparing items. Required when value is an object, since object identity (`===`) won't match across renders.

Select.Option

属性类型默认值

该组件没有专属属性,接受标准 HTML 属性。

Select.Group

把相关选项归为一组,带无障碍的 role="group"。 配合 Select.GroupLabel 可提供可见的分组标题。

Select.GroupLabel

Select.Group 的可见标题。为了无障碍,会自动与其父分组关联。

Select.Separator

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