Select
A themeable select built on the native popover API and a listbox panel — supports option groups, keyboard navigation and validation
Installation
Usage
Basic Usage
Basic select component with placeholder and options. The panel is plain DOM, so it follows theme colors — unlike a native <select> dropdown.
import { Select } from "@/ui";
const options = [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
{ label: "Svelte", value: "svelte" },
];
export default function SelectBasicDemo() {
return (
<Select
options={options}
placeholder="Select a framework..."
defaultValue="react"
/>
);
}
With Option Groups
Select component with grouped options for better organization.
import { useState } from "react";import { Select } from "@/ui";const options = [ { group: "Frontend", options: [ { label: "React", value: "react" }, { label: "Vue", value: "vue" }, { label: "Angular", value: "angular" }, ], }, { group: "Backend", options: [ { label: "Node.js", value: "node" }, { label: "Python", value: "python" }, { label: "Go", value: "go" }, ], },];export default function SelectGroupedDemo() { const [selectedValue, setSelectedValue] = useState<string>(""); return ( <Select value={selectedValue} onValueChange={setSelectedValue} options={options} placeholder="Select a technology..." /> );}Invalid State
Select component with an error message rendered below via the Error component.
import { Error, Select } from "@/ui";
const options = [
{ label: "United States", value: "us" },
{ label: "United Kingdom", value: "uk" },
{ label: "Canada", value: "ca" },
{ label: "Australia", value: "au" },
];
export default function SelectInvalidDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-2">
<Select options={options} placeholder="Select your country..." />
<Error>Please select your country.</Error>
<Select options={options} placeholder="Valid selection" defaultValue="us" />
</div>
);
}
Custom
The trigger is a button showing the selected label; the panel is an anchored popover. Compose styling via classNames.
The panel is plain DOM — it follows theme colors, unlike a native select dropdown.
"use client";
import { useState } from "react";
import { Select } from "@/ui";
const options = [
{ label: "Red", value: "red" },
{ label: "Green", value: "green" },
{ label: "Blue", value: "blue" },
];
export default function Demo() {
const [, setValue] = useState("");
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<Select options={options} placeholder="Pick a color..." onValueChange={setValue} />
<p className="text-sm text-muted-foreground">
The panel is plain DOM — it follows theme colors, unlike a native select dropdown.
</p>
</div>
);
}
API Reference
High-level Components
Select
A select built on the native popover API and CSS anchor positioning: the trigger button shows the selected label, and the listbox panel follows theme colors. Keyboard: ArrowDown / ArrowUp open the panel and move the highlight (wrapping), Home / End jump to the first / last option, Enter / Space select the highlighted option, Escape closes without selecting, Tab closes.
| Prop | Type | Default | Description |
|---|---|---|---|
options | (SelectOption | SelectOptionGroup)[] | - | Array of options or option groups to display |
value | string | - | Controlled selected value |
defaultValue | string | "" | Default selected value for uncontrolled state |
onValueChange | (value: string) => void | - | Fired when an option is selected |
placeholder | string | - | Placeholder text shown when no option is selected |
required | boolean | - | Sets aria-required on the trigger |
name | string | - | Renders a hidden input with this name, so the value joins native form submission |
disabled | boolean | - | Disables the trigger |
className | ClassNameValue | - | Custom classes, applied to the trigger |
style | React.CSSProperties | - | Inline styles, applied to the trigger |
classNames | { panel?; label?; option? } — each ClassNameValue | - | Custom classes for the panel, group labels and options |
styles | { panel? } — each React.CSSProperties | - | Inline styles for the panel |
...props | React.ComponentProps<"button"> | - | Remaining native button props spread on the trigger, such as id and aria-describedby |
Behavior notes:
- Selecting an option closes the panel, updates the value and returns focus to the trigger.
- The panel escapes ancestor
overflowclipping via the top layer, flips viapositionTryFallbacksnear viewport edges, and matches the trigger width withmin-width: anchor-size(width). - When the panel opens, the highlight starts on the currently selected option.
SelectOption
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Display text for the option |
value | string | - | Value associated with the option |
SelectOptionGroup
| Prop | Type | Default | Description |
|---|---|---|---|
group | string | - | Group label text |
options | SelectOption[] | - | Array of options in the group |