Skip to content
Drivn
5 min read

How to Add a Combobox to a React App

Step-by-step guide to adding a searchable, copy-and-own Combobox to any React app with the Drivn CLI — single select, multi-select, and grouped options.

A combobox is a select box you can type into — it shows a list of options, filters them as the user types, and writes the chosen value back to your state. Building one by hand means juggling open state, keyboard navigation, fuzzy filtering, and click-outside dismissal, which is why teams usually reach for a heavy dependency. Drivn takes a lighter path: the CLI copies a combobox.tsx file into your repository that wraps cmdk for the filtering and keyboard logic and adds the trigger, popover, tags, and selection state on top.

The component is a compound API built with React.createContextCombobox is the root, and Combobox.Trigger, Combobox.Content, Combobox.Item, Combobox.Group, Combobox.Empty, and Combobox.Separator hang off it with dot notation. The root holds the open state, the selected value, and a click-outside listener; the Content mounts a cmdk Command with shouldFilter so typing in the search input narrows the list for free. Because it tracks state with useState, the source is marked 'use client', while the page around it can still render on the server.

This guide adds the Combobox to an existing React app — install the CLI, render a single-select picker with controlled state, switch on multi-select with tags, then group options and handle the empty state. It works the same in Vite + React or a Next.js App Router project. For the full reference see the Combobox docs; for live patterns see the Combobox examples.

Prerequisites

Before installing the Combobox, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your CSS, 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. The Combobox is one of the few Drivn components with a runtime dependency beyond lucide-react: it imports Command as CommandPrimitive from cmdk for the searchable list and keyboard handling. The CLI installs cmdk automatically when you add the component, so there is nothing to add by hand — the installation page lists the minimal config if you are wiring a custom setup.

Step 1 — Install Drivn via the CLI

Run the CLI from your project root to add the Combobox source file. The command prompts once for your install directory (defaulting to src/components/ui/), writes combobox.tsx, and adds cmdk and lucide-react to your package.json if they are not already present. No global config file is created — the Combobox is a single TypeScript file in your repo that you edit like any other component. The icons it pulls from lucide-react are ChevronDown, X, Check, and Search, used for the trigger chevron, the clear button, the selected tick, and the search field. Confirm the file landed in your editor, then commit it. The CLI docs cover the flags for targeting a custom location during install.

1# add the Combobox to your existing React project
2npx drivn add combobox
3
4# verify the file was written
5ls src/components/ui/combobox.tsx

Step 2 — Render a single-select combobox

Import the Combobox and compose it from its sub-components. Hold the selected value in useState, pass it as value, and pass your setter as onChange. The Combobox.Trigger shows the current selection (look it up from your options array) and a placeholder when empty; Combobox.Content mounts the searchable list with its own placeholder for the search field. Each Combobox.Item takes a value and renders a Check icon when it is the selected one. The cmdk engine inside Content filters items as the user types, and selecting an item in single mode calls onChange and closes the popover automatically. Because the source is 'use client', render it inside a client component in a Next.js App Router app. See the Combobox docs for the full prop table.

1'use client'
2import { useState } from 'react'
3import { Combobox } from '@/components/ui/combobox'
4
5const frameworks = [
6 { label: 'React', value: 'react' },
7 { label: 'Vue', value: 'vue' },
8 { label: 'Svelte', value: 'svelte' },
9 { label: 'Next.js', value: 'nextjs' },
10]
11
12export function FrameworkPicker() {
13 const [value, setValue] = useState('')
14
15 return (
16 <Combobox value={value} onChange={(v) => setValue(v as string)}>
17 <Combobox.Trigger placeholder="Select framework...">
18 {frameworks.find((f) => f.value === value)?.label}
19 </Combobox.Trigger>
20 <Combobox.Content placeholder="Search frameworks...">
21 <Combobox.Empty />
22 {frameworks.map((f) => (
23 <Combobox.Item key={f.value} value={f.value}>
24 {f.label}
25 </Combobox.Item>
26 ))}
27 </Combobox.Content>
28 </Combobox>
29 )
30}

Step 3 — Multi-select and a clearable trigger

