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.
Drivn's Kbd and shadcn/ui's Kbd both render a native HTML <kbd> element styled like a physical keyboard key — a small pill with font-mono text, a muted background, and a fixed h-5 height that sits inline with body copy. Neither component pulls in a runtime UI library; both are roughly thirty lines of TypeScript that compose a cn() className over the native element. The two split on three details: how variants are exposed, how the grouping primitive is exported, and how the component handles icon children like a Command-key glyph from lucide-react.
Drivn ships a variant prop typed as keyof typeof styles.variants — default (filled with bg-muted text-muted-foreground) and outline (a hairline border via border border-border text-muted-foreground). The grouping primitive is exposed through Object.assign(KbdRoot, { Group }), which gives you Kbd.Group dot notation at the call site so a Command + Shift + P chord reads as one import with two members. shadcn's Kbd ships a single visual recipe without a variant prop and exports KbdGroup as a sibling component rather than a static property, so you import both names separately. Both libraries land on the same end result for the simplest case — a styled <kbd> next to a sentence — but Drivn's call site stays in one import line.
Drivn's styles.base also includes a precise SVG sizing rule — [&_svg:not([class*=size-])]:size-3 — that auto-sizes any unsized Lucide icon child to twelve pixels. Drop <Command /> inside a Drivn Kbd and the icon fits the pill without manual className. This page walks through every other difference: the variant API, the group export, the icon child handling, and how each library reaches a Mac-style ⌘K chord at the Command bar.
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
The Drivn Kbd accepts three props: variant (default or outline), className, and children. The variant prop is typed as keyof typeof styles.variants, so the editor autocompletes the two valid strings and a typo is a compile error. shadcn's Kbd takes className and children only — there is no variant prop because the single recipe handles the one visual style they ship. Both components forward the standard HTML attributes through the native <kbd> element, so title, id, aria-label, and data-* work on either side without a type override.
The Drivn call site reads <Kbd variant="outline">Esc</Kbd> and you get a bordered key. The shadcn call site reads <Kbd>Esc</Kbd> and you get the filled recipe. For grouping, Drivn writes <Kbd.Group><Kbd>⌘</Kbd><Kbd>K</Kbd></Kbd.Group> from a single import, while shadcn writes <KbdGroup><Kbd>⌘</Kbd><Kbd>K</Kbd></KbdGroup> from two imports. Both render the same DOM. See the full Kbd reference for every Drivn prop.
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 about thirty 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.

