Drivn vs shadcn/ui — Dropdown Component Compared
Compare Drivn Dropdown vs shadcn/ui — dot-notation menu with zero runtime deps versus the Radix DropdownMenu wrapper with full keyboard support.
Two dropdown menus, two opposite bets about what a menu owes its user. shadcn/ui's DropdownMenu is a styling layer over @radix-ui/react-dropdown-menu, and the published recipe re-exports roughly fifteen primitives — trigger, content, item, checkbox item, radio item, group, label, separator, shortcut, portal, and the four submenu pieces. Focus management, typeahead, arrow-key traversal, and floating-position math all live inside the Radix runtime. Drivn's Dropdown bets the other way: just under two hundred lines of plain React with zero runtime UI dependencies, where useState holds open state, a mousedown listener on document handles dismissal, and Tailwind utilities carry the animation.
The API shape follows from the bet. shadcn hands you fifteen flat imports plus asChild and a <Slot> from @radix-ui/react-slot so the trigger can adopt whatever element you pass it. Drivn hands you seven names behind a single import: Dropdown, .Trigger, .Content, .Item, .Group, .Label, and .Separator. The trigger already renders Drivn's Button at variant="outline" and rounded="md", so there is nothing to compose, and align on the root swaps left-0 for right-0 on the content.
What follows is the accounting: dependency weight, API surface, animation technique, dismissal behavior, and the list of things Drivn simply does not ship. Every shadcn snippet on this page mirrors the published recipe rather than an invented wrapper. Submenus, checkbox and radio items, typeahead, and arrow-key traversal are real reasons to stay on Radix. Drivn's Dropdown wins the other case — a short list of click-driven actions where owning the file beats owning the dependency.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying primitive | Plain React + useState | @radix-ui/react-dropdown-menu |
| Runtime UI dependencies | Zero | @radix-ui/react-dropdown-menu |
| Subcomponents to import | 7 (Dropdown, .Trigger, .Content, .Item, .Group, .Label, .Separator) | 15 flat exports |
| Trigger styling | Renders Button variant="outline" by default | asChild + your own button |
| Item icon prop | icon prop on Dropdown.Item | Inline JSX inside DropdownMenuItem |
| Destructive item | destructive boolean on Dropdown.Item | Manual className override |
| Keyboard navigation | ||
| Submenus | ||
| Checkbox items | ||
| Radio items | ||
| Typeahead | ||
| Portal rendering | ||
| Outside-click dismiss | document mousedown listener | Radix pointer-down outside |
| Animation | opacity + scale CSS transition (150 ms) | data-[state] + tailwindcss-animate utilities |
| License | MIT | MIT |
| Copy-paste install |
API side-by-side
shadcn/ui's DropdownMenu is a flat namespace of about fifteen named exports — DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, plus the checkbox, radio, submenu, group, portal, label, separator, and shortcut variants. The trigger uses Radix's asChild to adopt your button: wrap a <Button> in <DropdownMenuTrigger asChild> and Radix forwards refs and event handlers onto it. Drivn collapses the surface into dot notation. One import gets you the whole compound, and Dropdown.Trigger already renders Button with variant="outline" and rounded="md" inside the registry source — no asChild, no Slot.
Items are the other simplification. shadcn places icons as inline JSX inside <DropdownMenuItem> and styles destructive items by overriding className. Drivn moves both into props: <Dropdown.Item icon={Edit}> accepts a Lucide component or a ReactElement, and <Dropdown.Item destructive> flips on the destructive utility from the styles object. The call site goes from fifteen imports plus a Slot adapter to two imports and a flat children block.
1 // shadcn/ui — flat exports, asChild trigger, inline icons 2 import { 3 DropdownMenu, 4 DropdownMenuContent, 5 DropdownMenuItem, 6 DropdownMenuSeparator, 7 DropdownMenuTrigger, 8 } from '@/components/ui/dropdown-menu' 9 import { Button } from '@/components/ui/button' 10 import { Edit, Trash2 } from 'lucide-react' 11 12 export function ActionsMenu() { 13 return ( 14 <DropdownMenu> 15 <DropdownMenuTrigger asChild> 16 <Button variant="outline">Menu</Button> 17 </DropdownMenuTrigger> 18 <DropdownMenuContent align="end"> 19 <DropdownMenuItem> 20 <Edit className="mr-2 h-4 w-4" /> 21 Edit 22 </DropdownMenuItem> 23 <DropdownMenuSeparator /> 24 <DropdownMenuItem className="text-destructive"> 25 <Trash2 className="mr-2 h-4 w-4" /> 26 Delete 27 </DropdownMenuItem> 28 </DropdownMenuContent> 29 </DropdownMenu> 30 ) 31 } 32 33 // Drivn — dot notation, icon prop, destructive boolean 34 import { Dropdown } from '@/components/ui/dropdown' 35 import { Edit, Trash2 } from 'lucide-react' 36 37 export default function Page() { 38 return ( 39 <Dropdown align="right"> 40 <Dropdown.Trigger>Menu</Dropdown.Trigger> 41 <Dropdown.Content> 42 <Dropdown.Item icon={Edit}>Edit</Dropdown.Item> 43 <Dropdown.Separator /> 44 <Dropdown.Item icon={Trash2} destructive> 45 Delete 46 </Dropdown.Item> 47 </Dropdown.Content> 48 </Dropdown> 49 ) 50 }
Dependency footprint
Running npx shadcn add dropdown-menu writes @radix-ui/react-dropdown-menu into package.json, and that single line drags @radix-ui/react-popper, @radix-ui/react-portal, @radix-ui/react-presence, @radix-ui/react-slot, and a stack of internal Radix packages along behind it. What you have installed is a focus manager, a typeahead engine, a presence controller, and a floating-position solver — roughly 30 KB gzipped of behavior. For a menu holding three actions and a separator, most of that ships to the browser and never runs.
Drivn's Dropdown adds nothing to package.json. Open the registry source and the import list is three lines: React, cn from @/utils/cn, and Drivn's Button for the trigger. No Radix, no Floating UI, no portal manager, no transitive tree to audit at upgrade time. Just under two hundred lines, well under 2 KB gzipped on the route that renders it. The cost is stated plainly rather than buried: no arrow keys, no submenus, no checkbox or radio items, no typeahead. For the Edit/Duplicate/Delete row on a Table, the avatar menu in a header, or a kebab on a Card, that is the right trade.
1 // shadcn/ui — package.json after `npx shadcn add dropdown-menu` 2 { 3 "dependencies": { 4 "@radix-ui/react-dropdown-menu": "^2.x" 5 } 6 } 7 8 // Drivn — package.json after `npx drivn add dropdown` 9 // (no new dependencies added)
Animation technique
shadcn animates the DropdownMenu through tailwindcss-animate and Radix's data-[state=open] and data-[state=closed] attributes. The recipe applies data-[state=open]:animate-in, data-[state=closed]:animate-out, fade-in-0, zoom-in-95, and slide-in-from-top-2 to the content surface, plus a separate data-[side=*]:slide-in-from-* set for the four sides Radix can position toward. The animation is driven by Radix's presence component, which keeps the element in the tree long enough for the exit transition to play.
Drivn uses a plain CSS transition on opacity and scale. The styles.content block in the registry source declares transition-[opacity,scale] duration-150 ease-out, and Dropdown.Content toggles between opacity-100 scale-100 when open and opacity-0 scale-95 pointer-events-none when closed. No data-[state] attribute, no animate-in utility, no presence manager — the element stays in the DOM and CSS handles the fade and the slight scale. The result is a 150 ms entrance and exit that ships without a runtime animation library.
1 // Drivn — animation lives in styles.content + the open/closed class swap 2 const styles = { 3 content: cn( 4 'absolute top-full mt-1 min-w-[180px] z-50', 5 'bg-card border border-border rounded-[10px] p-1', 6 'shadow-lg', 7 'transition-[opacity,scale] duration-150 ease-out' 8 ), 9 } 10 11 // Inside Content — the open prop toggles two utility groups 12 <div 13 className={cn( 14 styles.content, 15 styles.align[align], 16 open 17 ? 'opacity-100 scale-100' 18 : 'opacity-0 scale-95 pointer-events-none', 19 className 20 )} 21 > 22 {children} 23 </div>
Outside-click, Escape, and what Drivn does not ship
Radix gives shadcn's DropdownMenu the full keyboard menu pattern from the ARIA Authoring Practices Guide: arrow keys to move focus, Home and End to jump to the ends, typeahead to filter by first letter, Escape to close while restoring focus to the trigger, and Tab to close while moving forward. Submenus open on ArrowRight, checkbox and radio items toggle on Space, and the floating content stays positioned through scroll and viewport edges via @radix-ui/react-popper.
Drivn's Dropdown ships outside-click and click-to-close, and that is all. The DropdownRoot component attaches a mousedown listener on document and calls setOpen(false) whenever the click lands outside the ref. Dropdown.Item calls the consumer's onClick and then closes the menu. There is no keyboard navigation, no submenus, no checkbox or radio items, and no Escape handler. If your menu needs the full keyboard pattern, pick shadcn's Radix-backed version. If it is three to five click-driven actions where mouse and touch are the primary inputs — the case Dropdown is designed for — Drivn's smaller surface is the right shape.
1 // Drivn — the entire dismissal surface is one effect on DropdownRoot 2 const close = React.useCallback( 3 () => setOpen(false), 4 [] 5 ) 6 7 React.useEffect(() => { 8 const onClick = (e: MouseEvent) => { 9 if (!ref.current?.contains(e.target as Node)) 10 close() 11 } 12 document.addEventListener('mousedown', onClick) 13 return () => 14 document.removeEventListener('mousedown', onClick) 15 }, [close])
Use cases — when each wins
Pick shadcn's Radix-backed DropdownMenu when the menu is a primary interaction surface that has to satisfy WCAG keyboard requirements, when items include checkbox or radio state, when you need submenus for hierarchical actions, or when the content must portal to escape an overflow: hidden ancestor. Power-user tools, document editors, dashboard chrome with many toggles, and design-system menus that ship to large teams all benefit from what Radix provides. The runtime cost is the price of admission for that behavior.
Pick Drivn's Dropdown when the menu is a short list of click-driven actions: the user-avatar menu in a header, an Edit/Duplicate/Delete row on a Table, a kebab menu on a Card, an action menu next to a Button group. The align prop covers the two positions these patterns need, the icon and destructive props on Dropdown.Item cover the styling cases, and the file you own after install stays under 200 lines. If the same app later grows a menu that needs submenus or full keyboard navigation, drop shadcn's DropdownMenu in for that specific surface — the two patterns can coexist without conflict.
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 — Drivn Dropdown is a click-driven menu. It opens when the trigger is clicked, closes on outside-click or on item-click, and does not handle arrow keys, Home/End, typeahead, or Escape. If your menu needs the full ARIA Authoring Practices keyboard pattern (arrow keys to move focus, typeahead to filter, Escape to dismiss while restoring focus to the trigger), shadcn/ui's Radix-backed DropdownMenu is the right pick for that surface. Drivn's Dropdown is designed for short action menus where mouse and touch are the primary inputs.
The component is small enough that the standard library covers it. useState tracks open/closed, a mousedown listener on document closes the menu on outside-click, and a CSS transition on opacity and scale handles the entrance and exit. There is no positioning math beyond absolute top-full mt-1, no portal manager, no presence component. The full file runs just under two hundred lines, ships zero runtime UI dependencies, and adds under 2 KB of gzipped JS to the route. Avoiding Radix here keeps the component editable: every behavior lives in one file you own after install.
Not out of the box — Dropdown.Item is a single action shape with icon and destructive props, and there is no Dropdown.CheckboxItem, Dropdown.RadioItem, or Dropdown.Sub in the registry source. If you need either pattern, you have two options: extend the component in your own repo (the source lives at @/components/ui/dropdown after install) or reach for shadcn's Radix-backed DropdownMenu for that specific surface. The two libraries can coexist in the same project.
For click-only menus the migration is largely a JSX rename. DropdownMenu becomes Dropdown, DropdownMenuTrigger asChild becomes Dropdown.Trigger (drop the inner Button — the trigger renders one for you), DropdownMenuContent align="end" becomes Dropdown.Content on a Dropdown align="right" root, DropdownMenuItem becomes Dropdown.Item with the icon moved from inline JSX to the icon prop, and DropdownMenuSeparator becomes Dropdown.Separator. Skip the migration on menus that use checkbox items, radio items, submenus, or rely on keyboard navigation — those stay on Radix.
The Dropdown file starts with "use client" because it uses useState and attaches a mousedown listener through useEffect. You can import it from a Server Component, and Next.js draws the client boundary automatically based on the directive at the top of the file. Render the trigger and content inside a server-rendered page or layout — the dropdown hydrates on the client without extra configuration. No portal is involved, so the menu mounts in the same DOM subtree as the trigger.