Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
Hotkeys API Reference
App Hotkeys API Reference
Key hold & held keys API Reference
Hotkey Recorder API Reference
Normalization & format API Reference

Vanilla Example: FormatForDisplay

import {
  formatForDisplay,
  parseHotkey,
  validateHotkey,
} from '@tanstack/hotkeys'
import type {
  FormatDisplayOptions,
  ParsedHotkeyChord,
  RegisterableHotkeyChord,
} from '@tanstack/hotkeys'

const app = document.getElementById('app')!
app.innerHTML = `
  <header><p class="eyebrow">TANSTACK HOTKEYS · VANILLA</p><h1>A shortcut, in every style.</h1>
  <p class="lead">Explore <code>formatForDisplay</code>: platform labels, independent symbols, custom separators, and individual keycaps. Display never changes a binding’s identity.</p></header>
  <main><section class="playground"><div class="controls">
    <h2>Try a binding</h2>
    <label>Hotkey string <input id="binding" value="Mod+Shift+ArrowUp" spellcheck="false"></label>
    <label>Platform <select id="platform"><option value="auto">Detect automatically</option><option value="mac">macOS</option><option value="windows">Windows</option><option value="linux">Linux</option></select></label>
    <label>Symbols <select id="symbols"><option value="default">Default (true)</option><option value="true">true — all symbols</option><option value="false">false — all labels</option><option value="keys">Keys only</option><option value="modifiers">Modifiers only</option><option value="partial">{ modifiers: false } — keys default to true</option></select></label>
    <label>Separator <select id="separator"><option value="default">Platform default (undefined)</option><option value="null">Platform default (null)</option><option value="empty">No separator</option><option value="custom">Custom</option></select></label>
    <label id="custom-label" hidden>Custom separator <input id="custom" value=" · "></label>
    <label class="check"><input id="layout" type="checkbox"> Supply a sample layoutMap (KeyQ → a, KeyS → o)</label>
    <label class="check"><input id="labels" type="checkbox"> Supply explicit keyLabels</label>
    <p class="note">Demo labels: KeyS → S, KeyQ → A, NumpadAdd → Numpad +, ArrowUp → Up. These are supplied labels, not inferred keyboard layouts.</p>
  </div><div class="preview" aria-live="polite"><p class="eyebrow">STRING OUTPUT</p><output id="string-output"></output><p class="eyebrow">PARTS AS KEYCAPS</p><div id="keycaps"></div><pre id="parts-output"></pre><p id="error" role="status"></p><pre id="snippet"></pre></div></section>
  <section><h2>Compare symbol settings</h2><p>All columns use your selected platform and separator. Keys and modifiers can be styled independently.</p><div class="table-scroll"><table><thead><tr><th>Binding</th><th>Default / true</th><th>Labels / false</th><th>Modifier symbols</th><th>Key symbols</th><th>parts: true</th></tr></thead><tbody id="comparisons"></tbody></table></div></section>
  <section><h2>Every input form</h2><p>Strings, raw logical objects, raw physical objects, and parsed bindings share the same formatter.</p><div class="input-grid" id="inputs"></div></section>
  <footer>Physical tokens such as <code>[KeyQ]</code> display a conventional letter label by default. Supply an already-resolved <code>layoutMap</code> for layout labels, or <code>keyLabels</code> for explicit overrides. The formatter never loads a layout map. <code>parts: true</code> returns an array and ignores the separator; a runtime boolean returns <code>string | string[]</code>.</footer></main>
`
// Typed lookups for the static playground controls created above.
const input = (id: string) => document.getElementById(id) as HTMLInputElement
const select = (id: string) => document.getElementById(id) as HTMLSelectElement
const element = (id: string) => document.getElementById(id)!
const samples = [
  'Mod+S',
  'Mod+Shift+ArrowUp',
  'Shift+ArrowDown',
  'Alt+ArrowLeft',
  'Control+ArrowRight',
  'Mod+Enter',
  'Mod+Tab',
  'Mod+Space',
  'Mod+Backspace',
  'Shift+Delete',
  'Escape',
  'Control++',
  'Alt+.',
  'Alt+ß',
  'Alt+[KeyS]',
  'Mod+[Digit2]',
  'Shift+[ArrowUp]',
  'Mod+[Slash]',
  '[NumpadEnter]',
  'Mod+[NumpadAdd]',
]
// A resolved sample map demonstrates layout lookup without calling browser APIs.
const layoutMap = new Map([
  ['KeyQ', 'a'],
  ['KeyS', 'o'],
])
const keyLabels = { KeyS: 'S', KeyQ: 'A', NumpadAdd: 'Numpad +', ArrowUp: 'Up' }
const symbolModes: Record<string, FormatDisplayOptions['useSymbols']> = {
  default: undefined,
  true: true,
  false: false,
  modifiers: { modifiers: true, keys: false },
  keys: { modifiers: false, keys: true },
  partial: { modifiers: false },
}
/** Renders each display part safely as text inside its own keycap. */
function keycaps(host: HTMLElement, parts: Array<string>) {
  host.replaceChildren(
    ...parts.map((part) => {
      const key = document.createElement('kbd')
      key.textContent = part
      return key
    }),
  )
}
/** Appends a text-only comparison cell, including user-supplied separator output. */
function cell(row: HTMLTableRowElement, text: string) {
  const td = row.insertCell()
  td.textContent = text
  return td
}
/** Recomputes previews, comparison rows, and input-form examples from the current controls. */
function render() {
  const platform = select('platform').value
  const separator = select('separator').value
  element('custom-label').hidden = separator !== 'custom'
  const options: FormatDisplayOptions & { parts?: false } = {
    ...(platform === 'auto'
      ? {}
      : { platform: platform as 'mac' | 'windows' | 'linux' }),
    useSymbols: symbolModes[select('symbols').value],
    separatorToken:
      separator === 'null'
        ? null
        : separator === 'empty'
          ? ''
          : separator === 'custom'
            ? input('custom').value
            : undefined,
    ...(input('labels').checked ? { keyLabels } : {}),
    ...(input('layout').checked ? { layoutMap } : {}),
  }
  const hotkey = input('binding').value
  const validation = validateHotkey(hotkey)
  element('error').textContent = validation.valid
    ? ''
    : validation.errors.join(' ')
  element('string-output').textContent = formatForDisplay(hotkey, options)
  const parts = formatForDisplay(hotkey, { ...options, parts: true })
  keycaps(element('keycaps'), parts)
  element('parts-output').textContent = JSON.stringify(parts)
  const optionCode = JSON.stringify(
    options,
    (_, value) => (value === layoutMap ? '__layoutMap__' : value),
    2,
  ).replace('"__layoutMap__"', 'layoutMap')
  element('snippet').textContent =
    (options.layoutMap
      ? `const layoutMap = new Map(${JSON.stringify([...layoutMap])})\n\n`
      : '') +
    `formatForDisplay(${JSON.stringify(hotkey)}, ${optionCode})\n\n// Render one <kbd> per part:\nformatForDisplay(${JSON.stringify(hotkey)}, {\n  ...options, parts: true\n})`
  const tbody = element('comparisons') as HTMLTableSectionElement
  tbody.replaceChildren()
  for (const sample of samples) {
    const row = tbody.insertRow()
    cell(row, sample)
    for (const useSymbols of [
      true,
      false,
      { modifiers: true, keys: false },
      { modifiers: false, keys: true },
    ]) {
      cell(row, formatForDisplay(sample, { ...options, useSymbols }))
    }
    keycaps(
      cell(row, ''),
      formatForDisplay(sample, { ...options, parts: true }),
    )
  }
  const forms: Array<{
    label: string
    code: string
    binding: RegisterableHotkeyChord | ParsedHotkeyChord
  }> = [
    {
      label: 'Logical string',
      code: "'Mod+Shift+ArrowUp'",
      binding: 'Mod+Shift+ArrowUp',
    },
    {
      label: 'Raw logical object',
      code: "{ key: 'ArrowUp', mod: true, shift: true }",
      binding: { key: 'ArrowUp', mod: true, shift: true },
    },
    { label: 'Physical string', code: "'Alt+[KeyQ]'", binding: 'Alt+[KeyQ]' },
    {
      label: 'Raw physical object',
      code: "{ code: 'KeyQ', alt: true }",
      binding: { code: 'KeyQ', alt: true },
    },
    {
      label: 'Parsed binding',
      code: "parseHotkey('Mod+Shift+ArrowUp', 'mac')",
      binding: parseHotkey('Mod+Shift+ArrowUp', 'mac'),
    },
    { label: 'Literal plus', code: "'Control++'", binding: 'Control++' },
  ]
  element('inputs').replaceChildren(
    ...forms.map(({ label, code, binding }) => {
      const card = document.createElement('article')
      const title = document.createElement('h3')
      title.textContent = label
      const source = document.createElement('code')
      source.textContent = code
      const result = document.createElement('div')
      result.className = 'form-result'
      keycaps(result, formatForDisplay(binding, { ...options, parts: true }))
      card.append(title, source, result)
      return card
    }),
  )
}
app.addEventListener('input', render)
render()
import {
  formatForDisplay,
  parseHotkey,
  validateHotkey,
} from '@tanstack/hotkeys'
import type {
  FormatDisplayOptions,
  ParsedHotkeyChord,
  RegisterableHotkeyChord,
} from '@tanstack/hotkeys'

