Skip to content
Drivn
7 min read

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.

Drivn and shadcn/ui ship dropdown menus from opposite ends of the dependency spectrum. shadcn/ui's DropdownMenu is a thin wrapper around @radix-ui/react-dropdown-menu, and the published recipe re-exports roughly fifteen primitives: DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, and DropdownMenuRadioGroup. Keyboard navigation, typeahead, submenus, checkbox and radio items, and portal rendering all live inside the Radix runtime. Drivn's Dropdown is a plain React file of about 168 lines that ships zero runtime UI dependencies — useState, a mousedown listener for outside-click, and Tailwind utilities for the animation.

The API shapes follow from those foundations. shadcn exposes fifteen named exports you import flat, and the recipe relies on asChild plus a <Slot> from @radix-ui/react-slot so the trigger can adopt any element. Drivn collapses the surface into dot notation: Dropdown, Dropdown.Trigger, Dropdown.Content, Dropdown.Item, Dropdown.Group, Dropdown.Label, and Dropdown.Separator. The trigger renders Drivn's Button with variant="outline" by default, so consumers get a styled handle without composing one. The align prop on the root accepts left or right and picks the matching styles.align utility on Dropdown.Content.

This page walks through every difference: dependency footprint, API surface, animation technique, keyboard behavior, and which use cases each component fits. The shadcn snippets shown in each section mirror the published recipe, not a hypothetical wrapper. If keyboard navigation, submenus, checkbox or radio items, or typeahead are core to your menu, that is a real reason to pick Radix through shadcn — Drivn's Dropdown is the right pick when the menu is a short list of click-driven actions.

Side-by-side comparison

FeatureDrivnshadcn/ui
Underlying primitivePlain React + useState@radix-ui/react-dropdown-menu
Runtime UI dependenciesZero@radix-ui/react-dropdown-menu
Subcomponents to import7 (Dropdown, .Trigger, .Content, .Item, .Group, .Label, .Separator)15 flat exports
Trigger stylingRenders Button variant="outline" by defaultasChild + your own button
Item icon propicon prop on Dropdown.ItemInline JSX inside DropdownMenuItem
Destructive itemdestructive boolean on Dropdown.ItemManual className override
Keyboard navigation
Submenus
Checkbox items
Radio items
Typeahead
Portal rendering
Outside-click dismissdocument mousedown listenerRadix pointer-down outside
Animationopacity + scale CSS transition (150 ms)data-[state] + tailwindcss-animate utilities
LicenseMITMIT
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
2import {
3 DropdownMenu,
4 DropdownMenuContent,
5 DropdownMenuItem,
6 DropdownMenuSeparator,
7 DropdownMenuTrigger,
8} from '@/components/ui/dropdown-menu'
9import { Button } from '@/components/ui/button'
10import { Edit, Trash2 } from 'lucide-react'
11
12export 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
34import { Dropdown } from '@/components/ui/dropdown'
35import { Edit, Trash2 } from 'lucide-react'
36
37export 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

shadcn's DropdownMenu pulls @radix-ui/react-dropdown-menu into package.json, and that package transitively brings in @radix-ui/react-popper, @radix-ui/react-portal, @radix-ui/react-presence, @radix-ui/react-slot, and a handful of internal Radix primitives. The recipe is a styling layer on top of a ~30 KB gzipped runtime that handles focus management, keyboard navigation, submenus, and floating-positioning math. For an actions menu with three to five options, most of that runtime is unused weight in your bundle.

Drivn ships zero runtime UI dependencies for the dropdown. The component imports React, cn from @/utils/cn, and Drivn's Button for the trigger. No Radix, no Floating UI, no portal manager. The full file weighs around 168 lines and adds well under 2 KB of gzipped JS to the route that uses it. The tradeoff is honest: you lose keyboard navigation, submenus, checkbox items, and typeahead. For a click-driven actions menu — the Edit/Duplicate/Delete row on a table, the user-avatar menu in a header, a kebab on a card — that tradeoff is the right one.

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
2const 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
2const close = React.useCallback(
3 () => setOpen(false),
4 []
5)
6
7React.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.

Get started

Install Drivn in one command

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

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

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 is about 168 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.