Skip to content
Drivn
5 min read

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.

A select is the right control whenever a user picks one value from a short, fixed list — a framework, a status, a country, a sort order. The native <select> element does that job but cannot be styled to match a design, so the Select component in Drivn replaces it with a styled trigger and a dropdown menu built from native buttons. The whole component lives in @/components/ui/select.tsx and imports nothing past react, the ChevronDown icon from lucide-react, and the local cn() utility — no Radix, no popper library.

Every example on this page imports the component from @/components/ui/select — the path the Drivn CLI installs it under — and uses the dot-notation API: Select as the root, then Select.Trigger, Select.Menu, and Select.Option. Each snippet is TypeScript because Drivn ships TypeScript-only. The one rule that runs through all of them: the Select is a controlled component, so you hold the chosen value in state and pass value plus onChange to the root.

The examples below cover a basic single-value picker, controlled state with useState, a custom trigger label, a Select used as a settings-form field, and the styling that marks the active option. The full props table lives on the Select docs page, and the shadcn/ui comparison sits 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.

1import { useState } from "react"
2import { Select } from "@/components/ui/select"
3
4const options = [
5 { label: "React", value: "react" },
6 { label: "Vue", value: "vue" },
7 { label: "Svelte", value: "svelte" },
8]
9
10export 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

Because the Select is controlled, the chosen value lives in your component state — which means you can use it anywhere else in the render, not just inside the dropdown. The example below holds the value in useState and shows it back to the user under the field. There is no internal state to query and no ref to read; the value you passed in is the single source of truth.

This matters when the selection drives the rest of the UI: a sort control that reorders a list, a filter that narrows a table, a theme picker that swaps tokens. The onChange callback fires with the new value the instant an option is clicked, so any derived rendering updates in the same pass. Set the initial state to a real value rather than an empty string when you want a default pre-selected — the trigger will render that option's label immediately.

1import { useState } from "react"
2import { Select } from "@/components/ui/select"
3
4const sorts = [
5 { label: "Newest first", value: "newest" },
6 { label: "Oldest first", value: "oldest" },
7 { label: "A to Z", value: "az" },
8]
9
10export 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.

1import { useState } from "react"
2import { Select } from "@/components/ui/select"
3
4const plans = [
5 { label: "Free", value: "free" },
6 { label: "Pro", value: "pro" },
7 { label: "Team", value: "team" },
8]
9
10export 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.

1import { useState } from "react"
2import { Select } from "@/components/ui/select"
3import { Label } from "@/components/ui/label"
4
5const zones = [
6 { label: "UTC", value: "utc" },
7 { label: "New York", value: "et" },
8 { label: "London", value: "gmt" },
9]
10
11export 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
2function 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}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

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.