# Select

> A button that opens a list to pick one option from, with groups, a check on the picked option, Input's sizes, and form support.

- Name: `select`
- Status: beta
- Categories: forms
- File: the install writes `select.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`, `class-variance-authority`. The install adds them.
- Other registry items: `@txbrendan/glide-highlight`. The install adds them.
- Gallery: https://txbrendan.dev/components/select
- Registry item: https://txbrendan.dev/r/select.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@txbrendan": "https://txbrendan.dev/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@txbrendan` in a public list that txbrendan/ui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @txbrendan/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @txbrendan/select
   ```

## Use it for

- Picking one option from a short list, about 5 to 15 options, like a country, a role, or a sort order.
- One highlight glides from option to option as the pointer moves, and jumps when the arrow keys move it. Reduced motion makes it jump.
- Pass items to Select, as \[{ value, label }\]. SelectValue then shows the picked label, even in server HTML. Without items it shows the raw value.
- Set defaultValue to start with an option picked. Leave it out and give SelectValue a placeholder to start empty.
- Name it with a SelectLabel inside Select, before SelectTrigger. It needs no id.
- When the label must sit outside Select, give Select an id and point a Label's htmlFor at it. With no visible label, put aria-label on SelectTrigger.
- Set name on Select to send the value with a form. Select keeps a hidden input in sync.
- Put width classes on SelectTrigger, like className='w-48'. It fills its container by default, like Input.
- The list opens at the end of the page body. In a box with its own theme, like a dark preview on a light page, pass that box as container on SelectPopup so the list keeps its colors. A container that clips its overflow can clip the list.

## Avoid it for

- Long lists people would rather search, like every city. Use a combobox.
- Two to four options people should see at once. Use a radio group or tabs.
- Actions, like Edit or Delete. Use a menu.
- Putting className on Select. It lays out as display: contents, so style SelectTrigger or SelectPopup instead.

## Props

- `Select`: `SelectProps`. The root. Holds the value and renders the hidden form input. It adds no box to the layout.
- `SelectLabel`: `SelectLabelProps`. Part. A label inside Select that names the trigger, with no id to set. A click focuses the trigger. It dims when Select is disabled.
- `SelectTrigger`: `SelectTriggerProps`. Part. The button that opens the list, with a chevron. It looks like Input and takes size.
- `SelectValue`: `SelectValueProps`. Part. Shows the picked option's label inside the trigger, or the placeholder.
- `SelectPopup`: `SelectPopupProps`. Part. The list in a portal. It opens below the trigger and flips above it when there is no room.
- `SelectItem`: `SelectItemProps`. Part. One option. Needs a value. Shows a check when it is the picked one.
- `SelectGroup`: `SelectGroupProps`. Part. Groups options under a SelectGroupLabel.
- `SelectGroupLabel`: `SelectGroupLabelProps`. Part. The group's heading. It names the group for screen readers.
- `SelectSeparator`: `SelectSeparatorProps`. Part. A thin line between options or groups. It is a top border, so a border color class recolors it.
- `modal`: `boolean`. Default `false`. On Select. False lets a press outside close the list and still reach the page, so one press moves from one open list to the next. True blocks the page and its scroll while the list is open.
- `items`: `{ value: string; label: ReactNode }[] | Record<string, ReactNode>`. On Select. Maps values to labels, so SelectValue shows the label.
- `defaultValue`: `string`. On Select. The option picked at the start, when you do not control it.
- `value`: `string`. Required on SelectItem. On Select, it controls the picked option. Use it with onValueChange.
- `onValueChange`: `(value, details) => void`. On Select. Called with the new value when someone picks an option.
- `name`: `string`. On Select. Form field name. The hidden input sends the value with the form.
- `id`: `string`. On Select. Becomes the trigger's id, so a Label's htmlFor can name it. SelectLabel does not need it.
- `placeholder`: `ReactNode`. On SelectValue. Shown in muted text while nothing is picked.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. On SelectTrigger. The same heights as Input. Density scales them.
- `disabled`: `boolean`. Default `false`. On Select, it blocks opening, removes the trigger from the Tab order, and dims SelectLabel. On SelectItem, it blocks picking that option. The arrow keys still reach it.
- `required`: `boolean`. Default `false`. On Select. The form will not submit until an option is picked.
- `side`: `"top" | "bottom" | "left" | "right" | "inline-start" | "inline-end"`. Default `"bottom"`. On SelectPopup. Where the list opens. It flips when there is no room.
- `align`: `"start" | "center" | "end"`. Default `"start"`. On SelectPopup. Lines the list up with the trigger's start, center, or end.
- `container`: `HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null`. On SelectPopup. The element the list opens in. By default, the end of the page body, so no parent can clip it. Pass an element, or a ref itself (not ref.current), to open it inside, for example to keep a parent's theme. Only client code has elements and refs. A null container shows no list.
- `aria-invalid`: `boolean`. On SelectTrigger. Shows the danger border. Set it when validation fails, and link the message with aria-describedby.
- `effect`: `"rise" | "grow"`. Default `"rise"`. On SelectPopup. How the list opens and closes. rise fades in and moves out from the trigger. grow fades in and grows from the trigger, and closes the same way. Left out, it follows an effect-\<name> class on \<body>, where popups open, and rises with none.
- `className`: `string`. Extra classes on any part except Select. Later classes win, so className='w-48' on SelectTrigger sets its width.

## Accessibility

- The trigger is a button with role="combobox" and aria-expanded. The list has role="listbox", and each option has role="option" with aria-selected.
- Enter, Space, or an arrow key opens the list. The arrow keys move through options, Enter picks, and Escape closes.
- Typing jumps to a matching option in the open list. On the closed trigger, typing picks the match right away, as a native select does.
- A disabled option has aria-disabled. The arrow keys still stop on it, but Enter does not pick it.
- Focus goes back to the trigger when the list closes.
- SelectLabel renders a native label whose for= matches the trigger's id, so it names the trigger in server HTML too. After hydration, the trigger's aria-labelledby points at it as well.
- A click on SelectLabel focuses the trigger and leaves the list closed, as a native select's label does. While Select is disabled, the click does nothing.
- A Label whose htmlFor matches the Select's id also names the trigger, in server HTML too, because the trigger is a native button. Clicking that Label opens the list, the same as a click on the trigger.
- A group is named by its SelectGroupLabel. The separator has role="presentation", so screen readers skip it.
- The trigger border uses --input, which the theme tests hold to 3:1 on the page and on cards. Text in the list uses popover-foreground on popover, held to 4.5:1.
- The highlighted option uses accent-foreground on accent, and group labels use muted-foreground on popover. The theme tests hold both to 4.5:1.
- The list fades in and moves --motion-distance as it opens. It closes with a quicker fade and a slight shrink toward the trigger. Reduced motion keeps the fades and drops the move and the shrink.

## Examples

### With a label

```tsx
const roles = [
  { value: "viewer", label: "Viewer" },
  { value: "editor", label: "Editor" },
  { value: "admin", label: "Admin" },
];

