React Select Component Examples
Copy-paste React Select examples: a basic picker, controlled state, a custom trigger label, a settings form field, and the selected-option style. Zero Radix.
Pick one value out of a short list of known choices and a select is the control you want: a status field, a sort order, a timezone, a plan tier. The browser's own <select> handles that interaction correctly but resists styling, which is why the Select component in Drivn rebuilds the affordance from a styled <button> trigger and a menu of native buttons. The file is @/components/ui/select.tsx, and its imports stop at react, ChevronDown from lucide-react, and the local cn() helper — no Radix, no positioning library.
Everything below imports from @/components/ui/select, the path the Drivn CLI writes the component to, and uses the dot-notation surface: Select wraps Select.Trigger, Select.Menu, and Select.Option. The snippets are TypeScript throughout, since Drivn ships .tsx source and nothing else. One rule carries across all five: the root is controlled, so the current value lives in your state, travels in through value, and comes back out through onChange.
The examples cover a plain single-value picker, controlled state driving the rest of a view, a composed trigger label, a Select sitting in a settings form, and how the active row earns its styling. Props are documented on the Select docs page; the Radix-backed alternative is covered on Drivn vs shadcn Select.
A basic single-value picker
The simplest Select: a root, a trigger, and a menu of options. Pass value and onChange to the Select root so it can track the current choice and report changes. Each Select.Option carries a value string; clicking one runs the root's onChange with that value and closes the menu. The Select.Trigger takes a placeholder that shows while nothing is picked, and its children render the label of the current selection.
Drivn does not auto-resolve the label from the value — the component has no options registry — so you look the label up yourself, as the options.find(...) call below does. That keeps the component a thin wrapper with no hidden abstraction. This shape is right for a status field, a category picker, or any control where the choices are known ahead of time. For a list the user needs to search through, reach for the Combobox instead.
1 import { useState } from "react" 2 import { Select } from "@/components/ui/select" 3 4 const options = [ 5 { label: "React", value: "react" }, 6 { label: "Vue", value: "vue" }, 7 { label: "Svelte", value: "svelte" }, 8 ] 9 10 export default function FrameworkPicker() { 11 const [value, setValue] = useState("") 12 13 return ( 14 <Select value={value} onChange={setValue}> 15 <Select.Trigger placeholder="Pick a framework"> 16 {options.find(o => o.value === value)?.label} 17 </Select.Trigger> 18 <Select.Menu> 19 {options.map(o => ( 20 <Select.Option key={o.value} value={o.value}> 21 {o.label} 22 </Select.Option> 23 ))} 24 </Select.Menu> 25 </Select> 26 ) 27 }
Reading the controlled value
Controlled means the value is never hidden inside the component, so it stays available anywhere else in the same render — not only to the dropdown that set it. The snippet below keeps the choice in useState and echoes it back beneath the field, but the same state could just as easily reorder a list, narrow a table, or move a chart range. There is no internal value to query and no ref to read out; what you passed in is what is selected.
onChange fires the moment an option is clicked, and the component closes the menu inside that same callback, so derived rendering lands in one pass rather than two. Two details are worth knowing. Initialise the state with a real option value instead of an empty string when a field should start pre-selected — the trigger renders whatever you hand it as children, so the default shows immediately and the placeholder never appears. And because the root registers a mousedown listener on the document to close on an outside click, clicking away leaves the current value untouched; dismissing the menu is not a way to clear the field. When a field does need clearing, render a reset control beside the Select and set the state back to '' yourself — the Button component has an outline variant that suits that job.
1 import { useState } from "react" 2 import { Select } from "@/components/ui/select" 3 4 const sorts = [ 5 { label: "Newest first", value: "newest" }, 6 { label: "Oldest first", value: "oldest" }, 7 { label: "A to Z", value: "az" }, 8 ] 9 10 export default function SortControl() { 11 const [sort, setSort] = useState("newest") 12 const current = sorts.find(s => s.value === sort) 13 14 return ( 15 <div className="space-y-2"> 16 <Select value={sort} onChange={setSort}> 17 <Select.Trigger placeholder="Sort by"> 18 {current?.label} 19 </Select.Trigger> 20 <Select.Menu> 21 {sorts.map(s => ( 22 <Select.Option key={s.value} value={s.value}> 23 {s.label} 24 </Select.Option> 25 ))} 26 </Select.Menu> 27 </Select> 28 <p className="text-sm text-muted-foreground"> 29 Sorting by: {sort} 30 </p> 31 </div> 32 ) 33 }
A custom trigger label
The Select.Trigger renders whatever you pass as its children, so the label is not locked to the option text. You can prefix it, format it, or compose it from other elements. The example below shows a richer trigger that pairs a static prefix with the selected label, so the field reads "Plan: Pro" instead of just "Pro".
When the trigger has no children — nothing picked yet — it falls back to the placeholder and applies the styles.placeholder class (text-muted-foreground) so the empty state reads as muted. Once a value is set, your children render at full contrast. Because the trigger is a native <button>, it also accepts any standard button attribute except onClick, which the component owns to toggle the menu — so disabled, aria-label, and id all flow straight through. Keep the children short; the trigger is a fixed 40-pixel-tall row and long text will truncate against the chevron.
1 import { useState } from "react" 2 import { Select } from "@/components/ui/select" 3 4 const plans = [ 5 { label: "Free", value: "free" }, 6 { label: "Pro", value: "pro" }, 7 { label: "Team", value: "team" }, 8 ] 9 10 export default function PlanField() { 11 const [plan, setPlan] = useState("pro") 12 const label = plans.find(p => p.value === plan)?.label 13 14 return ( 15 <Select value={plan} onChange={setPlan}> 16 <Select.Trigger placeholder="Choose a plan"> 17 {label && <span>Plan: {label}</span>} 18 </Select.Trigger> 19 <Select.Menu> 20 {plans.map(p => ( 21 <Select.Option key={p.value} value={p.value}> 22 {p.label} 23 </Select.Option> 24 ))} 25 </Select.Menu> 26 </Select> 27 ) 28 }
A Select inside a settings form
A Select earns its place in any form where one field is a fixed-choice value — a timezone, a role, a notification frequency. Treat it like any other controlled input: hold the value in state, render it inside a labelled row, and read it from state on submit. The Select does not render a hidden native input, so it contributes nothing to FormData — the value you collect comes from your own state.
The example below pairs the Select with a Label and stacks it the way a settings panel would. For a form managed by react-hook-form, register the field through a Controller and feed its field.value and field.onChange straight into the Select root. To group several Select fields with text inputs, the Input component shares the same border-input and rounded-[10px] styling, so the controls line up without extra work.
1 import { useState } from "react" 2 import { Select } from "@/components/ui/select" 3 import { Label } from "@/components/ui/label" 4 5 const zones = [ 6 { label: "UTC", value: "utc" }, 7 { label: "New York", value: "et" }, 8 { label: "London", value: "gmt" }, 9 ] 10 11 export default function TimezoneField() { 12 const [zone, setZone] = useState("utc") 13 14 return ( 15 <div className="space-y-1.5"> 16 <Label htmlFor="tz">Timezone</Label> 17 <Select value={zone} onChange={setZone}> 18 <Select.Trigger placeholder="Select a timezone"> 19 {zones.find(z => z.value === zone)?.label} 20 </Select.Trigger> 21 <Select.Menu> 22 {zones.map(z => ( 23 <Select.Option key={z.value} value={z.value}> 24 {z.label} 25 </Select.Option> 26 ))} 27 </Select.Menu> 28 </Select> 29 </div> 30 ) 31 }
How the active option is styled
The Select marks the currently selected option without you writing any conditional class. Inside the Option component, the registry compares each option's value against the root's context value, and when they match it merges styles.selected onto the row. That class is text-primary font-medium, so the chosen option reads in the brand color and a heavier weight while the rest stay default.
Every option is a native <button> carrying styles.option, which gives it a hover:bg-accent background and rounded-lg corners, so pointer feedback is built in. The menu container caps its height at max-h-[200px] and turns on overflow-y-auto, so a long option list scrolls inside the dropdown rather than running off the screen. Because the file is yours after install, restyling the active state is a one-line edit to styles.selected — swap in a background tint or a check icon. The verbatim Option source below shows exactly where the selected class is applied. See the Drivn vs shadcn Select page for how this compares to the Radix-backed recipe.
1 // Option — verbatim from the registry source 2 function Option({ 3 value: optValue, 4 children, 5 className, 6 ...props 7 }: { 8 value: string 9 children: React.ReactNode 10 className?: string 11 } & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'>) { 12 const { value, onSelect } = useSelect() 13 return ( 14 <button 15 type="button" 16 className={cn(styles.option, optValue === value && styles.selected, className)} 17 onClick={() => onSelect(optValue)} 18 {...props} 19 > 20 {children} 21 </button> 22 ) 23 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
It is a controlled component. You hold the chosen value in your own state and pass it to the Select root as the value prop, along with an onChange callback. The component has no internal value state to fall back on, so the value you supply is always the single source of truth for what is selected.
Initialise your state with the value of the option you want pre-selected instead of an empty string. The trigger renders the label of whatever value is currently set, so passing a real value on the first render makes that option appear selected immediately, with no placeholder shown.
No. The Select does not render a hidden native input, so it adds nothing to a form's FormData on submit. Read the chosen value from your component state instead. With react-hook-form, register the field through a Controller and connect field.value and field.onChange to the Select root.
The menu container caps its height with max-h-[200px] and sets overflow-y-auto, so a list longer than that scrolls inside the dropdown rather than overflowing the viewport. If the list is long enough that scanning becomes hard, switch to the Combobox component, which adds free-text filtering over the same options.