DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ButtonIconButtonLinkLabelBadgeStatusChipAvatarShortcutSpinnerSkeletonProgressMeter
AI agentsContributing
  1. Components
  2. Shortcut

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/ui

When 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 Shortcut per combination.
  • Platform-aware keys. Write mod once and it resolves to the platform's own modifier; add glyphs for 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 Key for a single key such as Esc in running text.

When not to use

  • To bind a key. Shortcut is a visual hint only. Put the handler on the control and declare it with aria-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-hidden instead of reading it twice.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Key, Shortcut, Surface } from "@tessinaui/ui";
example.tsx
{/* 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

PropTypeDefaultDescription
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)
squarebooleanderivedForce 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"inheritedOn-color treatment for colored surfaces; inherited from <Surface> or a parent Shortcut
glyphsbooleanfalseRender the ⌘ ⇧ ⌥ ⌃ modifier symbols on macOS and iOS (inherits from parent Shortcut)
platform"mac" | "other"auto-detectedForce a keyboard convention instead of detecting one
classNamestring—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

PropTypeDefaultDescription
keysstring[]—Shorthand: array of key names with auto-inserted separators
childrenReactNode—Compound: Key components as children — separators auto-injected between them
separatorstring"+"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"inheritedOn-color treatment; inherited from a wrapping <Surface>
glyphsbooleanfalseRender the ⌘ ⇧ ⌥ ⌃ modifier symbols on macOS and iOS
platform"mac" | "other"auto-detectedForce a keyboard convention instead of detecting one
dir"ltr" | "rtl"inheritedText direction for RTL layouts. Stamped only when set
classNamestring—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.

NameAliasesmacOSWindows / LinuxAnnounced as
mod—⌘ / CmdCtrlCommand / Control
commandcmd, meta⌘ / CmdMetaCommand / Meta
controlctrl⌃ / CtrlCtrlControl
shift—⇧ / ShiftShiftShift
altoption, opt⌥ / AltAltOption / Alt
enterreturn↵ / EnterEnterReturn / Enter
escapeescEscEscEscape
tab—⇥ / TabTabTab
backspace—⌫ / BackspaceBackspaceBackspace
deletedel⌦ / DeleteDeleteDelete
spacespacebarSpaceSpaceSpace
up down left rightarrowup, …↑ ↓ ← →↑ ↓ ← →Up Arrow, …
pageup pagedownpgup, pgdn⇞ ⇟ / PgUpPgUp PgDnPage Up / Page Down
home end—Home EndHome EndHome / End
capslockcaps⇪ / CapsCapsCaps Lock
fn—FnFnFunction
f1–f19—F1F1F1

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 forbids aria-label, so the name is real text: the visible form is aria-hidden and an sr-only twin alongside it is what gets announced. A cap reading Ctrl announces "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 of mod shift p reads "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-hidden so 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.

AvatarSpinner

On this page

When to useWhen not to useInstallationUsageExamplesDefaultSingle KeySizesRoundedVariantsSeparatorsCompoundGlyphsPlatformOn colorRTLIn a buttonIn a tooltipIn a search fieldAPI ReferenceKey propsShortcut propsKey namesOne key, one capAccessibility