Drivn vs shadcn/ui — Command Component Compared
Side-by-side comparison of Drivn and shadcn/ui React Command components — cmdk base, dot notation API, built-in Cmd+K shortcut, and Dialog wrapper.
Drivn and shadcn/ui both wrap cmdk for their command menu — the same primitive that powers the search palettes you find in editor-style apps. cmdk handles fuzzy search filtering, keyboard navigation, ARIA roles, and the rendering tricks that keep typing instant on hundreds of items. What differs between Drivn and shadcn is everything that sits on top: the API shape, the dialog wrapper, and how the Cmd+K shortcut gets wired.
shadcn ships nine named exports — Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, CommandShortcut, and CommandDialog — each as a separate line at the top of your file. Drivn collapses them into a single dot-notation namespace: Command, Command.Input, Command.List, Command.Group, Command.Item, plus Command.Dialog for the modal variant. One import covers every subcomponent and TypeScript narrows each prop set automatically without you reaching for generics at the call site.
The dialog wrapper diverges further. shadcn's CommandDialog re-exports Dialog from Radix UI and leaves the Cmd+K keyboard listener for you to write. Drivn's Command.Dialog already includes the listener and wraps the cmdk primitive in Drivn's zero-runtime-deps Dialog primitive. The result is fewer imports, less per-palette boilerplate, and one fewer dependency in your bundle when Radix is not used elsewhere in the project.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying library | cmdk | cmdk |
| API style | Dot notation (Command.Input) | Named exports (CommandInput) |
| Cmd+K shortcut | Built into Command.Dialog | Implement manually |
| Dialog wrapper | Drivn Dialog (no Radix) | Radix Dialog |
| Empty fallback | "No results found." default | Pass children manually |
| Loading state | <Command.Loading> | <CommandLoading> |
| Group headings | heading prop | heading prop |
| Keyboard navigation | cmdk baseline | cmdk baseline |
| License | MIT | MIT |
| Copy-paste install |
API side-by-side
shadcn/ui exports each command part as its own component — you import Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, and CommandShortcut from the same file. The component tree composes as named JSX elements, which works fine but adds a long import line and forces you to remember eight related names. Drivn mounts every part on a single Command object via Object.assign, so dot access — Command.Input, Command.List, Command.Item — returns the right subcomponent and TypeScript infers the matching props without manual generics. See the Command docs for the full prop reference.
1 // shadcn/ui — named exports 2 import { 3 Command, 4 CommandInput, 5 CommandList, 6 CommandEmpty, 7 CommandGroup, 8 CommandItem, 9 } from '@/components/ui/command' 10 11 <Command> 12 <CommandInput placeholder="Type a command..." /> 13 <CommandList> 14 <CommandEmpty>No results.</CommandEmpty> 15 <CommandGroup heading="Suggestions"> 16 <CommandItem>Calendar</CommandItem> 17 </CommandGroup> 18 </CommandList> 19 </Command> 20 21 // Drivn — dot notation 22 import { Command } from '@/components/ui/command' 23 24 <Command> 25 <Command.Input placeholder="Type a command..." /> 26 <Command.List> 27 <Command.Empty /> 28 <Command.Group heading="Suggestions"> 29 <Command.Item>Calendar</Command.Item> 30 </Command.Group> 31 </Command.List> 32 </Command>
Built-in Cmd+K keyboard shortcut
Every command palette ships with the same convention — Cmd+K on macOS, Ctrl+K on Windows and Linux. Drivn's Command.Dialog includes the listener directly. The component mounts a useEffect that registers a keydown handler on document, checks for e.key === 'k' plus (e.metaKey || e.ctrlKey), and calls onOpenChange(!open) to toggle the modal. shadcn does not include this — its CommandDialog is a wrapper around the primitive, and the keyboard listener is something you write in your own client component or via a custom hook. For most apps that means one extra useEffect and one cleanup function per palette. Drivn moves the boilerplate into the component itself.
1 React.useEffect(() => { 2 const onKeyDown = (e: KeyboardEvent) => { 3 if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { 4 e.preventDefault() 5 onOpenChange(!open) 6 } 7 } 8 document.addEventListener('keydown', onKeyDown) 9 return () => { 10 document.removeEventListener('keydown', onKeyDown) 11 } 12 }, [open, onOpenChange])
Dialog wrapper — Drivn primitive vs Radix
shadcn's CommandDialog wraps the cmdk primitive in @radix-ui/react-dialog. If your project does not use Radix elsewhere, that adds roughly twenty kilobytes of additional runtime per command palette. Drivn's Command.Dialog wraps the same cmdk primitive in the Dialog component Drivn already ships, which is zero-runtime-deps — no Radix, no floating-ui — just React state, a portal, and a focus trap. Both deliver the same UX: the modal opens centered, dims the background, traps focus, and closes on Escape. The bundle cost is the practical difference, and it adds up across small projects where every kilobyte of vendor JavaScript counts.
1 function CommandDialog({ 2 open, 3 onOpenChange, 4 label, 5 className, 6 children, 7 }: { 8 open: boolean 9 onOpenChange: (open: boolean) => void 10 label?: string 11 className?: string 12 children: React.ReactNode 13 }) { 14 // ... keyboard listener above ... 15 return ( 16 <Dialog open={open} onOpenChange={onOpenChange}> 17 <Dialog.Content 18 className={cn( 19 'p-0 max-w-lg rounded-[10px]', 20 className 21 )} 22 > 23 <CommandPrimitive 24 label={label} 25 className="flex flex-col overflow-hidden" 26 > 27 {children} 28 </CommandPrimitive> 29 </Dialog.Content> 30 </Dialog> 31 ) 32 }
Styling tokens and group headings
Both libraries ship Tailwind-styled subcomponents and rely on cmdk's data attributes for state. cmdk emits data-selected="true" on the highlighted item and data-disabled="true" on disabled items, which Drivn picks up with data-[selected=true]:bg-accent and data-[disabled=true]:opacity-50. Group headings use cmdk's [cmdk-group-heading] selector — Drivn writes [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs to keep the small uppercase label feel. shadcn matches this style closely, so the rendered palette looks near-identical out of the box. Token names diverge slightly: Drivn uses bg-card and border-border on the root; shadcn uses bg-popover and border directly. Both are theme tokens you control through CSS variables in the installation layer.
1 const styles = { 2 root: cn( 3 'flex flex-col overflow-hidden', 4 'bg-card border border-border rounded-[10px]' 5 ), 6 group: cn( 7 'overflow-hidden', 8 '[&_[cmdk-group-heading]]:px-2', 9 '[&_[cmdk-group-heading]]:py-1.5', 10 '[&_[cmdk-group-heading]]:text-xs', 11 '[&_[cmdk-group-heading]]:font-medium', 12 '[&_[cmdk-group-heading]]:text-muted-foreground' 13 ), 14 item: cn( 15 'relative flex items-center gap-2 px-2 py-1.5', 16 'text-sm rounded-lg cursor-default select-none', 17 'data-[selected=true]:bg-accent', 18 'data-[selected=true]:text-accent-foreground', 19 'data-[disabled=true]:pointer-events-none', 20 'data-[disabled=true]:opacity-50' 21 ), 22 }
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 Command source imports Command as CommandPrimitive from cmdk and the local Dialog from @/components/ui/dialog. Drivn's Dialog is zero-runtime-deps — no Radix, no floating-ui — so the only runtime cost beyond your existing React install is cmdk itself. shadcn/ui uses cmdk plus Radix Dialog for its CommandDialog; Drivn skips the Radix dependency entirely, which keeps the bundle smaller for projects that do not already pull Radix in elsewhere.
Use Command.Dialog and toggle its open state — the keyboard listener is already inside the component. The useEffect listens for key === 'k' plus metaKey || ctrlKey on document and calls onOpenChange(!open). shadcn does not include the listener in its CommandDialog, so you write the same useEffect yourself in a parent component. With Drivn, you pass an open boolean and an onOpenChange setter and the shortcut works.
Yes. cmdk accepts a filter prop on the Command root with the signature (value: string, search: string) => number. Return 1 for a match, 0 for no match, or any score in between for ranked results. Drivn forwards the prop directly via React.ComponentProps<typeof CommandPrimitive>, so the cmdk filter API is unchanged. You can also disable filtering entirely with shouldFilter={false} and handle matching outside the component.
Yes — cmdk handles arrow keys, Home, End, Page Up, Page Down, and Enter natively. The selected item exposes data-selected="true" and Drivn picks that up with data-[selected=true]:bg-accent so the highlighted row tracks your theme. There is no extra setup; render Command.Item rows inside a Command.List inside a Command and the keyboard model works the moment the component mounts in the browser.