Drivn vs shadcn/ui — Kbd Component Compared
Compare Drivn Kbd vs shadcn/ui — typed variants and Kbd.Group dot-notation versus shadcn's KbdGroup export. Both are zero-runtime-dep keyboard key wrappers.
Searching for a keyboard-shortcut hint component usually ends in the same place: a small pill that renders ⌘, Ctrl, or Esc inline with body copy. Drivn's Kbd and shadcn/ui's Kbd both solve it with the native HTML <kbd> element and a cn() className — no Radix, no runtime UI package, no portal, no context. Install either one and you own a single file that renders a single tag. The differences are narrower than a typical library comparison, and all of them sit in the API surface rather than the rendered output.
First, variants. Drivn types a variant prop as keyof typeof styles.variants, which gives you default (bg-muted text-muted-foreground) and outline (border border-border text-muted-foreground) with editor autocomplete on both names. shadcn ships one visual recipe and no variant prop at all. Second, grouping. Drivn attaches the chord wrapper via Object.assign(KbdRoot, { Group }), so Kbd.Group arrives from the same import as Kbd; shadcn exports KbdGroup under a separate name. Third, icon children. Drivn's styles.base carries [&_svg:not([class*=size-])]:size-3, which sizes an unstyled Lucide glyph to twelve pixels with nothing added at the call site.
What follows is the prop signature comparison, the styles object, the icon rule, the group export, and where each library is the better pick for a ⌘K hint next to a Command palette.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying primitive | Native <kbd> + cn() | Native <kbd> + cn() |
| Runtime UI dependencies | Zero | Zero |
| Variant prop | default | outline | None (single recipe) |
| Group primitive | Kbd.Group (dot notation) | KbdGroup (separate export) |
| Default height | h-5 (20px) | h-5 (20px) |
| Default radius | rounded-sm | rounded |
| Default font size | text-xs | text-[10px] |
| Default background | bg-muted text-muted-foreground | bg-muted text-muted-foreground |
| Outline variant | ||
| Auto-sized icon children | [&_svg:not([class*=size-])]:size-3 | Manual className on icon |
| select-none + pointer-events-none | ||
| Styles object pattern | styles.base + styles.variants | Inline cn() inside JSX |
| License | MIT | MIT |
| Copy-paste install |
API side-by-side
Drivn's KbdProps interface declares exactly three members: variant, className, and children. There is no ...props spread onto the <kbd> element, so arbitrary HTML attributes such as title or data-state are not forwarded by default — if a page needs them, add the spread to your local copy, which is a one-line edit because the file lives in your repo after npx drivn add kbd. The variant prop is typed as keyof typeof styles.variants, so a misspelled variant is a compile error rather than a silently unstyled key. shadcn's Kbd takes className plus the standard <kbd> attributes and exposes no variant prop, since the single recipe covers the one look they ship.
At the call site the gap is one word and one import. Drivn reads <Kbd variant="outline">Esc</Kbd>, with Kbd.Group already available from the same import. shadcn reads <Kbd>Esc</Kbd> and pulls KbdGroup in as a second name. Both render the same DOM. The full Kbd reference lists every prop, and the Kbd examples page shows the chord patterns in context.
1 // Drivn — variant prop + Kbd.Group dot notation 2 import { Kbd } from '@/components/ui/kbd' 3 4 export default function Shortcut() { 5 return ( 6 <Kbd.Group> 7 <Kbd variant="outline">Ctrl</Kbd> 8 <Kbd variant="outline">Shift</Kbd> 9 <Kbd variant="outline">P</Kbd> 10 </Kbd.Group> 11 ) 12 } 13 14 // shadcn/ui — separate KbdGroup import, no variant prop 15 import { Kbd, KbdGroup } from '@/components/ui/kbd' 16 17 export default function Shortcut() { 18 return ( 19 <KbdGroup> 20 <Kbd>Ctrl</Kbd> 21 <Kbd>Shift</Kbd> 22 <Kbd>P</Kbd> 23 </KbdGroup> 24 ) 25 }
Variants and the styles object
Drivn lifts the className construction into a styles constant above the component. The base key holds the layout, font, and behavior rules; the variants key holds the two visual styles; the group key holds the inline-flex layout for chord wrappers. Switching from the filled to the outline look at install time is a one-line edit to a labeled object — default flips to outline — and the autocomplete on variant={...} updates automatically because the prop type is derived from the object's keys.
shadcn's Kbd composes the same utility classes inside a single inline cn() call inside the JSX return. Adding a second visual style means either threading a variant prop and a cva-style helper through the component (shadcn's usual pattern but not present in their Kbd recipe), or duplicating the file. Drivn's approach trades a small amount of file size for a clearer edit surface: every visual decision is one line in a labeled config block rather than spread across a 300-character className string. See Drivn vs shadcn Button for the same pattern at a larger scale.
1 // Drivn — styles object lifted above the component (verbatim) 2 const styles = { 3 base: cn( 4 'pointer-events-none inline-flex items-center', 5 'justify-center select-none', 6 'h-5 w-fit min-w-5 gap-1 rounded-sm px-1', 7 'font-mono text-xs font-medium', 8 '[&_svg:not([class*=size-])]:size-3' 9 ), 10 variants: { 11 default: 'bg-muted text-muted-foreground', 12 outline: 'border border-border text-muted-foreground', 13 }, 14 group: 'inline-flex items-center gap-1', 15 }
Icon children — the SVG auto-sizing rule
The Drivn Kbd ships one detail that shadcn's default recipe does not: the selector [&_svg:not([class*=size-])]:size-3 inside styles.base. The rule reads as "for every descendant <svg> that does not already have a size-* class, set its size to twelve pixels." Drop a Lucide <Command /> inside a Drivn Kbd and it renders at twelve pixels automatically — no className="h-3 w-3", no wrapper, no manual prop on the icon component. The rule is opt-out: pass <Command className="size-4" /> and the explicit class wins because the not() matcher excludes any element that already carries a size- token.
shadcn's recipe handles this through documentation rather than CSS — their examples set the size on the icon directly. Both approaches work; Drivn's call site is shorter for the common Mac shortcut case where <Command />, <Option />, and <Shift /> glyphs sit inside Kbd elements next to body text. Pair this with the Command component to render a ⌘K palette trigger.
1 // Drivn — Command icon auto-sizes to 12px inside the Kbd 2 import { Kbd } from '@/components/ui/kbd' 3 import { Command } from 'lucide-react' 4 5 export default function Trigger() { 6 return ( 7 <Kbd.Group> 8 <Kbd><Command /></Kbd> 9 <Kbd>K</Kbd> 10 </Kbd.Group> 11 ) 12 }
Group export — dot notation vs separate name
Drivn's grouping primitive is attached to the root component via Object.assign(KbdRoot, { Group }). The export ends up at Kbd.Group, which keeps the call site as one import line — import { Kbd } from '@/components/ui/kbd' — and a clear parent-child read of the DOM. The pattern matches the rest of the Drivn surface: Dialog.Content, Card.Preview, Accordion.Item. The dot notation reads as one cohesive component family rather than a flat namespace of similarly-named primitives.
shadcn's Kbd exports KbdGroup as a sibling named export, so the call site imports both names from the same module. Both approaches are equivalent at the type level — TypeScript handles the static property and the named export with the same inference — but the call site differs. If your codebase already uses Drivn primitives like Dialog and Card, the dot notation keeps the Kbd consistent with the rest of the surface.
When each wins
Pick shadcn/ui's Kbd when you want the absolute smallest possible file — a single recipe with no variant logic and no static-property trickery. The result is one className string and one component function, which is the right amount of code for a tiny element you may not customize. If your codebase already uses shadcn's other primitives and you import everything as flat named exports, the Kbd lines up with the rest of the project.
Pick Drivn's Kbd when you want the outline variant out of the box, the Kbd.Group dot-notation API that matches the rest of the Drivn family, and the auto-sized icon-child rule for Lucide glyphs. The component is still under sixty lines after install, and the styles object keeps every visual decision in one labeled block instead of a long inline string. For the Command palette trigger pattern where ⌘K renders next to a search bar at the top of the Sidebar, the auto-icon-sizing rule alone pays for the rest of the surface.
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
Yes. Both components render a single native <kbd> element with a className applied via cn(). Drivn lifts the className construction into a styles.base constant and a styles.variants map keyed by the variant prop, while shadcn assembles the same utilities inline inside the JSX return. The output HTML is one <kbd> tag in either library, so screen readers and CSS selectors behave identically. The visible difference is the outline variant, which only Drivn ships out of the box, and the text-xs versus text-[10px] default font size.
Import Kbd from @/components/ui/kbd and the Command icon from lucide-react, then nest two <Kbd> elements inside a <Kbd.Group>. Drop <Command /> inside the first Kbd as the child — Drivn's styles.base includes [&_svg:not([class*=size-])]:size-3 so the icon auto-sizes to twelve pixels without any extra className. The second Kbd holds the letter "K". The full pattern is one import, eight lines of JSX, and pairs with the Command palette to trigger search on ⌘K.
Yes — the file lives in your repo after npx drivn add kbd, so you own it. Add a new key to styles.variants (for example, solid: 'bg-foreground text-background'), and the variant prop type updates automatically because it is typed as keyof typeof styles.variants. The autocomplete in your editor surfaces the new option immediately. shadcn's default Kbd has no variant prop, so adding one there means rewiring the component signature; in Drivn it is a one-line edit to a labeled config object.
Drivn attaches the Group primitive to the root component with Object.assign(KbdRoot, { Group }), which surfaces it as Kbd.Group at the call site. The pattern matches the rest of the Drivn surface — Dialog.Content, Card.Preview, Accordion.Item, Tabs.List — so component families read as one cohesive unit rather than a flat namespace of similarly-named primitives. Both approaches produce the same DOM and the same TypeScript inference; the difference is purely how the call site reads when scanning a file.
Yes. The Kbd file has no "use client" directive because it uses no client-only APIs — no useState, no useEffect, no event handlers. The component is a pure render function over the native <kbd> element. You can drop it inside a Server Component to render shortcut hints in a static layout, or inside a Client Component when the surrounding palette needs interactivity. Next.js draws the client boundary based on the consumer file, not the Kbd itself, so the same file works in both rendering modes without configuration.