Skip to content
Drivn
4 min read

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.

Both Drivn and shadcn/ui build their command menu on cmdk, so the hard parts — fuzzy matching, arrow-key traversal, ARIA roles, and keeping keystrokes responsive across hundreds of rows — behave identically in either library. The comparison is not about search quality. It is about the layer you actually write against: how many symbols land in your import statement, what the modal wrapper drags into the bundle, and whether the Cmd+K binding is code you own or code that ships inside the component.

shadcn spreads the palette across nine named exports — Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, CommandShortcut, and CommandDialog — so a moderately complete menu opens with a six-line import block. Drivn attaches every part to one namespace with Object.assign, which turns that block into a single import { Command } line and every part into dot access: Command.Input, Command.List, Command.Group, Command.Item, Command.Dialog. Prop types narrow per subcomponent, so you never reach for generics at the call site.

The modal path is where the two genuinely part ways. shadcn's CommandDialog wraps cmdk in Radix Dialog and leaves the keyboard binding to you. Drivn's Command.Dialog registers the keydown listener itself and wraps the same cmdk primitive in Drivn's zero-runtime-deps Dialog. Same palette on screen — one less vendor runtime in the bundle and one less useEffect in your codebase.

Side-by-side comparison

FeatureDrivnshadcn/ui
Underlying librarycmdkcmdk
API styleDot notation (Command.Input)Named exports (CommandInput)
Cmd+K shortcutBuilt into Command.DialogImplement manually
Dialog wrapperDrivn Dialog (no Radix)Radix Dialog
Empty fallback"No results found." defaultPass children manually
Loading state<Command.Loading><CommandLoading>
Group headingsheading propheading prop
Keyboard navigationcmdk baselinecmdk baseline
LicenseMITMIT
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
2import {
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
22import { 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.

1React.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.

1function 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

Rendered side by side the two palettes look nearly the same, because both lean on cmdk's data attributes rather than inventing their own state classes. cmdk stamps data-selected="true" on the active row and data-disabled="true" on blocked ones; Drivn reads those with data-[selected=true]:bg-accent, data-[selected=true]:text-accent-foreground, and data-[disabled=true]:opacity-50. Group headings are styled through cmdk's [cmdk-group-heading] selector rather than a wrapper element — [&_[cmdk-group-heading]]:px-2 through [&_[cmdk-group-heading]]:text-muted-foreground produce the small muted label without adding a component to the tree. Token choice is the one visible divergence: Drivn's root is bg-card border border-border rounded-[10px], while shadcn reaches for bg-popover and a bare border. Either way these resolve to CSS variables you set once during installation, so re-theming a palette is a variable edit, not a component edit. Drivn also fills in two defaults at the component level — Command.Empty falls back to "No results found." when you pass no children, and Command.Shortcut renders a real <kbd> styled ml-auto text-xs tracking-widest.

1const 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}
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. 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.