<div className="grid max-w-60 gap-1.5">
  <Select name="role" items={roles} defaultValue="editor">
    <SelectLabel>Role</SelectLabel>
    <SelectTrigger>
      <SelectValue />
    </SelectTrigger>
    <SelectPopup>
      {roles.map((role) => (
        <SelectItem key={role.value} value={role.value}>
          {role.label}
        </SelectItem>
      ))}
    </SelectPopup>
  </Select>
</div>
```

### With a Label outside Select (needs @txbrendan/label)

```tsx
import { Label } from "@/components/ui/label";

const roles = { viewer: "Viewer", editor: "Editor", admin: "Admin" };

<div className="grid max-w-60 gap-1.5">
  <Label htmlFor="role">Role</Label>
  <Select id="role" name="role" items={roles} defaultValue="editor">
    <SelectTrigger>
      <SelectValue />
    </SelectTrigger>
    <SelectPopup>
      <SelectItem value="viewer">Viewer</SelectItem>
      <SelectItem value="editor">Editor</SelectItem>
      <SelectItem value="admin">Admin</SelectItem>
    </SelectPopup>
  </Select>
</div>
```

### Empty, with a placeholder

```tsx
const sizes = { s: "Small", m: "Medium", l: "Large" };

<Select items={sizes} name="size" required>
  <SelectTrigger aria-label="Shirt size" className="w-40">
    <SelectValue placeholder="Pick a size" />
  </SelectTrigger>
  <SelectPopup>
    <SelectItem value="s">Small</SelectItem>
    <SelectItem value="m">Medium</SelectItem>
    <SelectItem value="l">Large</SelectItem>
  </SelectPopup>
</Select>
```

### Groups and a separator

```tsx
const zones = {
  "America/New_York": "New York",
  "America/Chicago": "Chicago",
  "Europe/London": "London",
  "Europe/Paris": "Paris",
};

<Select items={zones} defaultValue="Europe/London">
  <SelectTrigger aria-label="Time zone">
    <SelectValue />
  </SelectTrigger>
  <SelectPopup>
    <SelectGroup>
      <SelectGroupLabel>Americas</SelectGroupLabel>
      <SelectItem value="America/New_York">New York</SelectItem>
      <SelectItem value="America/Chicago">Chicago</SelectItem>
    </SelectGroup>
    <SelectSeparator />
    <SelectGroup>
      <SelectGroupLabel>Europe</SelectGroupLabel>
      <SelectItem value="Europe/London">London</SelectItem>
      <SelectItem value="Europe/Paris">Paris</SelectItem>
    </SelectGroup>
  </SelectPopup>
</Select>
```

### Controlled

```tsx
"use client";
import { useState } from "react";

const sorts = { new: "Newest first", old: "Oldest first", top: "Most votes" };

function SortPicker() {
  const [sort, setSort] = useState("new");
  return (
    <Select items={sorts} value={sort} onValueChange={(value) => value && setSort(value)}>
      <SelectTrigger size="sm" aria-label="Sort by" className="w-44">
        <SelectValue />
      </SelectTrigger>
      <SelectPopup align="end">
        <SelectItem value="new">Newest first</SelectItem>
        <SelectItem value="old">Oldest first</SelectItem>
        <SelectItem value="top">Most votes</SelectItem>
      </SelectPopup>
    </Select>
  );
}
```

### Grows from its trigger

```tsx
const sorts = [
  { value: "newest", label: "Newest first" },
  { value: "oldest", label: "Oldest first" },
];

<div className="grid max-w-60 gap-1.5">
  <Select items={sorts} defaultValue="newest">
    <SelectLabel>Sort</SelectLabel>
    <SelectTrigger>
      <SelectValue />
    </SelectTrigger>
    <SelectPopup effect="grow">
      {sorts.map((sort) => (
        <SelectItem key={sort.value} value={sort.value}>
          {sort.label}
        </SelectItem>
      ))}
    </SelectPopup>
  </Select>
</div>
```
