Components / Display
Kbd Displays a keyboard key or shortcut as a keycap. Two sizes, icon support, and a KbdGroup for combinations and sequences. Built on React Aria's Keyboard, so inside a menu item it's linked to the item as its shortcut description.
⌘ ⇧ ⌥ ⌃ Press⌘ K to open the command palette
import { Kbd, KbdGroup } from "@/components/ui/kbd" ;
export default function KbdDemo () {
return (
< div className = "flex flex-col items-center gap-4 text-muted-foreground text-sm" >
< KbdGroup >
< Kbd >⌘</ Kbd >
< Kbd >⇧</ Kbd >
< Kbd >⌥</ Kbd >
< Kbd >⌃</ Kbd >
</ KbdGroup >
< p className = "flex items-center gap-1.5" >
Press
< KbdGroup >
< Kbd >⌘</ Kbd >
< Kbd >K</ Kbd >
</ KbdGroup >
to open the command palette
</ p >
</ div >
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/kbd
The CLI installs dependencies and any other components this one uses.
import { Kbd, KbdGroup } from "@/components/ui/kbd" ;
< KbdGroup >
< Kbd >⌘</ Kbd >
< Kbd >K</ Kbd >
</ KbdGroup >
Kbd displays shortcuts, it doesn't bind them
Kbd only renders the keycap. Register the actual shortcut with your own
keydown listener or hotkey library, and keep the two in sync.
Kbd: to show which key or combination triggers an action, in help text, tooltips, menus, buttons and shortcut references.
MenuShortcut : the menu's own unstyled, right-aligned shortcut text. Use Kbd in a menu item when you want keycaps instead.
Badge : for labels and counts that aren't keys.
Use the platform's symbols (⌘ ⇧ ⌥ ⌃ on macOS) or words (Ctrl, Shift, Alt) consistently, and switch between them per platform if your audience is mixed.
< KbdGroup > { /* inline row of keys */ }
< Kbd >⌘</ Kbd > { /* one keycap */ }
< Kbd >
< Icon /> { /* optional, auto-sized to 12px */ }
Enter
</ Kbd >
</ KbdGroup >
Part Renders Notes Kbd<kbd dir="ltr">React Aria Keyboard. A keycap with a thicker bottom border. Not selectable and ignores pointer events. KbdGroup<span>Inline flex row with a 4px gap between keys and separators. Icon <svg>Any svg without a size-* class is sized to 12px.
md (22px tall) is the default for body text and help content. sm (18px) fits inside buttons, tooltips, menu items and inputs.
Esc sm · 18pxEsc md · 22px
import { Kbd } from "@/components/ui/kbd" ;
export default function KbdSizes () {
return (
< div className = "flex items-center gap-6 text-muted-foreground text-sm" >
< span className = "flex items-center gap-2" >
< Kbd size = "sm" >Esc</ Kbd > sm · 18px
</ span >
< span className = "flex items-center gap-2" >
< Kbd size = "md" >Esc</ Kbd > md · 22px
</ span >
</ div >
);
}
Put one Kbd per key in a KbdGroup for combinations. Add a "+" between keys when your UI uses word labels (Ctrl, Shift), and "then" for sequences such as G then I . A single Kbd with several symbols (⌘⇧P) is more compact for tight spaces.
Combination:Ctrl + Shift + P
Sequence:G then I
Compact:⌘⇧P
import { Kbd, KbdGroup } from "@/components/ui/kbd" ;
export default function KbdGroupExample () {
return (
< div className = "flex flex-col items-start gap-3 text-muted-foreground text-sm" >
< p className = "flex items-center gap-2" >
Combination:
< KbdGroup >
< Kbd >Ctrl</ Kbd >
< span aria-hidden >+</ span >
< Kbd >Shift</ Kbd >
< span aria-hidden >+</ span >
< Kbd >P</ Kbd >
</ KbdGroup >
</ p >
< p className = "flex items-center gap-2" >
Sequence:
< KbdGroup >
< Kbd >G</ Kbd >
< span >then</ span >
< Kbd >I</ Kbd >
</ KbdGroup >
</ p >
< p className = "flex items-center gap-2" >
Compact:
< Kbd >⌘⇧P</ Kbd >
</ p >
</ div >
);
}
Icons are sized automatically. Pair icon-only keys with visually hidden text (sr-only) so the key's name is read out; symbol glyphs like ⌘ may be read inconsistently by screen readers.
Command K Up arrow Down arrow EnterBackspace
import {
ArrowDownIcon,
ArrowUpIcon,
CommandIcon,
CornerDownLeftIcon,
DeleteIcon,
} from "lucide-react" ;
import { Kbd, KbdGroup } from "@/components/ui/kbd" ;
export default function KbdWithIcon () {
return (
< div className = "flex flex-wrap items-center gap-4 text-muted-foreground text-sm" >
< KbdGroup >
< Kbd >
< CommandIcon aria-hidden />
< span className = "sr-only" >Command</ span >
</ Kbd >
< Kbd >K</ Kbd >
</ KbdGroup >
< KbdGroup >
< Kbd >
< ArrowUpIcon aria-hidden />
< span className = "sr-only" >Up arrow</ span >
</ Kbd >
< Kbd >
< ArrowDownIcon aria-hidden />
< span className = "sr-only" >Down arrow</ span >
</ Kbd >
</ KbdGroup >
< Kbd >
< CornerDownLeftIcon aria-hidden /> Enter
</ Kbd >
< Kbd >
< DeleteIcon aria-hidden />
< span className = "sr-only" >Backspace</ span >
</ Kbd >
</ div >
);
}
Show the shortcut next to the action's name in a Tooltip . The light tooltip variant keeps the keycap colors readable; on the default dark tooltip, override the keycap colors with a class.
import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react" ;
import { Button } from "@/components/ui/button" ;
import { Kbd, KbdGroup } from "@/components/ui/kbd" ;
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip" ;
const actions = [
{ label: "Bold" , icon: BoldIcon, key: "B" },
{ label: "Italic" , icon: ItalicIcon, key: "I" },
{ label: "Underline" , icon: UnderlineIcon, key: "U" },
];
export default function KbdInTooltip () {
return (
< div className = "flex items-center gap-1" >
{actions. map (( a ) => (
< TooltipTrigger key = {a.label}>
< Button variant = "ghost" size = "icon-sm" aria-label = {a.label}>
< a.icon />
</ Button >
< Tooltip variant = "light" className = "flex items-center gap-2" >
{a.label}
< KbdGroup >
< Kbd size = "sm" >⌘</ Kbd >
< Kbd size = "sm" >{a.key}</ Kbd >
</ KbdGroup >
</ Tooltip >
</ TooltipTrigger >
))}
</ div >
);
}
Inside a React Aria MenuItem, Kbd picks up the item's keyboard-shortcut slot: it gets an id and the item references it with aria-describedby, so screen readers announce the shortcut with the item. Add ml-auto to push it to the right.
File
import { CopyIcon, FilePlusIcon, SaveIcon, Trash2Icon } from "lucide-react" ;
import { Button } from "@/components/ui/button" ;
import { Kbd } from "@/components/ui/kbd" ;
import {
MenuContent,
MenuItem,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu" ;
export default function KbdInMenu () {
return (
< MenuTrigger >
< Button variant = "outline" >File</ Button >
< MenuContent className = "w-56" >
< MenuItem textValue = "New file" >
< FilePlusIcon /> New file
< Kbd size = "sm" className = "ml-auto" >
⌘N
</ Kbd >
</ MenuItem >
< MenuItem textValue = "Save" >
< SaveIcon /> Save
< Kbd size = "sm" className = "ml-auto" >
⌘S
</ Kbd >
</ MenuItem >
< MenuItem textValue = "Duplicate" >
< CopyIcon /> Duplicate
< Kbd size = "sm" className = "ml-auto" >
⌘D
</ Kbd >
</ MenuItem >
< MenuSeparator />
< MenuItem textValue = "Delete" variant = "destructive" >
< Trash2Icon /> Delete
< Kbd size = "sm" className = "ml-auto" >
⌫
</ Kbd >
</ MenuItem >
</ MenuContent >
</ MenuTrigger >
);
}
A two-column cheat sheet: a description list per section, with combinations and sequences.
Navigation
Open command palette ⌘ K
Go to inbox G then I
Go to projects G then P Editing
New issue C
Assign to me I
Save and close ⌘ ↵
import { Kbd, KbdGroup } from "@/components/ui/kbd" ;
const groups = [
{
title: "Navigation" ,
shortcuts: [
{ label: "Open command palette" , keys: [ "⌘" , "K" ] },
{ label: "Go to inbox" , keys: [ "G" , "I" ], sequence: true },
{ label: "Go to projects" , keys: [ "G" , "P" ], sequence: true },
],
},
{
title: "Editing" ,
shortcuts: [
{ label: "New issue" , keys: [ "C" ] },
{ label: "Assign to me" , keys: [ "I" ] },
{ label: "Save and close" , keys: [ "⌘" , "↵" ] },
],
},
];
export default function KbdRecipeShortcuts () {
return (
< div className = "grid w-full max-w-lg gap-6 sm:grid-cols-2" >
{groups. map (( g ) => (
< section key = {g.title} aria-labelledby = { `shortcuts-${ g . title }` }>
< h3
id = { `shortcuts-${ g . title }` }
className = "mb-2 font-medium text-muted-foreground text-xs uppercase tracking-wide"
>
{g.title}
</ h3 >
< dl className = "flex flex-col divide-y" >
{g.shortcuts. map (( s ) => (
< div
key = {s.label}
className = "flex items-center justify-between gap-4 py-2 text-sm"
>
< dt >{s.label}</ dt >
< dd >
< KbdGroup >
{s.keys. map (( k , i ) => (
< span key = {k} className = "inline-flex items-center gap-1" >
{s.sequence && i > 0 && (
< span className = "text-muted-foreground text-xs" >
then
</ span >
)}
< Kbd size = "sm" >{k}</ Kbd >
</ span >
))}
</ KbdGroup >
</ dd >
</ div >
))}
</ dl >
</ section >
))}
</ div >
);
}
A search button that advertises ⌘ K and a window keydown listener that handles both ⌘K and Ctrl+K.
Search docs…⌘ K Press ⌘K or Ctrl+K anywhere on the page.
import { SearchIcon } from "lucide-react" ;
import { useEffect, useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import { Kbd, KbdGroup } from "@/components/ui/kbd" ;
export default function KbdRecipeSearchTrigger () {
const [ opened , setOpened ] = useState ( 0 );
useEffect (() => {
const onKeyDown = ( e : KeyboardEvent ) => {
if (e.key. toLowerCase () === "k" && (e.metaKey || e.ctrlKey)) {
e. preventDefault ();
setOpened (( n ) => n + 1 );
}
};
window. addEventListener ( "keydown" , onKeyDown);
return () => window. removeEventListener ( "keydown" , onKeyDown);
}, []);
return (
< div className = "flex w-full max-w-xs flex-col gap-2" >
< Button
variant = "outline"
className = "w-full justify-start text-muted-foreground"
onPress = {() => setOpened (( n ) => n + 1 )}
>
< SearchIcon /> Search docs…
< KbdGroup className = "ml-auto" >
< Kbd size = "sm" >⌘</ Kbd >
< Kbd size = "sm" >K</ Kbd >
</ KbdGroup >
</ Button >
< p
className = "text-center text-muted-foreground text-xs"
aria-live = "polite"
>
{opened === 0
? "Press ⌘K or Ctrl+K anywhere on the page."
: `Search opened ${ opened } ${ opened === 1 ? "time" : "times"}.` }
</ p >
</ div >
);
}
Kbd renders the semantic <kbd> element with dir="ltr", so key combinations keep their order in right-to-left layouts.
Screen readers read the text inside. Symbols like ⌘, ⇧ and ⌥ are read inconsistently across screen readers (by Unicode name, or skipped); include readable text (<span className="sr-only">Command</span>) where the shortcut matters, or use words.
Inside a menu item, the shortcut is exposed as the item's description via aria-describedby. Elsewhere, it's plain inline text.
Decorative separators such as "+" can be hidden with aria-hidden so the combination reads as the key names only.
A shortcut must never be the only way to reach an action. Keep a visible, clickable control.
Attribute On Present when data-slot="kbd"KbdAlways data-slot="kbd-group"KbdGroupAlways
The keycap uses bg-muted, text-muted-foreground and a border-b-2 bottom edge. On a colored surface, such as a solid button, override them:
< Button >
Publish
< Kbd size = "sm" className = "border-white/20 bg-white/10 text-inherit" >⌘↵</ Kbd >
</ Button >
Also accepts every prop of React Aria's Keyboard (the props of <kbd>).
No extra props. Accepts every prop of <span>.
PreviousItem Next Chart