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" }}
    />
  );
}

Installation

Barrel

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

Granular

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

Usage

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" }}
    />
  );
}

Examples

Basic

A select with a visible label. When you provide the label prop, the select automatically renders inside a Field wrapper with the label displayed above it.

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" }}
    />
  );
}

Sizes

Use the size prop to match Input sizing (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>
  );
}

Without Visible Label

When a visible label isn’t needed (e.g., in compact UIs or when context is clear), use aria-label for accessibility.

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" }}
    />
  );
}

With Description and Error

Select integrates with the Field wrapper to show description text and validation errors.

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

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

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

Placeholder

Use the placeholder prop to show text when no value is selected. When using renderValue to customize the display of selected values, the placeholder is shown instead of calling renderValue when the value is null.

<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",
      }}
    />
  );
}

Label with Tooltip

Add a tooltip icon next to the label for additional context using 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",
      }}
    />
  );
}

Custom Rendering

Use renderValue to customize how the selected value appears in the trigger button. This is useful when working with complex object data structures instead of simple string values.

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>
  );
}

The renderValue function is only called when a value is selected. Use placeholder to define what to show when no value is selected.

Select compares value with items to find which one is selected. For object items, it will compare if the object is the same reference not by value by default. If you want to compare object items by value, you can use

isItemEqualToValue

prop.

<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>

Loading

A select component with loading state. The loading state is passed to the component via the loading prop.

Loading State

Loading From Server (simulated 2s delay)

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

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

Multiple Selection

Enable multiple selection with the multiple prop. The value becomes an array of selected items. Use placeholder for the empty state and renderValue to customize how selections are displayed.

<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>
  );
}

More Example

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 Options

Options can be disabled with the disabled prop. Disabled options are greyed out and cannot be selected.

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>
  );
}

Disabled Items (via items prop)

The items object-map prop accepts descriptor objects with disabled alongside plain string values.

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 },
      }}
    />
  );
}

Grouped Options

Use Select.Group, Select.GroupLabel, and Select.Separator to organize options under labeled headers with visual dividers.

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>
  );
}

Groups with Disabled Options

Combine groups, separators, and disabled options with info tooltips to clearly separate available and unavailable choices.

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>
  );
}

Long List (Scrolling Test)

A select component with many options to test popup scrolling behavior. The popup should scroll smoothly without bounce/overscroll issues.

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 Reference

Select

PropTypeDefaultDescription
classNamestring-Additional CSS classes merged via `cn()`.
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

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Select.Group

Groups related options together with an accessible role=“group”. Use with

Select.GroupLabel

to provide a visible heading.

Select.GroupLabel

A visible heading for a Select.Group. Automatically associated with its parent group for accessibility.

Select.Separator

A visual divider line between option groups. Renders with role=“separator”.