Pass multiple to the root and the value becomes a string[]. In multiple mode, selecting an item toggles it in the array — the source runs arr.includes(v) ? arr.filter((i) => i !== v) : [...arr, v] — and the popover stays open so the user can pick several. The Combobox.Trigger renders the selected values as tags when you map them inside it, and passing clearable to the trigger shows an X button that calls onClear to reset the selection to an empty array. Initialize your state as useState<string[]>([]) so the types line up. Each selected item still shows its Check icon in the list, and the search input keeps filtering as you go.

1'use client'
2import { useState } from 'react'
3import { Combobox } from '@/components/ui/combobox'
4
5export function TagPicker() {
6 const [tags, setTags] = useState<string[]>([])
7
8 return (
9 <Combobox
10 multiple
11 value={tags}
12 onChange={(v) => setTags(v as string[])}
13 >
14 <Combobox.Trigger placeholder="Add tags..." clearable>
15 {tags.join(', ')}
16 </Combobox.Trigger>
17 <Combobox.Content placeholder="Search tags...">
18 <Combobox.Empty />
19 <Combobox.Item value="bug">Bug</Combobox.Item>
20 <Combobox.Item value="feature">Feature</Combobox.Item>
21 <Combobox.Item value="docs">Docs</Combobox.Item>
22 </Combobox.Content>
23 </Combobox>
24 )
25}

Step 4 — Group options, add icons, and handle empty results

For longer lists, wrap items in Combobox.Group with a heading and separate sections with Combobox.Separator. Each Combobox.Item accepts an icon prop that takes a component reference (icon={Globe}) and renders it before the label — the same icon-as-component pattern Drivn uses across its components. The Combobox.Empty element renders inside the list and, thanks to cmdk, shows automatically when the search query matches nothing; pass children to customize the text or leave it for the default "No results found." To restyle, open src/components/ui/combobox.tsx and edit the styles object — item controls the row (it reads data-[selected=true]:bg-accent from cmdk), content controls the popover, and every class reads your Tailwind tokens, so a theme change in globals re-themes every Combobox at once. See the Combobox examples for grouped lists and async data.

1'use client'
2import { useState } from 'react'
3import { Globe, Code } from 'lucide-react'
4import { Combobox } from '@/components/ui/combobox'
5
6export function GroupedPicker() {
7 const [value, setValue] = useState('')
8
9 return (
10 <Combobox value={value} onChange={(v) => setValue(v as string)}>
11 <Combobox.Trigger placeholder="Select..." />
12 <Combobox.Content>
13 <Combobox.Empty>Nothing matches.</Combobox.Empty>
14 <Combobox.Group heading="Frontend">
15 <Combobox.Item value="react" icon={Code}>React</Combobox.Item>
16 <Combobox.Item value="vue" icon={Code}>Vue</Combobox.Item>
17 </Combobox.Group>
18 <Combobox.Separator />
19 <Combobox.Group heading="Backend">
20 <Combobox.Item value="node" icon={Globe}>Node.js</Combobox.Item>
21 </Combobox.Group>
22 </Combobox.Content>
23 </Combobox>
24 )
25}
Get started

Install Drivn in one command

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

Follow Drivn updates
New components, improvements, and guides every release.
Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add combobox. The Combobox has no dependency on Next.js or any router — it renders anywhere React and Tailwind reach the DOM. The CLI installs cmdk and lucide-react alongside the component file, so there is nothing extra to configure. Vite + React is the most common non-Next setup and works without changes.

The Combobox.Content mounts a cmdk Command primitive with shouldFilter enabled, so typing in the search input narrows the visible items automatically — you do not write any filter logic. Each Combobox.Item carries its value, and cmdk matches the query against it. The Combobox.Empty element shows when nothing matches, again driven by cmdk, so the empty state needs no manual wiring.

Pass the multiple prop to the Combobox root and the value type changes from string to string[]. In multiple mode, selecting an item toggles it in the array and the popover stays open so the user can pick several; in single mode it replaces the value and closes. Initialize your state to match — useState('') for single, useState<string[]>([]) for multiple — and read the value back from onChange.

Yes. The Combobox root registers a mousedown listener on document in a useEffect and closes the popover when the click target is outside the component's wrapping <div>, tracked by a ref. The listener is cleaned up on unmount. You do not wire any outside-click handling yourself — it ships in the component file the CLI writes, and you can edit it like any other code in your repo.