Select

Selects let users pick one option from a collapsible list. They are built on Radix Select with Akanaka styling: focus rings, borders, and a portaled list aligned to the trigger.

Use a select when there are more than about four or five options and a radio group would take too much space. Pair SelectTrigger with Label (htmlFor / id) so the control has an accessible name.

Interactive preview

Configure the Select and see it update live. Turn on **Start with list open** to preview the open listbox; the control remounts the component so `defaultOpen` applies.

Options

Selected: (none)

Props

Select and parts

PropTypeDefaultDescription
Select (root)Wraps Radix Root. Pass `value` / `onValueChange` for controlled mode, `defaultValue` for uncontrolled. Supports `disabled`, `required`, `name` (native field when inside `<form>`), `open` / `onOpenChange`. Optional `placeholder` is forwarded to the default `SelectValue` inside `SelectTrigger`.
SelectTriggerbutton propsOpens the listbox. Default children: `SelectValue` + chevron. Match width with `className` (e.g. `w-full`). Forwards ref for focus and React Hook Form patterns.
SelectContentPopover panel and viewport; portaled to `document.body`. Uses `position="popper"` by default for alignment with the trigger.
SelectItemRadix Select.ItemOne option in the listbox. See **SelectItem** props below for the common API surface.
SelectValue{ placeholder?: ReactNode }Optional; default trigger already includes `SelectValue` with placeholder from `Select` root. Use when customizing trigger content.

SelectItem

PropTypeDefaultDescription
valuestringSubmitted value and internal selection key. Must not be empty (Radix constraint).
disabledbooleanfalseExcludes the item from keyboard selection and pointer picks.
textValuestringOptional plain-text label for typeahead when children are not simple strings.
childrenReactNodeVisible row label (and optional icons). Keep text concise.
classNamestringForwarded to the item row; prefer tokens over ad hoc colors.

Installation

Install packages
pnpm add @akanaka-design/components @akanaka-design/tokens
Styles (app root)
import "@akanaka-design/tokens/css";
import "@akanaka-design/components/styles.css";

Usage

Basic usage
import {
  Select,
  SelectTrigger,
  SelectContent,
  SelectItem,
} from "@akanaka-design/components";

export function Example() {
  return (
    <Select placeholder="Select a country">
      <SelectTrigger className="w-full max-w-xs" />
      <SelectContent>
        <SelectItem value="uk">United Kingdom</SelectItem>
        <SelectItem value="us">United States</SelectItem>
        <SelectItem value="ca">Canada</SelectItem>
      </SelectContent>
    </Select>
  );
}

With label

With label
import { Label, Select, SelectTrigger, SelectContent, SelectItem } from "@akanaka-design/components";

<Label htmlFor="country">Country</Label>
<Select placeholder="Select a country">
  <SelectTrigger id="country" className="w-full max-w-xs" />
  <SelectContent>
    <SelectItem value="uk">United Kingdom</SelectItem>
    <SelectItem value="us">United States</SelectItem>
  </SelectContent>
</Select>

With error

With error
<Select placeholder="Select a country" aria-invalid={!!error}>
  <SelectTrigger className="w-full max-w-xs" aria-invalid={!!error} />
  <SelectContent>...</SelectContent>
</Select>
{error ? <p className="text-body-sm text-error">{error}</p> : null}

Disabled options

Individual options can be disabled:

Disabled options
<Select placeholder="Select status">
  <SelectTrigger className="w-full max-w-xs" />
  <SelectContent>
    <SelectItem value="draft">Draft</SelectItem>
    <SelectItem value="published">Published</SelectItem>
    <SelectItem value="archived" disabled>Archived</SelectItem>
  </SelectContent>
</Select>

Uncontrolled

Uncontrolled
/* Omit value/onValueChange; optionally set defaultValue for the initial selection */
<Select placeholder="Select an option" defaultValue="b">
  <SelectTrigger className="w-full max-w-xs" />
  <SelectContent>
    <SelectItem value="a">Option A</SelectItem>
    <SelectItem value="b">Option B</SelectItem>
  </SelectContent>
</Select>

Controlled

Controlled
const [value, setValue] = useState<string | undefined>();

<Select
  placeholder="Select an option"
  value={value}
  onValueChange={setValue}
>
  <SelectTrigger className="w-full max-w-xs" />
  <SelectContent>
    <SelectItem value="a">Option A</SelectItem>
    <SelectItem value="b">Option B</SelectItem>
  </SelectContent>
</Select>

Open state (closed vs open)

Use defaultOpen for an initially expanded list (for example in local docs previews). For fully controlled visibility, use open and onOpenChange on the root Select.

Open state (controlled popover)
/* Control the popover for tutorials, tests, or parent-driven UI */
const [open, setOpen] = useState(false);

<Select open={open} onOpenChange={setOpen} placeholder="Choose…">
  <SelectTrigger className="w-full max-w-xs" />
  <SelectContent>
    <SelectItem value="x">Option</SelectItem>
  </SelectContent>
</Select>

React Hook Form

Use Controller (or useController) and wire value, onValueChange, and disabled from the field:

React Hook Form
import { Controller, useForm } from "react-hook-form";
import {
  Select,
  SelectTrigger,
  SelectContent,
  SelectItem,
} from "@akanaka-design/components";

type FormValues = { department: string };

export function DepartmentField() {
  const { control } = useForm<FormValues>({ defaultValues: { department: "" } });

  return (
    <Controller
      name="department"
      control={control}
      render={({ field }) => (
        <Select
          placeholder="Select department..."
          value={field.value || undefined}
          onValueChange={field.onChange}
          disabled={field.disabled}
        >
          <SelectTrigger className="w-full max-w-xs" />
          <SelectContent>
            <SelectItem value="eng">Engineering</SelectItem>
            <SelectItem value="mkt">Marketing</SelectItem>
          </SelectContent>
        </Select>
      )}
    />
  );
}

Native form submission

Native form submission
/* Radix injects a visually hidden native <select> when the trigger is inside a <form>
   (or when you pass a `form` prop matching a form id). Use this for classic form POST. */
<form action="/api/profile" method="post">
  <Select name="country" defaultValue="uk" placeholder="Country">
    <SelectTrigger />
    <SelectContent>
      <SelectItem value="uk">United Kingdom</SelectItem>
      <SelectItem value="us">United States</SelectItem>
    </SelectContent>
  </Select>
  <button type="submit">Save</button>
</form>

Accessibility

  • Keyboard: Space or Enter opens the list; Arrow up/down moves highlight; Enter selects; Escape closes. Typeahead jumps to matching items when textValue or string children are set on SelectItem.
  • Focus: Focus returns to the trigger after select or dismiss. The trigger is a real <button> with an aria-expanded state.
  • Errors: Forward aria-invalid to Select and SelectTrigger when validation fails, and surface the message next to the field (see With error above).
  • Disabled: Set disabled on the root to disable the whole control; use SelectItem disabled for individual rows.

Guidelines

When to use

  • Choosing from 5+ options
  • Space is limited
  • Options are familiar and don't need explanation

When not to use

  • 2–4 options — use radio buttons instead
  • Options need descriptions — use a listbox or cards
  • Multi-select — use checkboxes or a multi-select component

Content

  • Use clear, concise option labels
  • Order options logically (alphabetical, by frequency, or by importance)
  • Include a placeholder to prompt selection