Drivn vs shadcn/ui — Context Menu Compared
Side-by-side comparison of Drivn and shadcn/ui React Context Menu — zero runtime deps vs Radix, dot notation API, submenu hover model, and bundle cost.
Drivn and shadcn/ui both expose a right-click Context Menu primitive, but the implementations sit on opposite sides of the dependency line. shadcn/ui re-exports @radix-ui/react-context-menu with Tailwind styling glued on top — that gives you full Radix keyboard navigation, ARIA roles, focus management, plus checkbox and radio item variants for free, at the cost of pulling Radix into your bundle for one feature. Drivn ships a hand-written compound that uses nothing but React state, a context provider, and CSS. No Radix, no floating-ui, no portals — the menu is a fixed-positioned div placed at e.clientX / e.clientY from the onContextMenu event.
The API also diverges. shadcn exports fourteen named components — ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent — each as its own line at the top of the file. Drivn collapses the surface to one import: ContextMenu, then dot notation for every part — ContextMenu.Trigger, ContextMenu.Item, ContextMenu.Sub, ContextMenu.SubTrigger, ContextMenu.SubContent, ContextMenu.Group, ContextMenu.Label, ContextMenu.Separator. TypeScript narrows each prop set automatically and your import line stays a single statement no matter how many parts you compose.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying library | None — pure React + Tailwind | @radix-ui/react-context-menu |
| API style | Dot notation (ContextMenu.Item) | Named exports (ContextMenuItem) |
| Bundle cost beyond React | 0 runtime deps | ~25kb Radix runtime |
| Submenu trigger | Hover (CSS group-hover) | Hover + click |
| Keyboard navigation | Escape only | Full arrow keys + Home/End |
| CheckboxItem / RadioItem | ||
| Item icon prop | icon={Edit} or <Edit /> | Pass JSX as children |
| Shortcut text | shortcut="⌘C" prop | <ContextMenuShortcut> |
| Destructive item | destructive prop | Style manually |
| Copy-paste install |
API side-by-side
shadcn/ui treats every Context Menu part as a separate named component re-exported from Radix — fourteen identifiers that all start with ContextMenu. Drivn folds the same parts onto a single object via Object.assign, so dot access — ContextMenu.Trigger, ContextMenu.Item, ContextMenu.Sub — returns the matching subcomponent and TypeScript infers the right props at the call site. The visible difference shows up in the import line: shadcn pulls in eight to fourteen named symbols depending on the menu, while Drivn pulls in one. See the Context Menu docs for the full prop reference.
1 // shadcn/ui — named exports 2 import { 3 ContextMenu, 4 ContextMenuTrigger, 5 ContextMenuContent, 6 ContextMenuItem, 7 ContextMenuSeparator, 8 ContextMenuShortcut, 9 } from '@/components/ui/context-menu' 10 11 <ContextMenu> 12 <ContextMenuTrigger>Right-click here</ContextMenuTrigger> 13 <ContextMenuContent> 14 <ContextMenuItem> 15 Copy 16 <ContextMenuShortcut>⌘C</ContextMenuShortcut> 17 </ContextMenuItem> 18 <ContextMenuSeparator /> 19 <ContextMenuItem>Delete</ContextMenuItem> 20 </ContextMenuContent> 21 </ContextMenu> 22 23 // Drivn — dot notation 24 import { ContextMenu } from '@/components/ui/context-menu' 25 import { Copy, Trash2 } from 'lucide-react' 26 27 <ContextMenu> 28 <ContextMenu.Trigger> 29 <div>Right-click here</div> 30 </ContextMenu.Trigger> 31 <ContextMenu.Content> 32 <ContextMenu.Item icon={Copy} shortcut="⌘C"> 33 Copy 34 </ContextMenu.Item> 35 <ContextMenu.Separator /> 36 <ContextMenu.Item icon={Trash2} destructive> 37 Delete 38 </ContextMenu.Item> 39 </ContextMenu.Content> 40 </ContextMenu>
Zero runtime deps vs Radix
shadcn/ui's Context Menu is a styled wrapper around @radix-ui/react-context-menu. That dependency carries the whole Radix primitives runtime — collision detection, portal rendering, focus scope, dismissable layer — across roughly twenty-five kilobytes of vendor JavaScript. Drivn ships zero runtime UI deps. The Drivn implementation tracks open and the click pos in component state, listens for mousedown and keydown on document to close, and renders the menu as a fixed-positioned div directly in the DOM. There is no portal, no floating-ui, and nothing imported from outside React. If your project is not already paying the Radix cost for other components, this is the difference between a tiny right-click handler and a real third-party runtime.
1 React.useEffect(() => { 2 if (!open) return 3 const close = () => setOpen(false) 4 const onKeyDown = (e: KeyboardEvent) => { 5 if (e.key === 'Escape') close() 6 } 7 document.addEventListener('mousedown', close) 8 document.addEventListener('keydown', onKeyDown) 9 return () => { 10 document.removeEventListener('mousedown', close) 11 document.removeEventListener('keydown', onKeyDown) 12 } 13 }, [open]) 14 15 const onContextMenu = React.useCallback( 16 (e: React.MouseEvent) => { 17 e.preventDefault() 18 setPos({ x: e.clientX, y: e.clientY }) 19 setOpen(true) 20 }, [])
Item ergonomics — icon, shortcut, destructive
shadcn relies on JSX composition — every icon, every shortcut hint, every destructive style is something you wire in the children of ContextMenuItem. Drivn moves the common cases onto props. The icon prop accepts either a component reference (icon={Copy}) or a React element (icon={<Copy className="..." />}); the source uses React.isValidElement(Icon) ? Icon : <Icon className="w-4 h-4" /> to handle both. The shortcut prop renders a right-aligned muted span (ml-auto text-xs text-muted-foreground). The destructive prop swaps in red-tinted hover styles via text-destructive hover:bg-destructive/10. Reaching for these in shadcn means you nest ContextMenuShortcut, hand-style the destructive item, and lay out the icon yourself. Drivn covers the patterns that ninety percent of menus need without the boilerplate. The same icon-and-shortcut shape appears across the Dropdown component, so the API stays familiar across menu types.
1 const styles = { 2 item: cn( 3 'flex items-center gap-2 w-full px-3 py-2', 4 'text-sm text-foreground rounded-lg', 5 'hover:bg-accent transition-colors cursor-pointer' 6 ), 7 destructive: 'text-destructive hover:bg-destructive/10', 8 disabled: 'opacity-50 pointer-events-none', 9 shortcut: 'ml-auto text-xs text-muted-foreground', 10 } 11 12 // inside Item 13 <button 14 disabled={disabled} 15 className={cn( 16 styles.item, 17 destructive && styles.destructive, 18 disabled && styles.disabled, 19 className 20 )} 21 onClick={() => { 22 onClick?.() 23 setOpen(false) 24 }} 25 > 26 {Icon && (React.isValidElement(Icon) ? Icon : <Icon className="w-4 h-4" />)} 27 {children} 28 {shortcut && <span className={styles.shortcut}>{shortcut}</span>} 29 </button>
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 source imports React, the ChevronRight icon from lucide-react, and the local cn utility — nothing else. State lives in a small Context provider, the menu is a fixed-positioned div placed at the cursor coordinates, and submenus open via the group-hover/sub CSS selector. shadcn/ui's Context Menu sits on top of @radix-ui/react-context-menu, which adds roughly twenty-five kilobytes of vendor runtime per palette. Drivn skips that cost entirely.
Not yet. The current implementation closes on Escape and on any mousedown outside the menu, but it does not move focus through items with arrow keys. shadcn/ui inherits full arrow-key navigation, Home, End, and submenu open/close shortcuts from Radix. If keyboard parity with native context menus is a hard requirement, the shadcn/Radix variant is the better fit. For mouse-driven app shells where the right-click menu is a quick-action affordance, the Drivn model is enough and ships in roughly two hundred lines you can read end to end.
The ContextMenuRoot component listens for onContextMenu, calls e.preventDefault() to suppress the browser default, then stores { x: e.clientX, y: e.clientY } in state. The Content component reads that position and applies it via style={{ left: pos.x, top: pos.y }} on a fixed-positioned div. There is no collision detection — if the menu would clip the viewport edge, you place the trigger where it has room or override the position via the className prop. Radix handles edge-clipping automatically; Drivn keeps the implementation simple and trusts the layout.
Not in the current Context Menu compound. Drivn provides Item, Sub, SubTrigger, SubContent, Group, Label, and Separator — the surface most right-click menus actually use. shadcn/ui exports ContextMenuCheckboxItem, ContextMenuRadioGroup, and ContextMenuRadioItem because Radix exposes the primitives. If you need toggle items inside a context menu, render Drivn's Checkbox or Radio Group inside an Item and handle the state at the parent — the file is short enough that extending it for a custom variant is a small change.