# Keyboard shortcut hint

> A keyboard combination or sequence as nested kbd keycaps: Ctrl + K, ⌘K or G then I. One prop maps Mod to ⌘ or Ctrl, symbol keys carry spoken names, and it never listens for keys.

- ID: `cmp_keyboard_shortcut_hint_01`
- Slug: `keyboard-shortcut-hint-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-10-21T22:03:48Z
- Updated: 2026-10-21
- Available versions: `1.0.0`
- Kind: control
- Primary category: `typography`
- Detail page: https://pagesugar.com/components/keyboard-shortcut-hint-01
- Preview: https://pagesugar.com/preview/keyboard-shortcut-hint-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `default` | Default | yes | `sha256-75d0d7d9b5e4b2bd63bad86f8fc433420eed37a03160201ee6dc103875fc9a9e` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Imports: The source imports its own files by relative path only. Examples import from $lib/...; SvelteKit 3 projects have no $lib alias by default, so import from your #lib subpath (with the .svelte extension) or a relative path instead.
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: no
- Integration level: presentational
- Appearance modes: light
- Suggested directory: `src/lib/components/keyboard-shortcut-hint-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Pass the keys in order and, if you know it, the platform. It renders a kbd group of keycaps and nothing else: it does not register, handle or check the shortcut, does not detect the operating system, and does not reorder modifiers into a platform's conventional order.

Required props: `keys`

```svelte
<script lang="ts">
	import KeyboardShortcut from '$lib/components/keyboard-shortcut-hint-01/KeyboardShortcut.svelte';

	// Decide the platform yourself, for example from a user setting or a server-side header.
	const platform: 'mac' | 'other' = 'other';
</script>

<p>Press <KeyboardShortcut keys={['Mod', 'K']} {platform} size="sm" /> to search.</p>

<KeyboardShortcut keys={['Shift', 'Mod', 'P']} platform="mac" />
<KeyboardShortcut keys={['G', 'I']} sequence />
<KeyboardShortcut keys={['⌥', 'F']} labels={{ '⌥': 'Option' }} />
```

Limitations:

- It does not know the visitor's platform. Mod shows Ctrl unless you pass platform="mac"; choose it from a setting or a server-side hint so the server and the browser render the same thing.
- Keys render in the order you give them. On a Mac the conventional order is Control, Option, Shift, Command; write them in that order if you want it.
- Only the listed modifier names map to glyphs on a Mac. Any other key, Esc or Tab included, is shown as written.
- The hint is always laid out left to right (dir="ltr"), so Ctrl + K keeps Latin key order on a right-to-left page. Pass thenLabel and plusLabel in the page's language; the spoken order is the order of keys.
- The built-in spoken names are English. Pass labels, thenLabel and plusLabel for another language.
- Light pages by default. On a dark surface set the four tokens; the customisation guide has the values.

## Usage guide

### Keyboard shortcut hint

Shows a shortcut the page already supports, as keycaps. It never listens for the keys itself.

#### Write keys once, choose the platform yourself

Write the platform-dependent modifier as `Mod`, and pass `platform` when you know it:

```svelte
<KeyboardShortcut keys={['Mod', 'K']} platform="mac" />
<!-- ⌘ K, read "Command plus K" -->
<KeyboardShortcut keys={['Mod', 'K']} />
<!-- Ctrl + K, read "Control plus K" -->
```

The component does not sniff the user agent, because the server cannot see it and a guess made
in the browser would change the markup after hydration. Take the platform from a user setting,
or read a client hint such as `Sec-CH-UA-Platform` on the server and pass it down.

#### Sequences

`sequence` joins the keys with a word instead of a plus: `G then I`. Pass `thenLabel` (shown and
spoken) and `plusLabel` (spoken only) on non-English pages.

#### Inside prose

Use `size="sm"`. The 20 px caps fit inside a 16 px paragraph's line box, so the lines keep their
spacing, and the hint sits on the text baseline.

#### Spoken names