const app = document.getElementById('app')!
app.innerHTML = `
  <header><p class="eyebrow">TANSTACK HOTKEYS · VANILLA</p><h1>A shortcut, in every style.</h1>
  <p class="lead">Explore <code>formatForDisplay</code>: platform labels, independent symbols, custom separators, and individual keycaps. Display never changes a binding’s identity.</p></header>
  <main><section class="playground"><div class="controls">
    <h2>Try a binding</h2>
    <label>Hotkey string <input id="binding" value="Mod+Shift+ArrowUp" spellcheck="false"></label>
    <label>Platform <select id="platform"><option value="auto">Detect automatically</option><option value="mac">macOS</option><option value="windows">Windows</option><option value="linux">Linux</option></select></label>
    <label>Symbols <select id="symbols"><option value="default">Default (true)</option><option value="true">true — all symbols</option><option value="false">false — all labels</option><option value="keys">Keys only</option><option value="modifiers">Modifiers only</option><option value="partial">{ modifiers: false } — keys default to true</option></select></label>
    <label>Separator <select id="separator"><option value="default">Platform default (undefined)</option><option value="null">Platform default (null)</option><option value="empty">No separator</option><option value="custom">Custom</option></select></label>
    <label id="custom-label" hidden>Custom separator <input id="custom" value=" · "></label>
    <label class="check"><input id="layout" type="checkbox"> Supply a sample layoutMap (KeyQ → a, KeyS → o)</label>
    <label class="check"><input id="labels" type="checkbox"> Supply explicit keyLabels</label>
    <p class="note">Demo labels: KeyS → S, KeyQ → A, NumpadAdd → Numpad +, ArrowUp → Up. These are supplied labels, not inferred keyboard layouts.</p>
  </div><div class="preview" aria-live="polite"><p class="eyebrow">STRING OUTPUT</p><output id="string-output"></output><p class="eyebrow">PARTS AS KEYCAPS</p><div id="keycaps"></div><pre id="parts-output"></pre><p id="error" role="status"></p><pre id="snippet"></pre></div></section>
  <section><h2>Compare symbol settings</h2><p>All columns use your selected platform and separator. Keys and modifiers can be styled independently.</p><div class="table-scroll"><table><thead><tr><th>Binding</th><th>Default / true</th><th>Labels / false</th><th>Modifier symbols</th><th>Key symbols</th><th>parts: true</th></tr></thead><tbody id="comparisons"></tbody></table></div></section>
  <section><h2>Every input form</h2><p>Strings, raw logical objects, raw physical objects, and parsed bindings share the same formatter.</p><div class="input-grid" id="inputs"></div></section>
  <footer>Physical tokens such as <code>[KeyQ]</code> display a conventional letter label by default. Supply an already-resolved <code>layoutMap</code> for layout labels, or <code>keyLabels</code> for explicit overrides. The formatter never loads a layout map. <code>parts: true</code> returns an array and ignores the separator; a runtime boolean returns <code>string | string[]</code>.</footer></main>
`
// Typed lookups for the static playground controls created above.
const input = (id: string) => document.getElementById(id) as HTMLInputElement
const select = (id: string) => document.getElementById(id) as HTMLSelectElement
const element = (id: string) => document.getElementById(id)!
const samples = [
  'Mod+S',
  'Mod+Shift+ArrowUp',
  'Shift+ArrowDown',
  'Alt+ArrowLeft',
  'Control+ArrowRight',
  'Mod+Enter',
  'Mod+Tab',
  'Mod+Space',
  'Mod+Backspace',
  'Shift+Delete',
  'Escape',
  'Control++',
  'Alt+.',
  'Alt+ß',
  'Alt+[KeyS]',
  'Mod+[Digit2]',
  'Shift+[ArrowUp]',
  'Mod+[Slash]',
  '[NumpadEnter]',
  'Mod+[NumpadAdd]',
]
// A resolved sample map demonstrates layout lookup without calling browser APIs.
const layoutMap = new Map([
  ['KeyQ', 'a'],
  ['KeyS', 'o'],
])
const keyLabels = { KeyS: 'S', KeyQ: 'A', NumpadAdd: 'Numpad +', ArrowUp: 'Up' }
const symbolModes: Record<string, FormatDisplayOptions['useSymbols']> = {
  default: undefined,
  true: true,
  false: false,
  modifiers: { modifiers: true, keys: false },
  keys: { modifiers: false, keys: true },
  partial: { modifiers: false },
}
/** Renders each display part safely as text inside its own keycap. */
function keycaps(host: HTMLElement, parts: Array<string>) {
  host.replaceChildren(
    ...parts.map((part) => {
      const key = document.createElement('kbd')
      key.textContent = part
      return key
    }),
  )
}
/** Appends a text-only comparison cell, including user-supplied separator output. */
function cell(row: HTMLTableRowElement, text: string) {
  const td = row.insertCell()
  td.textContent = text
  return td
}
/** Recomputes previews, comparison rows, and input-form examples from the current controls. */
function render() {
  const platform = select('platform').value
  const separator = select('separator').value
  element('custom-label').hidden = separator !== 'custom'
  const options: FormatDisplayOptions & { parts?: false } = {
    ...(platform === 'auto'
      ? {}
      : { platform: platform as 'mac' | 'windows' | 'linux' }),
    useSymbols: symbolModes[select('symbols').value],
    separatorToken:
      separator === 'null'
        ? null
        : separator === 'empty'
          ? ''
          : separator === 'custom'
            ? input('custom').value
            : undefined,
    ...(input('labels').checked ? { keyLabels } : {}),
    ...(input('layout').checked ? { layoutMap } : {}),
  }
  const hotkey = input('binding').value
  const validation = validateHotkey(hotkey)
  element('error').textContent = validation.valid
    ? ''
    : validation.errors.join(' ')
  element('string-output').textContent = formatForDisplay(hotkey, options)
  const parts = formatForDisplay(hotkey, { ...options, parts: true })
  keycaps(element('keycaps'), parts)
  element('parts-output').textContent = JSON.stringify(parts)
  const optionCode = JSON.stringify(
    options,
    (_, value) => (value === layoutMap ? '__layoutMap__' : value),
    2,
  ).replace('"__layoutMap__"', 'layoutMap')
  element('snippet').textContent =
    (options.layoutMap
      ? `const layoutMap = new Map(${JSON.stringify([...layoutMap])})\n\n`
      : '') +
    `formatForDisplay(${JSON.stringify(hotkey)}, ${optionCode})\n\n// Render one <kbd> per part:\nformatForDisplay(${JSON.stringify(hotkey)}, {\n  ...options, parts: true\n})`
  const tbody = element('comparisons') as HTMLTableSectionElement
  tbody.replaceChildren()
  for (const sample of samples) {
    const row = tbody.insertRow()
    cell(row, sample)
    for (const useSymbols of [
      true,
      false,
      { modifiers: true, keys: false },
      { modifiers: false, keys: true },
    ]) {
      cell(row, formatForDisplay(sample, { ...options, useSymbols }))
    }
    keycaps(
      cell(row, ''),
      formatForDisplay(sample, { ...options, parts: true }),
    )
  }
  const forms: Array<{
    label: string
    code: string
    binding: RegisterableHotkeyChord | ParsedHotkeyChord
  }> = [
    {
      label: 'Logical string',
      code: "'Mod+Shift+ArrowUp'",
      binding: 'Mod+Shift+ArrowUp',
    },
    {
      label: 'Raw logical object',
      code: "{ key: 'ArrowUp', mod: true, shift: true }",
      binding: { key: 'ArrowUp', mod: true, shift: true },
    },
    { label: 'Physical string', code: "'Alt+[KeyQ]'", binding: 'Alt+[KeyQ]' },
    {
      label: 'Raw physical object',
      code: "{ code: 'KeyQ', alt: true }",
      binding: { code: 'KeyQ', alt: true },
    },
    {
      label: 'Parsed binding',
      code: "parseHotkey('Mod+Shift+ArrowUp', 'mac')",
      binding: parseHotkey('Mod+Shift+ArrowUp', 'mac'),
    },
    { label: 'Literal plus', code: "'Control++'", binding: 'Control++' },
  ]
  element('inputs').replaceChildren(
    ...forms.map(({ label, code, binding }) => {
      const card = document.createElement('article')
      const title = document.createElement('h3')
      title.textContent = label
      const source = document.createElement('code')
      source.textContent = code
      const result = document.createElement('div')
      result.className = 'form-result'
      keycaps(result, formatForDisplay(binding, { ...options, parts: true }))
      card.append(title, source, result)
      return card
    }),
  )
}
app.addEventListener('input', render)
render()