Skip to content

Pickers and overlays

Select

A button that opens a list and keeps one chosen value.

When to use it: Use it for one form choice from a known list.

On this page

Example

Select.tsxtsx
import {
  Label,
  Select,
  SelectOptGroup,
  SelectOption,
  SelectPopover,
  SelectTrigger,
  SelectValue,
} from "@comp0/react";
import { ChevronDownIcon } from "@heroicons/react/16/solid";

export function Example() {
  return (
    <Select as="div" className="flex max-w-xs flex-col gap-1.5" defaultValue="medium" name="size">
      <Label className="text-base font-medium text-zinc-900 sm:text-sm dark:text-zinc-100">
        Size
      </Label>
      <SelectTrigger className="flex w-full items-center justify-between gap-2 rounded border border-zinc-950/10 bg-white px-3 py-2.5 text-left text-base text-zinc-950 outline-teal-600 focus-visible:outline-2 sm:py-2 sm:text-sm dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-50 dark:outline-teal-400">
        <SelectValue />
        <ChevronDownIcon className="size-4 shrink-0 text-zinc-400" aria-hidden="true" />
      </SelectTrigger>
      <SelectPopover
        placement="bottom"
        offset={4}
        className="max-h-60 w-[anchor-size(width)] max-w-[anchor-size(width)] overflow-y-auto rounded border-0 bg-white p-1 opacity-100 shadow-lg ring-1 ring-zinc-950/10 transition-[opacity,translate] duration-150 ease-out starting:-translate-y-1 starting:opacity-0 motion-reduce:transition-none dark:bg-zinc-900 dark:shadow-none dark:ring-white/10"
      >
        <SelectOptGroup label="Standard sizes">
          <div
            aria-hidden="true"
            className="px-3 pb-1 pt-2 text-xs font-medium text-zinc-500 dark:text-zinc-400"
          >
            Standard sizes
          </div>
          <SelectOption
            className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 hover:bg-zinc-100 data-active:bg-teal-100 data-active:text-zinc-950 data-selected:bg-teal-100 focus-visible:bg-teal-200 focus-visible:outline-2 focus-visible:outline-teal-600 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800 dark:data-active:bg-teal-950 dark:data-active:text-zinc-50 dark:data-selected:bg-teal-950 dark:focus-visible:bg-teal-800 dark:focus-visible:outline-teal-400"
            value="small"
          >
            Small
          </SelectOption>
          <SelectOption
            className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 hover:bg-zinc-100 data-active:bg-teal-100 data-active:text-zinc-950 data-selected:bg-teal-100 focus-visible:bg-teal-200 focus-visible:outline-2 focus-visible:outline-teal-600 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800 dark:data-active:bg-teal-950 dark:data-active:text-zinc-50 dark:data-selected:bg-teal-950 dark:focus-visible:bg-teal-800 dark:focus-visible:outline-teal-400"
            value="medium"
          >
            Medium
          </SelectOption>
        </SelectOptGroup>
        <SelectOptGroup label="Extended sizes">
          <div
            aria-hidden="true"
            className="px-3 pb-1 pt-2 text-xs font-medium text-zinc-500 dark:text-zinc-400"
          >
            Extended sizes
          </div>
          <SelectOption
            className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 hover:bg-zinc-100 data-active:bg-teal-100 data-active:text-zinc-950 data-selected:bg-teal-100 focus-visible:bg-teal-200 focus-visible:outline-2 focus-visible:outline-teal-600 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800 dark:data-active:bg-teal-950 dark:data-active:text-zinc-50 dark:data-selected:bg-teal-950 dark:focus-visible:bg-teal-800 dark:focus-visible:outline-teal-400"
            value="large"
          >
            Large
          </SelectOption>
        </SelectOptGroup>
      </SelectPopover>
    </Select>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. Select

    Selected value, open state, and optional visually hidden native select provider. Does not add a DOM element.

  2. Label

    Visible name connected to the trigger. Owns a DOM element.

  3. SelectTrigger

    Button that opens choices. Owns a DOM element.

  4. SelectValue

    Selected option text. Owns a DOM element.

  5. SelectPopover

    The listbox surface. Owns a DOM element.

  6. SelectOptGroupOptional

    Optional native-style group of related options. Owns a DOM element.

  7. SelectOption

    Selectable option. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start Select with a value or defaultValue.

  2. 2

    Add the supporting parts

    Put SelectValue inside SelectTrigger, then add SelectPopover beside it; wrap related options in a labelled SelectOptGroup.

  3. 3

    Make the behavior clear

    Select owns its value, field, form serialization, and open state; control the list with open, defaultOpen, and onToggle.

    Exampletsx
    <Select name="size" defaultValue="small"><Label>Size</Label><SelectTrigger><SelectValue /></SelectTrigger><SelectPopover><SelectOption value="small">Small</SelectOption></SelectPopover></Select>

Keyboard

Opens or chooses.
Space
Opens or chooses.
Moves through options.
Esc
Closes the list.

Forms and accessibility

When name or required is set, Select renders a visually hidden native select proxy for submission and validation; it is not an input type=hidden.

Accessibility checklist

  • Use a visible Label so the trigger and its listbox share a clear name; without one, name both parts explicitly.
  • Give every SelectOptGroup a native label so grouped options have a name.
  • Make the chosen value readable in SelectValue.
  • Use native required feedback or FieldError to explain a missing choice.

API reference

Importtsx
import { Label, Select, SelectOptGroup, SelectPopover, SelectOption, SelectTrigger, SelectValue } from "@comp0/react";

Select

Context only

Selected value, open state, and optional visually hidden native select provider.

PropTypeDescription
valuestringControlled choice.
defaultValuestringInitial choice.
onChange(value: string) => voidReceives the next choice.
open / defaultOpenbooleanControlled or initial open state of the listbox.
onToggle(open: boolean) => voidReceives the next open state.
namestringSubmission name for the hidden native select.
formstringAssociates the native select with a form by id.
disabled / invalid / requiredbooleanField-wide states.

Label

DOM element

Visible name connected to the trigger.

SelectTrigger

DOM element

Button that opens choices.

PropTypeDescription
disabledbooleanDisables opening.
aria-labelstringNames the trigger when there is no visible Label.

SelectValue

DOM element

Selected option text.

PropTypeDescription
placeholderReactNodeShown while nothing is selected.
valueReactNodeOverrides the displayed text for the selected option.

SelectPopover

DOM element

The listbox surface.

PropTypeDescription
aria-labelstringNames the listbox when there is no visible Label.
placementPopoverPlacementTrigger side to open on, such as "bottom"; flips when there is no room.
offsetnumberPixel gap between the trigger and the listbox.

SelectOptGroup

OptionalDOM element

Optional native-style group of related options.

PropTypeDescription
labelstringNames the group before its options.

SelectOption

DOM element

Selectable option.

PropTypeDescription
valuestringThis option’s value.
disabledbooleanDisables the option.
textValuestringOverrides the text crawled from children when markup makes it ambiguous.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

[data-open]

on SelectTrigger, SelectPopover

Choices are open.
:popover-open

on SelectPopover

Native pseudo-class equivalent.
[data-placeholder]

on SelectValue

No value is selected.
[data-value]

on SelectValue

A value is selected.

Keep exploring