Glyphs such as ⌘ ⌥ ⇧ ⌃ ⌫ and the arrows carry a hidden spoken name. `labels` adds or overrides
names: `labels={{ '⌘': 'Cmd' }}`.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `keys` | `string[]` | yes |  | Keys in the order they are pressed, such as \['Mod', 'K'\]. Mod becomes ⌘ or Ctrl from platform. An empty array renders nothing. |
| `sequence` | `boolean` | no | `false` | Keys pressed one after another rather than together. The separator becomes the word in thenLabel, shown and spoken. |
| `labels` | `Record<string, string>` | no |  | Spoken names by key, such as { '⌘': 'Command' }, read by screen readers in place of the printed key. Merged over the built-in names for symbol keys. |
| `size` | 'sm' \| 'md' | no | `'md'` | sm is 20 px caps with 12 px text, for prose and dense rows; md is 24 px caps with 13 px text. |
| `platform` | 'mac' \| 'other' | no | `'other'` | The platform you have chosen. mac shows ⌘ ⇧ ⌥ ⌃ ↵ ⌫ for Mod, Shift, Alt, Ctrl, Enter and Backspace and drops the plus; other shows Mod as Ctrl. Never detected. |
| `thenLabel` | `string` | no | `'then'` | The word shown and spoken between the keys of a sequence. |
| `plusLabel` | `string` | no | `'plus'` | The word spoken, not shown, between the keys of a combination. |
| `class` | `string` | no |  | Extra classes on the outer kbd, for margins or alignment. |

## Customization

Retone the keycaps with four --keyboard-shortcut-hint-\* variables. Edit the MAC and SPOKEN maps in the source to change which names become glyphs and what screen readers say for them.

- Keycap colours: --keyboard-shortcut-hint-surface is the cap fill, --keyboard-shortcut-hint-ink its label, --keyboard-shortcut-hint-muted the + and then separators, and --keyboard-shortcut-hint-hairline the cap's edge and its one-pixel bottom lip.
- Dark or tinted page: set surface to rgb(255 255 255 / 0.06), ink to #d4d4d8, muted to #a1a1aa and hairline to rgb(255 255 255 / 0.14) on the dark container. The lip is a hairline, not a shadow, so it still shows on dark.
- Cream or warm page: keep surface #ffffff so the caps lift off the page, and set hairline to a warm alpha such as rgb(60 40 20 / 0.16).
- Platform: pass platform="mac" when you know the visitor is on a Mac, from a user setting or a server-side hint. Write Mod once in keys and both platforms read correctly.
- Another language: pass thenLabel and plusLabel, and labels for any symbol key whose spoken name should change.
- More glyphs on a Mac: add entries to MAC in the source, for example tab: '⇥', and give each new glyph a spoken name in SPOKEN.
- Shape: the caps are 4 px (sm) and 6 px (md) rectangles. Change rounded-sm and rounded-md in SIZES; keep them below the cap's half-height so they stay keys, not pills.

| Token | Public CSS variable |
| --- | --- |
| `surface` | `--keyboard-shortcut-hint-surface` |
| `ink` | `--keyboard-shortcut-hint-ink` |
| `muted` | `--keyboard-shortcut-hint-muted` |
| `hairline` | `--keyboard-shortcut-hint-hairline` |

## Accessibility

- Renders an outer kbd holding one kbd per key, the nesting the HTML spec gives for a key combination. kbd has no role, so nothing announces it as a key; the spoken text below is what carries the meaning.
- Symbol keys print a glyph hidden from assistive technology and a visually hidden name beside it, so ⌘ is read as Command rather than skipped or read as "place of interest sign".
- The + and then separators are hidden from assistive technology and a spoken plus or then is supplied, so Ctrl + K reads as Control plus K and a sequence reads G then I.
- It registers no keyboard listener and is not focusable. The shortcut it describes is yours to implement, and the hint should only show shortcuts that actually work on the page.
- Put the hint where the shortcut's action is named, such as inside the trigger button it opens or next to a menu item, so the shortcut is announced with the action it runs.
- The default keycap label is zinc-600 on its white cap (7.7:1) and the separators zinc-500, which clear 4.5:1 on white (4.8:1) and zinc-50 (4.6:1). In forced-colours mode each cap draws a system-colour outline in place of its hairline.

Known limitations:

- Contrast is checked for the default colours on white and zinc-50 pages. On zinc-100 or darker greys the then separator falls to 4.4:1; set --keyboard-shortcut-hint-muted to #52525b there. A retone is the consumer's to check.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Default (`default`)
- Entry: `KeyboardShortcut.svelte`
- Suggested directory: `src/lib/components/keyboard-shortcut-hint-01`
- Files: 1
- Artifact digest: `sha256-75d0d7d9b5e4b2bd63bad86f8fc433420eed37a03160201ee6dc103875fc9a9e`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `KeyboardShortcut.svelte`

