Skip to content

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.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Playground
Command palette⌘K
Save⌘S
Deploy to production⌘⇧D
Delete deployment⌫
CloseEsc

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".

macOS
Command palette⌘K
Save⌘S
Deploy to production⌘⇧D
Windows
Command paletteCtrlK
SaveCtrlS
Deploy to productionCtrlShiftD

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.

Command palette⌘K
Save⌘S
Deploy to production⌘⇧D

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.

Press ⌘ K to open the command palette, Esc to close it, and ↑ ↓ to move through the results. Hold ⇧ to extend the selection.

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.

No joinerMenus, lists
⌘⇧D
With joinerProse
CtrlShiftD

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.

K
Letter
⌘
Modifier
Esc
Named
↑
Arrow
↵
Enter
⌫
Backspace
⌘K
Chord
⌘⇧D
Three-key
CtrlShiftD
With joiner
⌘K
On dark surface

Anatomy

Every part, every measurement, and the reason it is that number.

⌘⇧DCtrlShiftD

A keycap with a heavier bottom border than its sides, sized to the type around it rather than to a fixed pixel value.

  1. Height18–20px

    Sized to sit on a 13px line without disturbing it. A keycap taller than its line makes prose leading visibly uneven.

  2. 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".

  3. Padding0 5px, 1px vertical

    Deliberately tight vertically. The horizontal padding does the visual work; vertical padding pushes the cap off the baseline.

  4. 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.

  5. Type11px, 500 weight

    Sans, not monospace. Monospace means "these literal characters"; a key is a physical object, and the two must not be confused.

  6. 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.

  7. Radius5px

    Slightly less than a button at the same height. Keys are harder-edged objects than controls.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed 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

TokenValueUsed for
--space-1Gap between caps in a chord

Radius

TokenValueUsed for
5px—Keycap corners — tighter than a button

Typography

TokenValueUsed for
font-sansThe label. Never monospace.

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightPaddingLabel gapTypeMin widthWhen to use
Default18px0 5px—11px18pxIn menus, tooltips and lists.
In prose1.35em——0.85em1.35emSized relative to the surrounding text so it never disturbs the line height.
Large24px——13px24pxA 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————4remA right-aligned shortcut column in a menu, so chords of different lengths line up.

Do

⌘KCtrlK
Render the platform’s own glyphsA Mac user reads ⌘ instantly and has to translate "Cmd". Detecting the platform once is a few lines and removes the translation entirely.
<span role="img" aria-label="Command Shift D">
Give the chord one accessible nameThree separate kbd elements announce as three unrelated keys. One label — "Command Shift D" — is what makes the shortcut comprehensible.
⌃⌥⇧⌘KControl, Option, Shift, Command
Use the printed modifier orderControl, Option, Shift, Command on macOS; Ctrl, Alt, Shift on Windows. It is the order on every OS menu, and reversing it makes a familiar shortcut look wrong.
Rename⌘E
Show it where the action isMenus, tooltips and the command palette are where shortcuts get learned. A shortcuts page nobody opens teaches nobody anything.

Don't

Cmd+K
Do not use monospaceMonospace is a promise that these are the literal characters to type. A key is a physical object, and confusing the two undermines both conventions.
⎇❖⌤
Do not invent symbolsOnly use glyphs the platform actually prints. A made-up symbol for a key the user has never seen is a puzzle in place of an instruction.
Export⌘Enot bound
Do not show a shortcut that does not existA rendered key that does nothing when pressed is worse than no hint — it teaches the user that the hints are unreliable.

Click Deploy to start the rollout.

Do not use one for a UI labelA keycap tells the user to press a physical key. Wrapping a button’s name in one sends them looking at their keyboard for a control on screen.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.3.1Info and RelationshipsA1.4.3Contrast (Minimum)AA1.4.4Resize TextAA4.1.2Name, Role, ValueA

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

TabNothing — 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.
AttributeApplied toNotes
<kbd>Each keyThe native element. It carries the semantics for free and is what assistive tech expects.
role="img" + aria-labelA chord wrapper"Command Shift D". Three kbd elements otherwise announce as three unrelated keys.
aria-hiddenA "+" joinerIt is punctuation between caps, not something to read aloud.
aria-keyshortcutsThe control the shortcut triggersThe real mechanism: it tells assistive tech what the shortcut is, independently of any visual keycap.

Code

Example usage

tsx
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

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

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

PropTypeDefaultDescription
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.
classNamestring—Where inverted-surface colours are applied — a Tooltip needs its own border and fill.

Shortcut

PropTypeDefaultDescription
keys*string[]—Modifiers first, in the platform’s printed order, then the key.
joinerstring—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.

Notes

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.