Drivn vs shadcn/ui — Combobox Component Compared
Drivn vs shadcn/ui React Combobox: Drivn ships a real compound component with built-in multi-select — shadcn documents a Popover + Command composition recipe.
The shadcn/ui docs have a Combobox page, but the CLI has no combobox component to add. What the page gives you is a recipe: install the Popover primitive, install the Command primitive, then write the glue that holds the two together. Drivn ships the assembled thing. Combobox arrives as one compound primitive — Combobox, Combobox.Trigger, Combobox.Content, Combobox.Item — with open state, outside-click dismissal, and selection logic already inside the component file.
Count the difference at the call site. The shadcn version pulls nine named imports out of two files, holds a useState(false) for the popover and a useState("") for the selection, and renders a hand-styled <Button role="combobox"> that carries its own chevron. The Drivn version is one import and three tags under a root that takes value and onChange. Nothing else is yours to wire.
Three surfaces actually diverge, and this page walks each one: the import and API shape, multi-select support (a multiple prop in Drivn, undocumented in the shadcn recipe), and the popover layer — Radix Popover plus its floating-ui dependency on one side, absolute top-full positioning and a ten-line mousedown listener on the other. Every snippet below is checked against the Combobox source the Drivn CLI writes into your repo. Migrating an existing shadcn recipe is mostly deletion: collapse the two imports into one, drop the open boolean, and let the Drivn root hold through context what your component was holding by hand.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Ships as single component | Composition recipe (Popover + Command) | |
| Underlying primitive | cmdk | cmdk + @radix-ui/react-popover |
| Imports needed | 1 (Combobox) | 2 (Popover + Command) |
| API shape | Dot notation (Combobox.Trigger) | Flat (PopoverTrigger + CommandInput) |
| Built-in multi-select | multiple prop with tag chips | |
| Open state | Internal — useContext | External useState(false) |
| Click-outside dismissal | Built in via mousedown listener | Via Radix Popover |
| Clearable trigger | clearable prop | Hand-rolled X button |
| Runtime UI deps | cmdk + lucide-react | cmdk + @radix-ui/react-popover + lucide-react |
| License | MIT | MIT |
API side-by-side
shadcn's recommended Combobox is a long composition. You import Popover, PopoverTrigger, PopoverContent, Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem — nine names from two files. You hold an open boolean in useState(false) and pass it to Popover as a controlled prop. You hold a value string in useState("") and update it inside the CommandItem onSelect handler. You style the trigger as a <Button variant="outline" role="combobox" aria-expanded={open}> and add a ChevronsUpDown icon yourself.
Drivn collapses all of that into one tag tree. The Combobox source holds the open state inside a context provider, exposes Combobox.Trigger as the click target with the chevron and clearable X already in place, and renders Combobox.Content as an absolute-positioned panel that animates with transition-[opacity,scale]. You pass value and onChange to the root and the rest is handled internally.
1 // shadcn/ui — composition recipe 2 'use client' 3 import { useState } from 'react' 4 import { Check, ChevronsUpDown } from 'lucide-react' 5 import { Button } from '@/components/ui/button' 6 import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command' 7 import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' 8 9 export function FrameworkCombobox() { 10 const [open, setOpen] = useState(false) 11 const [value, setValue] = useState('') 12 13 return ( 14 <Popover open={open} onOpenChange={setOpen}> 15 <PopoverTrigger asChild> 16 <Button variant="outline" role="combobox" aria-expanded={open}> 17 {value || 'Select framework...'} 18 <ChevronsUpDown className="ml-2 h-4 w-4" /> 19 </Button> 20 </PopoverTrigger> 21 <PopoverContent> 22 <Command> 23 <CommandInput placeholder="Search framework..." /> 24 <CommandList> 25 <CommandEmpty>No framework found.</CommandEmpty> 26 <CommandGroup> 27 <CommandItem onSelect={(v) => { setValue(v); setOpen(false) }}>React</CommandItem> 28 </CommandGroup> 29 </CommandList> 30 </Command> 31 </PopoverContent> 32 </Popover> 33 ) 34 } 35 36 // Drivn — single component 37 'use client' 38 import { useState } from 'react' 39 import { Combobox } from '@/components/ui/combobox' 40 41 export function FrameworkCombobox() { 42 const [value, setValue] = useState('') 43 44 return ( 45 <Combobox value={value} onChange={setValue}> 46 <Combobox.Trigger placeholder="Select framework...">{value}</Combobox.Trigger> 47 <Combobox.Content placeholder="Search framework..."> 48 <Combobox.Empty /> 49 <Combobox.Item value="react">React</Combobox.Item> 50 </Combobox.Content> 51 </Combobox> 52 ) 53 }
Multi-select with tag chips
shadcn's Combobox recipe is single-select only. The composition page does not document a multi-select pattern — the official advice is to fork the recipe, swap value: string for value: string[], render the selected values as tag chips above the trigger, and write the toggle logic by hand. Drivn ships multi-select as a built-in prop. Pass multiple to the root and the Combobox source switches the trigger from rendering a single string to rendering tag chips, each with an <X /> button that removes the item via the same onSelect handler that adds it.
The internal logic is in the root. When multiple is set, onSelect reads the current array, toggles the value via arr.includes(v) ? arr.filter(i => i !== v) : [...arr, v], and forwards the new array to onChange. The trigger reads Array.isArray(value) ? value : [] and renders one <span className={styles.tag.base}> per entry. Click on the X inside a tag and the same onSelect removes it. No external state, no separate add/remove handlers — one onChange covers both directions.
1 // Drivn — multi-select with multiple prop 2 'use client' 3 import { useState } from 'react' 4 import { Combobox } from '@/components/ui/combobox' 5 6 export function FrameworksCombobox() { 7 const [value, setValue] = useState<string[]>([]) 8 9 return ( 10 <Combobox multiple value={value} onChange={(v) => setValue(v as string[])}> 11 <Combobox.Trigger placeholder="Select frameworks..." /> 12 <Combobox.Content placeholder="Search frameworks..."> 13 <Combobox.Empty /> 14 <Combobox.Item value="react">React</Combobox.Item> 15 <Combobox.Item value="vue">Vue</Combobox.Item> 16 <Combobox.Item value="angular">Angular</Combobox.Item> 17 <Combobox.Item value="svelte">Svelte</Combobox.Item> 18 </Combobox.Content> 19 </Combobox> 20 ) 21 }
Open state and click-outside
shadcn delegates open state and click-outside dismissal to Radix Popover. You pass open and onOpenChange to <Popover>, hold the boolean in your component, and Radix handles the listener that closes the popover when the user clicks outside or presses Escape. The pattern works, but the open state lives in your component file even though no other code in your component reads it. Drivn moves the boolean inside the component. The root calls useState(false) and exposes open and setOpen via context to Combobox.Trigger and Combobox.Content.
Click-outside dismissal is also internal. The Combobox source attaches a mousedown listener on document inside useEffect and closes the dropdown when the click target is not inside the root ref. The cleanup function removes the listener on unmount. The whole behavior is about ten lines of plain React — no Radix Popper, no portal, no positioning library. The dropdown uses absolute top-full left-0 right-0 to position itself directly under the trigger.
1 // Drivn — internal click-outside, verbatim from the Combobox source 2 React.useEffect(() => { 3 const onClick = (e: MouseEvent) => { 4 if (!ref.current?.contains(e.target as Node)) 5 close() 6 } 7 document.addEventListener('mousedown', onClick) 8 return () => 9 document.removeEventListener('mousedown', onClick) 10 }, [close])
Bundle cost and customization
Add up what each version puts in node_modules. The shadcn recipe needs cmdk for the search list and @radix-ui/react-popover for the panel, and Radix Popover pulls @floating-ui/react-dom behind it to position that panel. Drivn keeps cmdk and drops the rest: the dropdown positions itself with absolute top-full left-0 right-0 mt-1 z-50, animates with transition-[opacity,scale] duration-150 ease-out, and dismisses through the mousedown listener from the previous section. That leaves cmdk and lucide-react for the chevron, X, and check icons — two packages to track instead of four.
Customization flips as well. In the shadcn recipe the trigger is your shared <Button>, so restyling the combobox means editing a component every other screen also renders. In Drivn the trigger belongs to the Combobox source: open the file, edit styles.trigger.base, save. Every Combobox in the app follows and your buttons stay untouched. The entire style surface is one object at the top of that file — trigger, tag chips, dropdown, list, item, separator — which is also where you would change the rounded-[10px] corners or swap the bg-muted chips for something branded. The Combobox examples page shows what that unlocks: groups, clearable triggers, item icons, disabled items, async loading.
1 // Drivn — styles object lives in the Combobox source you own 2 const styles = { 3 base: 'relative', 4 trigger: { 5 base: cn( 6 'flex items-center justify-between w-full min-h-10', 7 'px-3 gap-2', 8 'border border-input rounded-[10px] text-sm', 9 'focus:outline-none transition-[color,box-shadow]', 10 'focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring', 11 'cursor-pointer' 12 ), 13 singleText: 'flex-1 truncate text-left', 14 placeholder: 'text-muted-foreground', 15 // ... 16 }, 17 content: cn( 18 'absolute top-full left-0 right-0 mt-1 z-50', 19 'bg-card border border-border rounded-[10px]', 20 'shadow-lg overflow-hidden', 21 'transition-[opacity,scale] duration-150 ease-out' 22 ), 23 // ... edit this object to restyle every Combobox in your app 24 }
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
No. The shadcn docs site has a Combobox page but the page documents a composition recipe — combine the Popover primitive with the Command primitive, hold the open state in useState, hold the selected value in another useState, and style the trigger button as a custom outline button with a chevron icon. The recipe is about 40 lines of glue code that you copy into your repo. Drivn ships a single Combobox compound component instead.
The Drivn Combobox accepts a multiple prop on the root. When set, the internal onSelect callback toggles values in a string array — arr.includes(v) ? arr.filter(i => i !== v) : [...arr, v] — and the trigger switches from rendering a single string to rendering tag chips, each with an X icon that removes the item. The whole pattern lives in the component source. shadcn's composition recipe is single-select only.
Because the Combobox dropdown only needs three things: position the panel under the trigger, animate it on open, and dismiss on outside click. The Drivn Combobox source uses CSS absolute top-full for positioning, transition-[opacity,scale] for the animation, and a mousedown listener inside useEffect for the dismissal. About ten lines for the listener — no floating-ui dependency, no portal, no extra package.
Yes. The Drivn Combobox wraps the same cmdk primitive shadcn does, so Combobox.Group (with a heading prop) and Combobox.Separator are exposed via dot notation on the root. The fuzzy search filtering, keyboard navigation, and ARIA roles all come from cmdk and behave identically. The difference is only in the wrapper around cmdk, not the search behavior itself.

