Skip to content
Drivn logoDrivn
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.

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

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

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.

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.