Keyboard Map
A keyboard layout diagram that paints your app's bound shortcuts onto the keys — platform-correct glyphs and modifier order, whole-chord highlighting, and a readable binding list.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/keyboard-map.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "KeyboardMap" component: a keyboard
layout drawing with an app's bound shortcuts painted onto the caps.
Contract
- KeyboardMap(props extends React.HTMLAttributes<HTMLDivElement>):
- bindings: KeyBinding[] where KeyBinding = { keys: string[]; action: string }.
`keys` is the chord in press order and accepts semantic names:
mod / cmd / command / meta / super / win, ctrl / control, alt / option / opt,
shift, enter / return, esc / escape, space, tab, backspace, delete,
up / down / left / right (or arrowup / …), home / end, pageup / pagedown
(pgup / pgdn), f1…f12, single letters, digits and punctuation.
- platform?: "auto" | "mac" | "win" (default "auto").
- layout?: "60" | "tkl" | KeyboardMapLayout (default "60").
- legend?: boolean (default true).
- KeyboardMapLayout = { name: string; rows: KeyboardMapKey[][] } and
KeyboardMapKey = { id: string; label?: string; w?: number }. `id` is the
canonical key name a binding matches against, `w` is the cap width in key
units (1u = one letter key), and `id: ""` renders a decorative gap so a nav
cluster can be offset from the main block. Export both types so a consumer
can hand-author an ISO / split / numpad board.
- No callbacks. This is a read-only picture; the only state it owns is which
chord is currently spotlit.
Behavior
- Platform is the point of the component, and it changes two things, not one:
1. Glyphs — mac renders ⌘ ⌥ ⌃ ⇧, windows renders Win / Alt / Ctrl / Shift.
`mod` is the portable primary modifier: it resolves to the ⌘ key on mac
and to Ctrl everywhere else, so one binding list feeds both boards.
2. Positions — the bottom row on a mac board runs Ctrl · Option · Command,
on a Windows board it runs Ctrl · Win · Alt. The third cap from the left
is a different key on the two platforms, so the preset layouts swap the
order instead of only swapping the labels. Drawing mac order under
Windows glyphs would put the reader's thumb on the wrong key.
- "auto" detection is SSR-safe: read it through useSyncExternalStore with a
server snapshot of "win", so the server and the client's first paint agree
and the real value arrives on the second render. Never read `navigator`
during render. Pass an explicit platform if you server-render and don't want
the one-frame swap.
- Each binding is normalised into canonical key ids once, then indexed by key,
so a cap knows every chord it participates in. A chord is deduped by
(keys, action) — two identical rows would otherwise collide as React keys.
- Highlighting has three cap states, and the whole chord lights at once:
- active — the key belongs to the currently spotlit chord(s): solid primary.
- bound — the key belongs to some chord: tinted fill, stronger border, and
a small count badge when more than one chord uses it.
- idle — plain keycap.
Spotlight priority: hovered/focused legend row (exactly one chord) → hovered
cap (every chord on that key, which is how "one key, several actions" is
read) → pinned chord. Clicking a legend row pins/unpins it (aria-pressed),
so the chord stays lit with the pointer away.
- Every physical cap that matches lights up — both Shifts, both ⌘s. Either one
works on a real keyboard, so highlighting only the left one would be a lie.
- A chord whose keys aren't on the chosen layout (F2 on a 60% board) still
appears in the legend but lights nothing: pick the layout that has the keys,
or pass your own rows.
Rendering & styling
- Semantic tokens only: bg-card / border for the frame, bg-muted +
text-muted-foreground for idle caps, bg-primary/15 + border-primary/40 +
text-foreground for bound ones, bg-primary + text-primary-foreground for the
active chord. No hardcoded colours; light and dark both come free.
Don't reach for --chart-1..5 to categorise bindings — that ramp is one hue
and identical in both themes, so categories would be indistinguishable.
- Geometry, not pixels: a row is `aspect-ratio: <units> / 1` so its own width
becomes its height, and each cap is `width: (w / units) * 100%`. Nothing is
measured in JS, and one square cap falls out of the arithmetic. Padding on
the slot (not gap on the row) keeps every row exactly `units` wide, which is
what makes the rows line up.
- Sizing: the board is `clamp(units * 30px, 100%, units * 40px)` inside an
`overflow-x-auto` box. It shrinks to fit until a cap would drop below ~30px,
then the box scrolls sideways instead of squeezing the legends into mush —
at 375px a full 60% board is 450px wide and scrolls, with 26px caps and a
12.6px face. Cap text is sized in container-query units (`cqi`) against a
wrapper, because an element is never its own query container.
- Long faces ("PrtSc", "PgUp") scale down by an em-relative factor derived
from cap width ÷ label length, so nothing is clipped and nothing is
abbreviated into nonsense.
- Accessibility: the board is `role="img"` with an aria-label naming the
layout, platform and shortcut count — sixty caps read one at a time is
noise, and the legend under it carries the same information as text. It is
also `tabIndex={0}` so the scroll box is reachable without a mouse. Legend
rows are real buttons carrying an sr-only spoken form ("Command plus Shift
plus P"), because a screen reader reading ⌘ ⇧ P is gibberish. When
legend={false}, the same list stays in the accessibility tree as a plain
visually-hidden list with no focusable children — an sr-only tab stop is an
invisible keyboard trap. Put `sr-only` on a wrapper div, never directly on
the <ul>/<table>: CSS width is only a lower bound on a table box, so the
hidden list would push the page into horizontal scroll.
- Motion: colour transitions only, all with motion-reduce:transition-none.
Nothing depends on animation.
Customization levers
- Layout: pass your own `rows` for ISO, split, numpad, ortholinear or a
product-specific board. Keep every row summing to the same unit count and
use `id: ""` gaps for cluster offsets; everything else is derived.
- Key vocabulary: the alias table (mod / cmd / option / pgup …) and the face
tables (glyph + spoken name) are the single source of truth — add an entry
there instead of pre-formatting glyphs at each call site.
- Density: MIN_UNIT_PX / MAX_UNIT_PX set the legible floor and the ceiling.
Raise the floor for a touch-first surface; lower the ceiling to keep the
board from dominating a wide page.
- Legend: `legend={false}` for a compact board (settings sidebar, hero
screenshot); keep it on for a real shortcuts page. Swap the auto-fit grid's
15rem track minimum to control how many columns it breaks into.
- Emphasis: the three cap states are three className branches. Category
colouring is the one thing to avoid — use grouping in the legend, or ship
one map per group, instead of colour-coding caps.
- Pin behaviour: drop the pin (and aria-pressed) if the map is purely
decorative; add an onSelect callback if a click should open your rebind UI.Concepts
- mod as the portable modifier — bindings are authored once with
mod; it lands on the ⌘ cap on a mac board and on Ctrl on a Windows one, so the same data drives both drawings instead of two hand-maintained lists. - Platform swaps positions, not just glyphs — a mac bottom row runs Ctrl · Option · Command and a Windows one runs Ctrl · Win · Alt. Relabelling caps in place would draw a keyboard nobody owns, so the preset layout itself changes with the platform.
- Chord as one highlight — hovering or focusing a legend row lights every cap in that chord simultaneously and names it once in the detail bar, which is what makes
⌘ ⇧ Pread as one shortcut instead of three separate bindings. Every matching physical cap lights (both Shifts, both ⌘s) because either one really does work. - One key, several actions — a cap carries a count badge when more than one chord uses it, and hovering it lists all of them; the key is a bucket of chords, not a single label.
- Picture plus readable list — the board is
role="img"with a summarising label, and the binding list underneath is the text version, so a screen reader gets "Command palette — Command plus Shift plus P" instead of sixty unlabelled caps. Withlegend={false}that list stays in the accessibility tree, visually hidden and non-interactive. - Legible floor, then scroll — the board shrinks with its container only until a cap would stop being readable (~30px), after which the box scrolls sideways. A full keyboard squeezed into 375px would be unreadable, and the legend already carries every binding for anyone who doesn't want to scroll.
Event Card
An event card with a calendar tear-off that survives cross-day, cross-month and cross-year spans, day boundaries computed in an explicit IANA zone, and an ordered capacity machine where “ended” outranks every seat count.
Scrollbar Styles
Six presets that re-skin the browser's own scrollbar in pure CSS — no JavaScript, no drawn thumb, and one capability-tiered sheet that keeps the webkit pseudo-elements and the standard properties from cancelling each other out.