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.
Selected: (none)
Props
Select and parts
| Prop | Type | Default | Description |
|---|---|---|---|
| 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`. |
| SelectTrigger | button props | — | Opens the listbox. Default children: `SelectValue` + chevron. Match width with `className` (e.g. `w-full`). Forwards ref for focus and React Hook Form patterns. |
| SelectContent | — | — | Popover panel and viewport; portaled to `document.body`. Uses `position="popper"` by default for alignment with the trigger. |
| SelectItem | Radix Select.Item | — | One 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
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Submitted value and internal selection key. Must not be empty (Radix constraint). |
| disabled | boolean | false | Excludes the item from keyboard selection and pointer picks. |
| textValue | string | — | Optional plain-text label for typeahead when children are not simple strings. |
| children | ReactNode | — | Visible row label (and optional icons). Keep text concise. |
| className | string | — | Forwarded to the item row; prefer tokens over ad hoc colors. |
Installation
pnpm add @akanaka-design/components @akanaka-design/tokensimport "@akanaka-design/tokens/css";
import "@akanaka-design/components/styles.css";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
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
<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:
<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
/* 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
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.
/* 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:
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
/* 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
textValueor stringchildrenare set onSelectItem. - Focus: Focus returns to the trigger after select or dismiss. The trigger is a real
<button>with anaria-expandedstate. - Errors: Forward
aria-invalidtoSelectandSelectTriggerwhen validation fails, and surface the message next to the field (see With error above). - Disabled: Set
disabledon the root to disable the whole control; useSelectItem disabledfor 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