Display

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

type Platform = "mac" | "win"

/* ------------------------------------------------------------------ key ids */

/**
 * Alias table → canonical key id. Canonical ids are what the layout rows are
 * authored with, so a binding written as `["cmd", "shift", "P"]` and a cap
 * authored as `meta` meet in the middle.
 *

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/keyboard-map.json

Prompt

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 ⌘ ⇧ P read 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. With legend={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.

On This Page