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 包装内渲染,
标签显示在其上方。
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)。
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 可以显式固定位置,
碰撞处理仍然生效。
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,
可以打开对比效果。当空间不足时,
此模式会自动停用并回退到常规的锚定定位。
Opens below the trigger
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 包装集成, 可在输入框下方显示描述文字。
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 属性可显示校验错误。存在错误时,
界面中会用错误信息替代描述。
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
/>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 在标签旁添加一个提示图标,
用于提供补充说明。
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 自定义选中值在触发器按钮中的外观。当处理复杂的
对象数据结构,而不只是简单字符串值时,
这一用法很有用。
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 秒)
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>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>
);
}更多示例
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 属性禁用。
禁用选项呈灰色显示,无法被选中。
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 的描述对象,
也接受普通的字符串值。
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,
用带标签的标题和视觉分隔线组织选项。
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>
);
}分组与禁用选项
将分组、分隔线和禁用选项与信息提示结合使用, 可以清晰地区分可用与不可用的选择。
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 组件,用于测试弹出面板的滚动行为。弹出面板应平滑滚动, 不会出现反弹或过度滚动的问题。
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
| Prop | Type | Default | Description |
|---|---|---|---|
| align | "start" | "center" | "end" | - | 如何相对于指定边对齐弹出层。 |
| alignItemWithTrigger | boolean | - | 定位器是否与触发器重叠,以使选中项文本与触发器的值文本对齐。这仅适用于鼠标输入,并在空间不足时自动禁用。 |
| alignOffset | number | OffsetFunction | - | 沿对齐轴的额外偏移量(像素)。也接受一个函数,该函数返回偏移量,用以读取锚点和定位元素及其所在边和对齐方式的尺寸。 该函数接受一个包含以下属性的
|
| anchor | ReactNode | - | 用于定位弹出层的参照元素。默认情况下,弹出层将相对于触发器进行定位。 |
| arrowPadding | number | - | 箭头与弹出层边缘之间需保持的最小距离。 用于防止箭头元素超出弹出层的圆角。 |
| collisionAvoidance | CollisionAvoidance | - | 确定定位弹出层时如何处理碰撞。
当 |
| collisionBoundary | Boundary | - | 一个元素或矩形,用于限定弹出层所在的区域。 |
| collisionPadding | Padding | - | 与碰撞边界边缘保持的额外间距。 |
| disableAnchorTracking | boolean | - | 是否禁用弹窗对其定位锚点的布局偏移追踪。 |
| positionMethod | "absolute" | "fixed" | - | 确定使用哪个 CSS |
| side | "top" | "bottom" | "left" | "right" | "inline-end" | "inline-start" | - | 弹出层对齐锚元素的哪一侧。可能会自动更改以避免碰撞。 |
| sideOffset | number | OffsetFunction | - | 锚点与弹出层之间的距离(像素)。也接受一个函数,该函数返回距离以读取锚点和定位器元素的尺寸,以及其侧边和对齐方式。 该函数接受一个
|
| sticky | boolean | - | 锚元素滚动出视口后,是否将弹出层保持在视口内。 |
| className | string | - | 通过 |
| render | ReactNode | - | 替换触发元素,同时保留 Select 行为。 |
| size | "xs" | "sm" | "base" | "lg" | "base" | 选择触发器的大小。与 Input 组件大小匹配。 |
| label | ReactNode | - | 选择框的标签内容。提供后,将启用 Field 包装器,并在选择框上方显示可见标签。如需在无可见标签时实现无障碍,请改用 |
| hideLabel | boolean | - | - |
| placeholder | string | - | 未选择任何值时所显示的占位文本。 |
| loading | boolean | - | 当 |
| disabled | boolean | - | 选择器是否被禁用。 |
| required | boolean | - | 选择器是否为必填项。当为 |
| labelTooltip | ReactNode | - | 通过信息图标显示在标签旁边的 Tooltip 内容。 |
| value | T | - | 当前选中的值(受控模式)。 |
| children | ReactNode | - |
|
| description | ReactNode | - | 显示在选择框下方的帮助文本。 |
| error | string | object | - | 错误消息字符串或带有 |
| onValueChange | (value: T) => void | - | 选择变化时的回调 |
| defaultValue | T | - | 非受控模式的初始值。 |
| renderValue | (value: T) => ReactNode | - | 一个函数,返回用于在触发器中格式化选中值的 |
| items | Record<string, string> | Array<{ label: ReactNode; value: T }> | - | 弹窗中渲染的项目数据结构。接受纯对象映射( |
| isItemEqualToValue | (item: T, value: T) => boolean | - | 用于比较项目的自定义相等函数。当值为对象时需要,因为对象标识( |
Select.Option
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Select.Group
使用可访问的 role="group" 将相关选项分组。
可配合 Select.GroupLabel 提供可见标题。
Select.GroupLabel
Select.Group 的可见标题。
自动与其父组关联,保证无障碍。
Select.Separator
选项组之间的视觉分隔线。渲染为 role="separator"。