Select
Custom select/dropdown. Located at apps/web/components/ui/select.tsx.
Do not use native <select>. AGENTS.md rule.
Usage
<Select
value={position}
onChange={(v) => setPosition(v)}
options={[
{ value: "member", label: "Member", description: "Team member. Default." },
{ value: "manager", label: "Manager", description: "Leads a department." },
{ value: "ceo", label: "CEO", description: "Cross-dept observer. Singleton." },
]}
/>
Features
- Keyboard nav (arrow keys, enter, escape)
- Option descriptions (shown below label in muted text)
- Disabled options (
disabled: truein the option object) - Custom icon per option
- Empty state when
options=[]
Props
interface SelectOption {
value: string;
label: string;
description?: string;
disabled?: boolean;
icon?: React.ReactNode;
}
interface SelectProps {
value: string;
onChange: (value: string) => void;
options: SelectOption[];
placeholder?: string;
disabled?: boolean;
className?: string;
}
Example: fetched options
const [models, setModels] = useState<SelectOption[]>([]);
useEffect(() => {
api.fetch("/providers/models").then((r) => {
setModels(
r.models.map((m) => ({
value: m.model,
label: m.model,
description: m.provider,
}))
);
});
}, []);
<Select
value={selectedModel}
onChange={setSelectedModel}
options={models}
disabled={models.length === 0}
/>
Why not native
Native <select>:
- Looks different on every browser (Chrome/Safari/Firefox)
- Can't match our tokens (can't style option dropdowns)
- No support for descriptions per option
- No icon support
- Limited keyboard nav customization
Our <Select> gives all of that at the cost of ~200 lines of code. One-time cost, consistent everywhere.
Don't
- Don't put
<Select>inside a form and submit via native form — the component doesn't bubble a synthetic change event. Use React state and submit via onClick handler. - Don't use for > 20 options without a search — extend with a typeahead if needed (doesn't exist yet)