Drivn vs shadcn/ui — Select Compared
Compare Drivn Select vs shadcn/ui — a context-driven dot-notation dropdown versus a Radix portal popper. Both copy-paste, one ships zero Radix deps.
Drivn's Select and shadcn/ui's Select both give you a styled dropdown that replaces the unstyleable native <select> element, but they are built on opposite foundations. shadcn/ui wraps @radix-ui/react-select and re-exports roughly nine pieces — Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator, and the scroll buttons — each a thin shell over a Radix primitive. Radix renders the open menu into a portal, positions it with its popper engine, and ships full arrow-key navigation and typeahead.
Drivn ships a single file at @/components/ui/select.tsx. It imports React, the ChevronDown icon from lucide-react, and the local cn() utility — nothing else. The four parts are bound together with Object.assign so you call them through dot notation: Select, Select.Trigger, Select.Menu, and Select.Option. A React context carries the open state and the selected value between them, useState tracks whether the menu is open, and a useEffect mousedown listener closes it on an outside click.
The practical split is a context-driven dropdown rendered in the document flow versus a Radix portal popper. This page walks the runtime footprint, the API shape, the open/close model, and the keyboard story so you can pick the flavor that fits.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying primitive | React context + native buttons | @radix-ui/react-select |
| Runtime UI dependencies | lucide-react icon + cn() utility | @radix-ui/react-select |
| API shape | Dot notation — Select.Trigger / Menu / Option | Nine flat exports |
| Menu rendering | Absolute-positioned div, stays in the DOM tree | Portal + positioned popper |
| Open/close state | useState in the Select root | Radix internal state machine |
| Keyboard navigation | Not built in — add it in your copy | Arrow keys + typeahead included |
| Client component | Yes — 'use client' | Yes — 'use client' |
| License | MIT | MIT |
| Copy-paste install |
The runtime footprint
shadcn/ui's Select is a layer over @radix-ui/react-select. The component file imports SelectPrimitive from the Radix package and re-exports its Root, Trigger, Value, Content, Item, Group, Label, and Separator parts, plus two scroll-button helpers. Radix ships the state machine, the portal, the popper positioning, and the keyboard model — all of that JavaScript lands in your bundle the moment you import the component.
Drivn's Select imports React, the ChevronDown icon from lucide-react, and the local cn() utility. Every class the component uses lives in the styles object below, reproduced verbatim from the registry. styles.trigger is the 40-pixel-tall bordered button, styles.menu is the absolutely positioned dropdown with max-h-[200px] overflow-y-auto, and styles.option is each row. There is no Radix package, no portal library, and no popper engine. For a button-triggered action list rather than a value picker, see the Dropdown component, which follows the same zero-dependency framing.
1 // Drivn — styles object, no Radix (verbatim from registry) 2 const styles = { 3 base: 'relative', 4 trigger: cn( 5 'flex items-center justify-between w-full h-10 px-4', 6 'border border-input rounded-[10px] text-sm', 7 'focus:outline-none transition-colors', 8 'cursor-pointer' 9 ), 10 placeholder: 'text-muted-foreground', 11 chevron: cn( 12 'w-4 h-4 text-muted-foreground', 13 'transition-transform duration-200' 14 ), 15 menu: cn( 16 'absolute top-full left-0 right-0 mt-1 z-50', 17 'bg-card border border-border rounded-[10px] p-1', 18 'shadow-lg max-h-[200px] overflow-y-auto', 19 'transition-[opacity,scale] duration-150 ease-out' 20 ), 21 option: cn( 22 'flex items-center w-full px-3 py-2 text-sm rounded-lg', 23 'hover:bg-accent transition-colors cursor-pointer' 24 ), 25 selected: 'text-primary font-medium', 26 }
Dot notation vs nine flat exports
shadcn/ui spreads the Select across nine named exports. A real menu needs Select, SelectTrigger, SelectValue, SelectContent, and SelectItem at minimum, so a single import line runs long and every consumer has to remember which flat name maps to which slot. Drivn binds its four parts with Object.assign(SelectRoot, { Trigger, Menu, Option }), so one import of Select gives you the whole compound and the JSX reads as a tree: Select.Trigger, Select.Menu, Select.Option.
The call site below is the public API, matching the Select docs page. You pass value and onChange to the root, a placeholder to the trigger, and a value to each option. The trigger's children render the label of the current selection — Drivn does not auto-resolve the label from the value, so you look it up yourself, which keeps the component free of an options-registry abstraction. For free-text filtering over the same list, 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 Page() { 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 }
The keyboard story
This is the one place the two flavors genuinely diverge. Radix gives shadcn/ui's Select arrow-key navigation, Home/End jumps, typeahead matching, and a full ARIA listbox role set out of the box. Drivn's Select is built from native <button> elements: every option and the trigger are focusable and operable with Tab and Enter, and the trigger toggles the menu, but there is no arrow-key roving between options and no typeahead.
For most product UIs — a status picker, a country field, a sort control with a handful of choices — Tab and Enter over a short list is enough, and the simpler component is the right trade. When you do need full keyboard support, you own the file: add a keydown handler to Select.Menu that moves focus across the option buttons, and you have it without pulling in Radix. The selected option already carries a visual cue through styles.selected (text-primary font-medium), so the wiring is the only missing piece. For a list that is keyboard-first by design, the Command menu ships arrow-key navigation already built in.
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
Not out of the box. Drivn's Select is built from native button elements, so Tab and Enter operate the trigger and every option, but there is no roving arrow-key focus or typeahead between options. shadcn/ui inherits both from Radix. Because you own the Drivn file after install, you can add a keydown handler to Select.Menu to move focus across the option buttons.
No. Select.Menu is an absolutely positioned div that stays inside the component's own DOM tree, positioned with top-full so it sits directly below the trigger. shadcn/ui renders its content into a portal via Radix. The Drivn menu therefore inherits its parent stacking context — if the field sits inside a container with overflow hidden, give the menu room or a z-index.
The SelectRoot component registers a mousedown listener on the document inside a useEffect. On every click it checks whether the target is contained within the component ref; if it is not, it calls close() to set the open state false. Selecting an option also closes the menu, because onSelect runs your onChange and then close() in sequence.
Drivn binds the four parts with Object.assign, attaching Trigger, Menu, and Option as properties of the Select root. One import of Select gives you the whole compound, and the JSX reads as a tree. shadcn/ui spreads its Select across nine flat exports, which means longer import lines and remembering which name maps to which slot.
Yes. Treat it as a controlled input: hold the chosen value in state and pass value and onChange to the Select root. The component does not render a hidden native input, so on submit you read the value from your own state rather than from form data. This pairs cleanly with react-hook-form by registering the field through a Controller.

