Shortcut
Keyboard shortcut badges for documenting and displaying key combinations. Canonical key names resolve to the platform's own modifiers and symbols, each keycap carrying the name a screen reader announces.
pnpm add @tessinaui/uiWhen to use
- A hint beside the action it triggers. In a button, a menu row, a Tooltip or a Search field, so people learn the key where they already look.
- Documentation and help screens that list key combinations, one
Shortcutper combination. - Platform-aware keys. Write
modonce and it resolves to the platform's own modifier; addglyphsfor the symbol form on macOS and iOS. - Frequent actions. A shortcut earns its space when people repeat the action often, not for a one-off.
- A key inside a sentence. Use
Keyfor a single key such asEscin running text.
When not to use
- To bind a key.
Shortcutis a visual hint only. Put the handler on the control and declare it witharia-keyshortcuts; see Accessibility. - As the only instruction. Keycap styling is not conveyed by assistive technology, so give the instruction in words too.
- To label a clickable control. It is not interactive. Use a Button and place the hint inside it.
- For a long list of commands. Use Command or a Menubar, where each row carries its own hint.
- Where the hint repeats what is already announced, such as a menu item's
own shortcut. Mark it
aria-hiddeninstead of reading it twice.
Installation
pnpm add @tessinaui/uiUsage
import { Key, Shortcut, Surface } from "@tessinaui/ui";{/* Single key */}
<Key>Esc</Key>
{/* Shorthand — keys array */}
<Shortcut keys={["Ctrl", "K"]} />
<Shortcut keys={["Cmd", "Shift", "P"]} />
{/* "mod" — ⌘ on macOS, Ctrl everywhere else */}
<Shortcut keys={["mod", "K"]} />
{/* ⌘ ⇧ ⌥ ⌃ symbols, on macOS and iOS */}
<Shortcut keys={["mod", "shift", "p"]} glyphs />
{/* Force a platform — previews, tests, server-resolved rendering */}
<Shortcut keys={["mod", "K"]} glyphs platform="mac" />
{/* On a colored surface */}
<Surface background="primary">
<Shortcut keys={["mod", "K"]} tone="on-color" />
</Surface>
{/* Compound — children for custom compositions */}
<Shortcut>
<Key>Ctrl</Key>
<Key>K</Key>
</Shortcut>
{/* Mixed rounded per-key */}
<Shortcut>
<Key>Cmd</Key>
<Key rounded="full">⌘</Key>
</Shortcut>
{/* Rounded variant */}
<Shortcut keys={["Ctrl", "S"]} rounded="full" />
{/* Custom separator */}
<Shortcut keys={["Ctrl", "K"]} separator="then" />
{/* Size */}
<Shortcut keys={["Ctrl", "S"]} size="lg" />
{/* RTL */}
<Shortcut keys={["Ctrl", "Z"]} dir="rtl" />Examples
Default
A keyboard combination rendered from the keys array with auto-inserted separators.
Single Key
The standalone Key component for individual keys like Esc or Enter.
Sizes
Five sizes — xs, sm, md, lg, and xl.
Rounded
Border radius variants — none, sm, md, lg, and full.
Variants
Three weights — secondary is the filled plate, outline the edge only, and
ghost the bare glyph. Every weight keeps the same box, so switching never
moves the mark.
Separators
Customize the character shown between keys with the separator prop.
Compound
Compose individual Key children for per-key control, like mixing rounded styles.
Glyphs
glyphs swaps the words for the ⌘ ⇧ ⌥ ⌃ symbols on macOS and iOS. Windows and
Linux keep the words — they have no glyph convention to follow.
Platform
"mod" is the platform's primary modifier: ⌘ on macOS, Ctrl everywhere else.
platform forces the answer when you need a deterministic one.
On color
tone="on-color" puts keycaps on a colored surface. Inside a <Surface> it is
inherited, so nested shortcuts adapt on their own.
RTL
dir="rtl" mirrors the key order for right-to-left layouts. Leave dir unset and the shortcut inherits the page's direction.
In a button
A hint inside the control it belongs to. It sits in trailingIcon, which the
button renders aria-hidden, and the shortcut is announced by
aria-keyshortcuts on the button instead — otherwise a screen reader reads
"Accept Enter" as though the key were part of the label.
In a tooltip
The familiar pattern: hover a control, get one line naming it plus the keys that trigger it.
It composes HoverCard rather than Tooltip, because Tooltip is a static card
— it has content slots but no trigger, so it cannot appear on hover. Three
overrides turn a card into a tooltip: w-auto max-w-none so the popup
shrink-wraps its line, whitespace-nowrap to keep it to one line, and shorter
open/close delays.
If you want the static card instead, Tooltip takes a shortcuts array directly
and scales the caps against the card — an lg card gets md caps, an sm card
gets xs — so nothing needs composing by hand there.
In a search field
Search accepts showShortcut, which puts a ⌘K cap in the field, swaps it for
Esc once there is a query, and binds the global focus listener to match.
API Reference
Key props
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Visual size of the key badge (inherits from parent Shortcut) |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Border radius (inherits from parent Shortcut) |
square | boolean | derived | Force the square cap on or off. Unset, a cap is square exactly when it shows one character. Set it for a cap whose content cannot be counted, such as an icon; forcing a word key square clips it |
variant | "secondary" | "outline" | "ghost" | "secondary" | Visual weight (§1). secondary is the filled plate, outline trades the fill for an edge, ghost is the bare glyph with no plate at all. Every weight keeps the same box — the border is present and transparent in all three — so switching never shifts the mark |
tone | "default" | "on-color" | inherited | On-color treatment for colored surfaces; inherited from <Surface> or a parent Shortcut |
glyphs | boolean | false | Render the ⌘ ⇧ ⌥ ⌃ modifier symbols on macOS and iOS (inherits from parent Shortcut) |
platform | "mac" | "other" | auto-detected | Force a keyboard convention instead of detecting one |
className | string | — | Additional CSS classes |
All native <kbd> HTML attributes are forwarded.
The exported keyVariants gives access to the same class map for one-off
compositions.
Shortcut props
| Prop | Type | Default | Description |
|---|---|---|---|
keys | string[] | — | Shorthand: array of key names with auto-inserted separators |
children | ReactNode | — | Compound: Key components as children — separators auto-injected between them |
separator | string | "+" | Character(s) displayed between keys. Defaults to "" under glyphs on macOS and iOS |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Propagated to all child Key components via context |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Propagated to all child Key components via context |
variant | "secondary" | "outline" | "ghost" | "secondary" | Propagated to all child Key components via context |
tone | "default" | "on-color" | inherited | On-color treatment; inherited from a wrapping <Surface> |
glyphs | boolean | false | Render the ⌘ ⇧ ⌥ ⌃ modifier symbols on macOS and iOS |
platform | "mac" | "other" | auto-detected | Force a keyboard convention instead of detecting one |
dir | "ltr" | "rtl" | inherited | Text direction for RTL layouts. Stamped only when set |
className | string | — | Additional CSS classes |
children takes precedence over keys when both are provided. Native <span>
attributes are forwarded; dir is re-typed to the two values above.
Key names
Names are matched case-insensitively, and common aliases are accepted. Anything unrecognised renders exactly as written, so app-specific keys still work.
| Name | Aliases | macOS | Windows / Linux | Announced as |
|---|---|---|---|---|
mod | — | ⌘ / Cmd | Ctrl | Command / Control |
command | cmd, meta | ⌘ / Cmd | Meta | Command / Meta |
control | ctrl | ⌃ / Ctrl | Ctrl | Control |
shift | — | ⇧ / Shift | Shift | Shift |
alt | option, opt | ⌥ / Alt | Alt | Option / Alt |
enter | return | ↵ / Enter | Enter | Return / Enter |
escape | esc | Esc | Esc | Escape |
tab | — | ⇥ / Tab | Tab | Tab |
backspace | — | ⌫ / Backspace | Backspace | Backspace |
delete | del | ⌦ / Delete | Delete | Delete |
space | spacebar | Space | Space | Space |
up down left right | arrowup, … | ↑ ↓ ← → | ↑ ↓ ← → | Up Arrow, … |
pageup pagedown | pgup, pgdn | ⇞ ⇟ / PgUp | PgUp PgDn | Page Up / Page Down |
home end | — | Home End | Home End | Home / End |
capslock | caps | ⇪ / Caps | Caps | Caps Lock |
fn | — | Fn | Fn | Function |
f1–f19 | — | F1 | F1 | F1 |
The symbol column applies under glyphs; otherwise the word is used. escape
and space have no symbol on purpose — ⎋ reads to almost nobody.
Modifiers render in the order you write them. The convention worth following is the macOS one: ⌃ ⌥ ⇧ ⌘, modifiers before the key.
One key, one cap
Every key gets its own keycap, and single-key caps are uniform squares — a ⌘
cap, a C cap and a ⌫ cap are always the same size. Never put a whole run in one
cap (<Key>⌘C</Key> as a single chip); that is how a wide ⌘C chip ends up next
to a small lone ⌫. The menu shortcut wrappers split their strings automatically
via the exported splitKeyRun("⌘⇧S") → ["⌘", "⇧", "S"]; use it anywhere else a
combined string arrives.
Accessibility
-
Each keycap carries its own accessible name.
<kbd>is a generic role, which forbidsaria-label, so the name is real text: the visible form isaria-hiddenand ansr-onlytwin alongside it is what gets announced. A cap readingCtrlannounces "Control"; ⌘ announces "Command". -
Symbols you pass yourself are named too, including mixed runs —
<Key>⌘S</Key>announces "Command S" rather than a stray character. -
Separators are
aria-hidden; a shortcut reads as its keys, in order. -
VoiceOver passed 2026-09-14 (Safari).
keys={["Ctrl", "K"]}reads "Control", "K", and the glyph form ofmod shift preads "Command", "Shift", "P". NVDA and JAWS are untested. -
Shortcut binds nothing. It is a hint. Put the binding on the control and declare it with
aria-keyshortcuts, whose syntax is its own (Meta+K, space-separated for alternatives):example.tsx <button aria-keyshortcuts="Meta+K" onClick={open}> Search <Shortcut keys={["mod", "k"]} glyphs aria-hidden /> </button> -
When the hint repeats something already announced — a menu item's own shortcut, a search field's placeholder — mark the hint
aria-hiddenso it is not read twice. -
Not interactive: it takes no focus and has no pointer target, so WCAG 2.5.5's 44px minimum does not apply. The size scale follows the text it sits beside.