KBD
Rendering a physical key. Platform-correct glyphs, chord ordering, and never inventing a symbol.
Also called Keycap, Shortcut, Hotkey, Key — in this system all of them are KBD.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
Platform-correct glyphs
The same shortcut, twice. Detect once at startup and render the right form — a Mac user should never have to translate "Cmd".
Where shortcuts get discovered
A hint in the search field, a chord on a menu row, a shortcut in a tooltip. These three places account for almost all shortcut discovery.
In prose
A keycap in a sentence needs to sit on the text baseline without disturbing the line height, which is why the vertical padding is smaller than it looks like it should be.
Chord order and joiners
Modifiers in the platform’s printed order. A "+" between caps is optional — it helps in dense prose and adds noise in a right-aligned menu column.
Interaction states
Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.
Every part, every measurement, and the reason it is that number.
A keycap with a heavier bottom border than its sides, sized to the type around it rather than to a fixed pixel value.
- Height18–20px
Sized to sit on a 13px line without disturbing it. A keycap taller than its line makes prose leading visibly uneven.
- Min widthEqual to the height
So a single letter renders square and "Esc" renders wide. Without the minimum, "K" is a narrow sliver beside "Shift".
- Padding0 5px, 1px vertical
Deliberately tight vertically. The horizontal padding does the visual work; vertical padding pushes the cap off the baseline.
- Bottom border2px, sides 1px
The single detail that makes it read as a physical key. It is the cheapest skeuomorphism left in the system and it works.
- Type11px, 500 weight
Sans, not monospace. Monospace means "these literal characters"; a key is a physical object, and the two must not be confused.
- Chord gap4px between caps
Tight enough that a chord reads as one instruction. The gap to surrounding text is 6px, marking the chord as a unit.
- Radius5px
Slightly less than a button at the same height. Keys are harder-edged objects than controls.
Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.
Color
| Token | Value | Used for |
|---|---|---|
| --ds-surface-raised | — | Keycap fill |
| --ds-border | — | Keycap border, doubled at the bottom |
| --ds-fg-secondary | — | The key label |
| --ds-fg-disabled | — | A "+" joiner between caps |
| --ds-fg-inverse | — | Keycaps on an inverted surface, such as inside a Tooltip |
Spacing
| Token | Value | Used for |
|---|---|---|
| --space-1 | Gap between caps in a chord |
Radius
| Token | Value | Used for |
|---|---|---|
| 5px | — | Keycap corners — tighter than a button |
Typography
| Token | Value | Used for |
|---|---|---|
| font-sans | The label. Never monospace. |
Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.
| Size | Height | Padding | Label gap | Type | Min width | When to use |
|---|---|---|---|---|---|---|
| Default | 18px | 0 5px | — | 11px | 18px | In menus, tooltips and lists. |
| In prose | 1.35em | — | — | 0.85em | 1.35em | Sized relative to the surrounding text so it never disturbs the line height. |
| Large | 24px | — | — | 13px | 24px | A cheatsheet or a keyboard-shortcuts page, where the keys are the content. |
| Chord gap | — | — | 4px | — | — | Between caps. 6px to the surrounding text, marking the chord as one unit. |
| Column | — | — | — | — | 4rem | A right-aligned shortcut column in a menu, so chords of different lengths line up. |
<span role="img" aria-label="Command Shift D">Click Deploy to start the rollout.
Not a checklist to run at the end. These are the requirements the component was built from.
Contrast
- The label owes 4.5:1 at 11px — small text has no exemption, and this is some of the smallest text in the system.
- The keycap border owes 3:1: it is what makes the element read as a key rather than as emphasised text.
- On an inverted surface such as a Tooltip, the cap needs its own border and fill values. Reusing the light-surface tokens produces an invisible key.
- In forced-colors mode the fill is dropped, so the border must be a real border rather than a box-shadow.
Keyboard
| Tab | Nothing — a KBD is never focusable. It describes a key; it is not one. |
Screen readers
- Announce the chord as a phrase: "Command Shift D", not "Command, Shift, D" as three items.
- Spell out symbols. ⌘ read as an unknown glyph, or skipped entirely, is worse than "Command".
- Put aria-keyshortcuts on the control itself. That is what a screen-reader user actually queries; the visual keycap is for everyone else.
Focus & touch
- A KBD is never in the focus order and never interactive. If a key hint is clickable — pressing it runs the action — that is a Button that happens to be styled as a key, and it needs a button’s semantics.
- A device with no keyboard has no use for a shortcut hint. Hide keycaps on coarse pointers — the ⌘K chip in a search field is pure noise on a phone, and it takes space from the placeholder that is doing the real work.
| Attribute | Applied to | Notes |
|---|---|---|
| <kbd> | Each key | The native element. It carries the semantics for free and is what assistive tech expects. |
| role="img" + aria-label | A chord wrapper | "Command Shift D". Three kbd elements otherwise announce as three unrelated keys. |
| aria-hidden | A "+" joiner | It is punctuation between caps, not something to read aloud. |
| aria-keyshortcuts | The control the shortcut triggers | The real mechanism: it tells assistive tech what the shortcut is, independently of any visual keycap. |
Example usage
1import { Kbd } from '@/ui/Display'23<Kbd>⌘</Kbd> <Kbd>K</Kbd>45// A chord needs ONE accessible name. Three kbd elements announce as three6// unrelated keys.7function Shortcut({ keys }: { keys: string[] }) {8 return (9 <span role="img" aria-label={keys.map(spoken).join(' ')} className="inline-flex gap-1">10 {keys.map((k) => <Kbd key={k}>{k}</Kbd>)}11 </span>12 )13}1415const SPOKEN: Record<string, string> = {16 '⌘': 'Command', '⇧': 'Shift', '⌥': 'Option', '⌃': 'Control',17 '⌫': 'Backspace', '↵': 'Enter', '↑': 'Up arrow', '↓': 'Down arrow',18}19const spoken = (k: string) => SPOKEN[k] ?? k2021// Detect once at startup, not per render.22const isMac = typeof navigator !== 'undefined' &&23 /Mac|iPod|iPhone|iPad/.test(navigator.platform ?? navigator.userAgent)2425// One definition, two renderings. Modifier order follows what the OS prints:26// Control, Option, Shift, Command on macOS.27export function keysFor(shortcut: Shortcut) {28 return isMac ? shortcut.mac : shortcut.win29}3031// The visual keycap is for sighted users. THIS is what a screen reader queries.32<button aria-keyshortcuts="Meta+K" onClick={openPalette}>33 Search <Shortcut keys={keysFor(PALETTE)} />34</button>Framework-free HTML
<!-- In prose: sized relative to the text so it never disturbs the leading. -->
<p>Press <kbd>⌘</kbd> <kbd>K</kbd> to open the command palette.</p>
<!-- A chord: one name for the whole thing. -->
<span role="img" aria-label="Command Shift D">
<kbd>⌘</kbd>
<kbd>⇧</kbd>
<kbd>D</kbd>
</span>
<!-- With a joiner in dense prose. The + is punctuation, not content. -->
<span role="img" aria-label="Control Shift D">
<kbd>Ctrl</kbd>
<span aria-hidden="true">+</span>
<kbd>Shift</kbd>
<span aria-hidden="true">+</span>
<kbd>D</kbd>
</span>
<!-- The real mechanism sits on the control, not on the keycap. -->
<button type="button" aria-keyshortcuts="Meta+K">
Search
<span role="img" aria-label="Command K"><kbd>⌘</kbd><kbd>K</kbd></span>
</button>CSS
kbd {
display: inline-flex;
align-items: center;
justify-content: center;
/* Square for a single letter, wide for "Esc". Without the minimum, "K"
is a narrow sliver beside "Shift". */
min-inline-size: 1.35em;
block-size: 1.35em;
padding-inline: 5px;
border: 1px solid var(--ds-border);
/* The one detail that makes it read as a physical key. */
border-block-end-width: 2px;
border-radius: 5px; /* tighter than a button */
background: var(--ds-surface-raised);
color: var(--ds-fg-secondary);
/* Sans, NOT mono: monospace means "these literal characters", and a key
is a physical object. */
font-family: inherit;
font-size: 0.85em; /* relative, so it scales with the text */
font-weight: 500;
line-height: 1;
white-space: nowrap;
}
/* A chord is one unit: tight inside, looser to the text around it. */
.ds-shortcut { display: inline-flex; gap: 4px; margin-inline: 2px; }
/* On an inverted surface the light-surface tokens produce an invisible key. */
[role='tooltip'] kbd,
.ds-inverted kbd {
border-color: rgb(255 255 255 / 0.25);
background: rgb(255 255 255 / 0.1);
color: inherit;
}
/* The fill is dropped here, so the border must be a real border. */
@media (forced-colors: active) {
kbd { border: 1px solid; background: transparent; }
}
/* No keyboard, no use for a key hint. */
@media (pointer: coarse) {
.ds-shortcut--hint { display: none; }
}Component API
Kbd
| Prop | Type | Default | Description |
|---|---|---|---|
| children* | ReactNode | — | One key. A whole chord in one cap loses the visual separation that makes it readable. |
| size | 'sm' | 'md' | 'sm' | Medium for a shortcuts page where the keys are the content. |
| className | string | — | Where inverted-surface colours are applied — a Tooltip needs its own border and fill. |
Shortcut
| Prop | Type | Default | Description |
|---|---|---|---|
| keys* | string[] | — | Modifiers first, in the platform’s printed order, then the key. |
| joiner | string | — | A "+" between caps. Useful in prose, noise in a right-aligned menu column. |
| platform | 'mac' | 'win' | 'auto' | 'auto' | Detected once at startup. Rendering ⌘ to a Windows user is a puzzle. |
Professional tips
- Define each shortcut once, with both platform forms, and derive every rendering from it. Two hand-maintained lists will disagree within a month.
- Right-align the shortcut column in menus and give it a minimum width, so chords of different lengths line up down the list.
- Use ↵ for Enter and ⌫ for Backspace on macOS, but spell them out on Windows — the symbols are not printed on those keyboards.
- Hide key hints entirely on touch. A ⌘K chip on a phone is noise taking space from the placeholder that is doing the real work.
- If a shortcut is user-configurable, render the current binding rather than the default. A hint that no longer matches is worse than none.
Performance
- Detect the platform once at module load and store the result. Reading navigator on every render is wasteful and, in some browsers, deprecated.
- Render keycaps as text, not as images or generated SVG. Text scales with the user’s font size, which 1.4.4 requires.
- Size the cap in em rather than px so it tracks the surrounding type. A fixed 18px cap inside 20px prose looks like a rendering error.
Common mistakes
- Monospace keycaps, confusing "press this key" with "type these characters".
- Rendering ⌘ to Windows users, or "Cmd" to Mac users.
- Three kbd elements with no wrapper, announcing as three unrelated keys.
- Fixed pixel sizing, so caps do not scale with the user’s text size.
- Light-surface colours reused inside a Tooltip, producing an invisible key.
- Showing a shortcut that is not actually bound.
- Wrapping a UI label in a keycap, sending users to look at their keyboard.
- A box-shadow instead of a border, so the cap vanishes in forced-colors mode.
Real-world recommendations
- Menus and tooltips are where shortcuts are learned. A dedicated shortcuts page is worth having and is not where adoption comes from.
- The ⌘K chip in a fake search box is the single most effective shortcut advertisement in modern product UI — users learn it without ever reading documentation.
- Cross-platform apps that hardcode one platform’s glyphs get support tickets from the other. Deriving both from one definition removes an entire class of them.
- If a shortcut needs three modifiers, it is probably not a shortcut anyone will use. Reserve the short chords for the things people actually do repeatedly.