How to Add a Select to a React App
Step-by-step guide to adding a select dropdown to any React app with the Drivn CLI — a copy-paste, click-outside-aware component built on React state.
Reaching for a select means one field in your React app is a single choice from a short, fixed list — a framework, a status, a timezone, a sort order — and the native <select> element, which cannot be styled to match a design, is not going to cut it. You could hand-roll a dropdown with a button, a positioned menu, and an outside-click listener, but getting the open state, the click-away close, and the affordances right is exactly the kind of fiddly work most teams would rather solve once.
Drivn solves it by copy-paste: the CLI writes a select.tsx file into your repository. The Select is a compound built with Object.assign — a SelectRoot joined to Trigger, Menu, and Option — that you call through dot notation. The root holds open in React.useState, shares the selected value through a React.createContext, and closes on an outside click via a mousedown listener, so select.tsx carries a 'use client' directive. It imports nothing past React, the ChevronDown icon from lucide-react, and the cn utility.
This guide adds the Select to any React app — install the CLI, render the control inside a client component, lift the chosen value into controlled state, and restyle the trigger and menu from the one styles object you own. It works the same in Vite + React or Next.js. For the App Router walkthrough see the Next.js Select guide.
Prerequisites
Before installing Select, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your styles, TypeScript configured (the component ships as a .tsx file), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired; for a custom setup the installation page lists the minimal config. The Select is a small install — its only imports past React are the ChevronDown icon from lucide-react and the local cn utility for class merging, so lucide-react is the one package to have present. The one thing to keep in mind before you render it is that the Select is a controlled component: it keeps no internal copy of the value, so you hold the selection in your own state and feed it back through the value prop.
Step 1 — Install Drivn via the CLI
Run the CLI from your project root to add the Select source file. The command prompts once for your install directory (defaulting to src/components/ui/) and writes a single select.tsx — no other files, because the component has no peer dependency beyond React, lucide-react, and the cn utility. No global config file is created; the result is TypeScript source you edit like any other component. Confirm it landed in your editor, then commit it for a clean baseline. If your project uses a monorepo layout or a non-standard source path, the CLI docs cover the flags for targeting a custom location during install. Because the file is yours after install, future Drivn releases will not overwrite your edits.
1 # add the Select source file 2 npx drivn add select
Step 2 — Render the select in a client component
Import Select and mount it inside a client component — because the root owns its open state and registers a mousedown listener, select.tsx ships with a 'use client' directive, so the file that renders it must be a client component too. One import covers the whole control: SelectRoot, Trigger, Menu, and Option are joined with Object.assign, so you write Select.Trigger, Select.Menu, and Select.Option off a single Select. Give the root a value and an onChange, put the current label inside Select.Trigger, and map your options into Select.Option elements inside Select.Menu. Each Select.Option carries a value string; clicking one fires the root's onChange with that value and closes the menu. Drop the control into a settings panel, a filter bar, or a form beside the other Drivn primitives.
1 'use client' 2 import { useState } from 'react' 3 import { Select } from '@/components/ui/select' 4 5 const frameworks = [ 6 { label: 'React', value: 'react' }, 7 { label: 'Vue', value: 'vue' }, 8 { label: 'Svelte', value: 'svelte' }, 9 ] 10 11 export function FrameworkSelect() { 12 const [value, setValue] = useState('') 13 return ( 14 <Select value={value} onChange={setValue}> 15 <Select.Trigger placeholder="Pick a framework"> 16 {frameworks.find((f) => f.value === value)?.label} 17 </Select.Trigger> 18 <Select.Menu> 19 {frameworks.map((f) => ( 20 <Select.Option key={f.value} value={f.value}> 21 {f.label} 22 </Select.Option> 23 ))} 24 </Select.Menu> 25 </Select> 26 ) 27 }
Step 3 — Control the value and read the selection
The Select is controlled: the root destructures value and onChange and forwards them into context without keeping an internal copy, so the selection reflects only what your component holds. Hold the value in useState, pass it as the value prop, and update it from onChange, which fires with the chosen option's value string. Because the value lives in your state, you can use it anywhere else in the render — gate a submit button, filter a list, or post the choice to a route handler — not just inside the dropdown. The Trigger shows whatever you put in its children, usually the label resolved with a find on your options array, and falls back to the placeholder (default Select...) in text-muted-foreground when nothing is picked. To pre-select an option, initialise the state with that option's value instead of an empty string and the trigger renders its label on first paint.
1 const [value, setValue] = useState('react') // pre-selected 2 3 <Select value={value} onChange={setValue}> 4 <Select.Trigger placeholder="Pick a framework"> 5 {frameworks.find((f) => f.value === value)?.label} 6 </Select.Trigger> 7 {/* ...menu */} 8 </Select>
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
Frequently asked questions
Yes. The root holds its open state in React.useState, keeps a React.useRef on its wrapper, and registers a mousedown listener to close on an outside click — all client-only APIs — so select.tsx ships with a 'use client' directive. In any React app that renders on the client it just works; in the Next.js App Router, mount it inside a client component and the surrounding route can stay a Server Component.
It is controlled. The root forwards the value and onChange you pass into context without keeping an internal copy, so the selection reflects only what your component holds. Hold the value in your own useState, pass it as value, and update it from onChange, which fires with the chosen value string and then closes the menu. Without a value and onChange the trigger label and highlighted option will not update.
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 — resolved with a find on your options array — so passing a real value on first render makes that option appear selected immediately, with no placeholder shown. The matching option also picks up the selected highlight inside the open menu.
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. For a searchable list of options rather than a fixed set, reach for the Combobox instead.
Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add select. The component has no dependency on Next.js or any router — it is a self-contained compound built on useState, useRef, context, and the ChevronDown icon, with no imports past React, lucide-react, and the cn utility. In a Vite app everything already runs on the client, so the 'use client' directive is simply ignored.