Role: entry · 6310 bytes · SHA-256 `437a569874a695e657c0e6de7780c79f7e09eedb25feccf2b3d188ca1763c4b8`

```svelte
<script lang="ts" module>
	export type KeyboardShortcutSize = 'sm' | 'md';
	export type KeyboardShortcutPlatform = 'mac' | 'other';
</script>

<script lang="ts">
	interface Props {
		/** Keys in the order they are pressed, e.g. ['Mod', 'K']. 'Mod' becomes ⌘ or Ctrl. */
		keys: string[];
		/** Keys pressed one after another rather than together; the separator becomes "then". */
		sequence?: boolean;
		/** Spoken names by key, e.g. { '⌘': 'Command' }. Merged over the built-in names. */
		labels?: Record<string, string>;
		/** Keycap size: sm is 20 px for prose and dense rows, md is 24 px. */
		size?: KeyboardShortcutSize;
		/** Platform the consumer has chosen. Maps Mod and the modifier names; never detected. */
		platform?: KeyboardShortcutPlatform;
		/** The word shown and spoken between the keys of a sequence. */
		thenLabel?: string;
		/** The word spoken (not shown) between the keys of a combination. */
		plusLabel?: string;
		/** Extra classes on the outer kbd, for margins or alignment. */
		class?: string;
	}

	let {
		keys,
		sequence = false,
		labels,
		size = 'md',
		platform = 'other',
		thenLabel = 'then',
		plusLabel = 'plus',
		class: className
	}: Props = $props();

	/*
	 * Modifier names a consumer writes once, shown the way each platform prints them. Keys are
	 * matched without case. Only 'mac' swaps names for glyphs; elsewhere only Mod changes.
	 */
	const MAC: Record<string, string> = {
		mod: '⌘',
		cmd: '⌘',
		command: '⌘',
		ctrl: '⌃',
		control: '⌃',
		alt: '⌥',
		option: '⌥',
		opt: '⌥',
		shift: '⇧',
		enter: '↵',
		return: '↵',
		backspace: '⌫'
	};
	const OTHER: Record<string, string> = { mod: 'Ctrl' };

	/* What a screen reader should say for keys whose printed form it reads badly or not at all. */
	const SPOKEN: Record<string, string> = {
		'⌘': 'Command',
		'⌥': 'Option',
		'⇧': 'Shift',
		'⌃': 'Control',
		'⌫': 'Delete',
		'⌦': 'Forward Delete',
		'⎋': 'Escape',
		'⇥': 'Tab',
		'⇪': 'Caps Lock',
		'←': 'Left Arrow',
		'→': 'Right Arrow',
		'↑': 'Up Arrow',
		'↓': 'Down Arrow',
		'␣': 'Space',
		Ctrl: 'Control',
		Esc: 'Escape',
		Del: 'Delete',
		PgUp: 'Page Up',
		PgDn: 'Page Down'
	};

	/*
	 * Modifier glyphs are drawn smaller than capital letters in most text faces, so they are
	 * scaled an eighth larger to sit at the letters' optical size. A scale, not a font size, so
	 * the glyph's metrics never move its cap off the shared baseline. Arrows already match.
	 */
	const GLYPHS = new Set(['⌘', '⌥', '⇧', '⌃', '⌫', '⌦', '⎋', '⇥', '⇪', '↵', '⏎']);

	/*
	 * One height and a square minimum width per size, so a lone "K" is a key, not a sliver. The
	 * caption line box is the cap's full height: the cap centres its label and shares that
	 * label's baseline with the text around the hint.
	 */
	const SIZES: Record<KeyboardShortcutSize, { root: string; cap: string }> = {
		sm: { root: 'text-xs', cap: 'h-5 min-w-5 rounded-sm px-1 leading-5' },
		md: { root: 'text-[0.8125rem]', cap: 'h-6 min-w-6 rounded-md px-1.5 leading-6' }
	};

	/** A map lookup that ignores inherited names, so a key called "constructor" stays text. */
	const own = (map: Record<string, string> | undefined, key: string) =>
		map && Object.hasOwn(map, key) ? map[key] : undefined;

	const step = $derived(Object.hasOwn(SIZES, size) ? SIZES[size] : SIZES.md);
	const mac = $derived(platform === 'mac');

	const caps = $derived(
		(keys ?? [])
			.map((key) => String(key).trim())
			.filter(Boolean)
			.map((key) => {
				const text = own(mac ? MAC : OTHER, key.toLowerCase()) ?? key;
				const enter = text === '↵' || text === '⏎' ? (mac ? 'Return' : 'Enter') : undefined;
				const spoken = own(labels, key) ?? own(labels, text) ?? own(SPOKEN, text) ?? enter;
				return {
					text,
					spoken: spoken && spoken !== text ? spoken : undefined,
					glyph: GLYPHS.has(text)
				};
			})
	);
</script>

<!--
	The shortcut is notation, so it keeps Latin key order on a right-to-left page (dir="ltr").
	Each separator travels with the key after it: the group wraps between steps, never leaving
	a "+" or "then" stranded at the end of a line.
-->
{#if caps.length}
	<kbd
		dir="ltr"
		class={[
			'keyboard-shortcut-hint inline-flex max-w-full flex-wrap items-baseline gap-1 align-baseline font-sans font-medium tracking-normal normal-case',
			step.root,
			className
		]}
	>
		{#each caps as cap, index (index)}
			<span class="inline-flex items-baseline gap-1 whitespace-nowrap">
				{#if index > 0}
					{#if sequence}
						<span class="font-normal text-(--_muted)" aria-hidden="true">{thenLabel}</span>
					{:else if !mac}
						<span class="font-normal text-(--_muted)" aria-hidden="true">+</span>
					{/if}
					<!-- Spaces inside the spoken word: flex gaps are not word boundaries to a screen reader. -->
					<span class="sr-only">{` ${sequence ? thenLabel : plusLabel} `}</span>
				{/if}
				<kbd
					class={[
						'keyboard-shortcut-hint__key inline-flex shrink-0 items-center justify-center bg-(--_surface) font-[inherit] whitespace-nowrap text-(--_ink) tabular-nums shadow-(--_cap) forced-colors:outline-1 forced-colors:-outline-offset-1 forced-colors:outline-solid',
						step.cap
					]}
				>
					{#if cap.spoken}
						<span class={cap.glyph ? 'inline-block scale-[1.125]' : undefined} aria-hidden="true"
							>{cap.text}</span
						><span class="sr-only">{cap.spoken}</span>
					{:else}
						{cap.text}
					{/if}
				</kbd>
			</span>
		{/each}
	</kbd>
{/if}

<style>
	/*
	 * Public tokens: set --keyboard-shortcut-hint-* on the hint or any ancestor to retone it. A
	 * hint paints no accent, so it has no accent tokens: surface is the keycap fill, ink its
	 * label, muted the separators, and hairline the cap edge.
	 */
	.keyboard-shortcut-hint {
		--_surface: var(--keyboard-shortcut-hint-surface, #ffffff);
		--_ink: var(--keyboard-shortcut-hint-ink, #52525b);
		--_muted: var(--keyboard-shortcut-hint-muted, #71717a);
		--_hairline: var(--keyboard-shortcut-hint-hairline, rgb(0 0 0 / 0.12));

		/*
		 * Formula: a hairline all round plus one more along the bottom, inset so the cap keeps
		 * its box. Light from above, one pixel of lip; never a bevel.
		 */
		--_cap: inset 0 0 0 1px var(--_hairline), inset 0 -1px 0 var(--_hairline);
	}
</style>
```

## Artifacts

### Default (`default`) (default)

- Artifact digest: `sha256-75d0d7d9b5e4b2bd63bad86f8fc433420eed37a03160201ee6dc103875fc9a9e`
- Entry: `KeyboardShortcut.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_keyboard_shortcut_hint_01/1.0.0/default/sha256-75d0d7d9b5e4b2bd63bad86f8fc433420eed37a03160201ee6dc103875fc9a9e/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_keyboard_shortcut_hint_01/1.0.0/default/sha256-75d0d7d9b5e4b2bd63bad86f8fc433420eed37a03160201ee6dc103875fc9a9e/bundle.zip (5846 bytes, sha256 `23cfd3408a640c9ab4abe6f1aafaee1cbcd572156df0e39510134db7abf0a57e`)

Files:

- `KeyboardShortcut.svelte` (entry, 6310 bytes): https://pagesugar.com/artifacts/cmp_keyboard_shortcut_hint_01/1.0.0/default/sha256-75d0d7d9b5e4b2bd63bad86f8fc433420eed37a03160201ee6dc103875fc9a9e/source/KeyboardShortcut.svelte
