Next.js Kbd for Keyboard Shortcut Hints
Add a Next.js keyboard-key hint that renders in Server Components with zero client JS. Drivn ships one Kbd file, dot-notation groups, no runtime deps.
A <kbd> element renders a keyboard key inline — the ⌘K hint in a command menu, the Ctrl Shift P combo in a docs page, the Esc badge on a dialog. It carries no behavior of its own; it is a styled label that tells the reader which keys to press. In the Next.js App Router that makes it unusually simple: a key hint is pure markup with no state, so it can render entirely on the server and ship zero client JavaScript. The keyboard listener that actually acts on the shortcut is a separate concern that does need the client — and keeping the two apart is what keeps the page fast.
Drivn's Kbd is built for exactly that. After install it lives in src/components/ui/kbd.tsx as two small functions — Kbd for a single key and Kbd.Group for a combo — with every class held in one styles object. It is not marked 'use client': there are no hooks, no effects, no state, so you can drop it straight into a server-rendered page or layout. The element is pointer-events-none by design, because a key hint is read, never clicked.
This guide installs Drivn in a Next.js 16 project, renders a key hint in a Server Component, composes a ⌘K command-menu badge, wires a real keyboard shortcut in a client island, and restyles the one styles object every key reads from. Every snippet comes from the component's real API.
Install in a Next.js 16 project
Drivn installs through a small CLI that writes the component source directly into your repository — there is no runtime npm package to version-lock. From the root of your Next.js 16 project run npx drivn add kbd. The Kbd has no component dependencies and imports no icons itself, so the CLI writes a single kbd.tsx file and nothing else. The CLI reference documents every flag, including targeting a custom directory or installing several components at once. After install you own the file; future Drivn releases will not overwrite it. Commit the change for a clean baseline before you compose it into hints and shortcuts, and confirm the file landed under your UI directory in the editor.
1 # from the root of your Next.js 16 project 2 npx drivn add kbd
Render in a Server Component
Because the Kbd holds no state and runs no effect, its source has no 'use client' directive — you can import and render it inside a server component with zero client JavaScript shipped for the hint. A single key is <Kbd>Esc</Kbd>; the component wraps your text in a native <kbd> element styled to a compact h-5 min-w-5 pill with a monospace label. The variant prop switches between default (a filled bg-muted chip) and outline (a bordered chip that reads lighter against dense UI). Set it to match the surface — filled inside a card, outline against a busy toolbar. Because the element is pointer-events-none, it never intercepts a click meant for the control behind it, so you can place a hint inside a button or menu row safely. See the installation guide for project setup.
1 // app/page.tsx — a Server Component, no 'use client' 2 import { Kbd } from '@/components/ui/kbd' 3 4 export default function Page() { 5 return ( 6 <p className="text-sm text-muted-foreground"> 7 Press <Kbd>Esc</Kbd> to close, <Kbd variant="outline">Enter</Kbd> to confirm. 8 </p> 9 ) 10 }
Wire a real keyboard shortcut in a client island
The Kbd renders the hint, but it never listens for keystrokes — pointer-events-none and no event handlers means the shortcut logic is yours to add, and that part does need the client. Put a small 'use client' component next to the badge, register a keydown listener in a useEffect, and act when the combo fires. The pattern below opens a menu on ⌘K / Ctrl+K and renders the same Kbd.Group badge as its trigger label, so the visual hint and the real binding live in one island while the page around it stays on the server. Keep the listener component small — only the interactive part hydrates, and the server still streams the rest of the route. This clean split is the whole reason Kbd ships without a client boundary.
1 'use client' 2 import { useEffect, useState } from 'react' 3 import { Kbd } from '@/components/ui/kbd' 4 import { Command } from 'lucide-react' 5 6 export function CommandTrigger() { 7 const [open, setOpen] = useState(false) 8 useEffect(() => { 9 function onKey(e: KeyboardEvent) { 10 if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { 11 e.preventDefault() 12 setOpen((v) => !v) 13 } 14 } 15 document.addEventListener('keydown', onKey) 16 return () => document.removeEventListener('keydown', onKey) 17 }, []) 18 return ( 19 <button onClick={() => setOpen(true)}> 20 Search 21 <Kbd.Group> 22 <Kbd><Command /></Kbd> 23 <Kbd>K</Kbd> 24 </Kbd.Group> 25 </button> 26 ) 27 }
Customize the one styles object
Every class the Kbd renders lives in a single styles object at the top of the file you own after install. base sets the shared shape — pointer-events-none inline-flex items-center, the h-5 w-fit min-w-5 gap-1 rounded-sm px-1 box, a font-mono text-xs font-medium label, and the [&_svg:not([class*=size-])]:size-3 rule that sizes child icons. variants holds the two looks: default is bg-muted text-muted-foreground, outline is border border-border text-muted-foreground. group is the inline-flex items-center gap-1 wrapper Kbd.Group uses. Edit any of them once and every key hint in your app updates, and because the colors read from your Tailwind tokens, changing them in theming re-themes the hints for dark and light mode automatically. The styles object below is copied verbatim from kbd.ts.
1 const styles = { 2 base: cn( 3 'pointer-events-none inline-flex items-center', 4 'justify-center select-none', 5 'h-5 w-fit min-w-5 gap-1 rounded-sm px-1', 6 'font-mono text-xs font-medium', 7 '[&_svg:not([class*=size-])]:size-3' 8 ), 9 variants: { 10 default: 'bg-muted text-muted-foreground', 11 outline: 'border border-border text-muted-foreground', 12 }, 13 group: 'inline-flex items-center gap-1', 14 }
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 — the Kbd is one of the Drivn components that is not a client component. Its source has no 'use client' directive because it holds no state and runs no effect; it is a styled native <kbd> element. You can render a key hint directly inside a server-rendered page or layout and it ships zero client JavaScript. You only need a client boundary for the code that listens for the keystroke, which is a separate component you write yourself.
The Kbd is display-only — it is pointer-events-none and has no event handlers, so it never reacts to keys. Add a small 'use client' component that registers a keydown listener in a useEffect, checks for your combo (for example e.key === 'k' && (e.metaKey || e.ctrlKey)), and runs your action. Render the Kbd.Group badge inside that same island as the visible hint while the listener does the work.
Yes. Pass a Lucide component such as Command as the child of a Kbd, and the base class [&_svg:not([class*=size-])]:size-3 sizes the SVG to match the text automatically. This is how the ⌘K badge is built — a Kbd holding the Command icon next to a Kbd holding K, both wrapped in Kbd.Group. No manual width or height classes are needed on the icon.
The default variant is a filled chip (bg-muted text-muted-foreground) that reads clearly on plain backgrounds like a card or a paragraph. The outline variant swaps the fill for a border (border border-border text-muted-foreground) so it stays legible against dense or busy surfaces like a toolbar. Both share the same box and monospace label; pick per placement, and change either in the styles.variants map in the file you own.

