Displaying Hotkeys in Your Lit UI
Note
Showing hotkey labels on the page is one of the few parts of TanStack Hotkeys that needs extra care in server-rendered apps. The server doesn't know the user's operating system, so a label it renders can be wrong for the user, and Lit keeps the server's text after hydration. Lit has no provider, so pass the platform to formatForDisplay yourself. See Format labels in server-rendered apps.
Use formatForDisplay whenever a hotkey appears in a menu, button, tooltip, cheat sheet, or shortcut settings panel. It accepts chord strings, including physical ones such as 'Mod+[KeyS]', chord objects, parsed chords, and sequences. Store the hotkey itself, and format it when you render. A label is for reading, not for registering.
Format a hotkey#
import { formatForDisplay } from '@tanstack/lit-hotkeys'
formatForDisplay('Mod+S', { platform: 'mac' }) // '⌘ S'
formatForDisplay('Mod+[KeyS]', { platform: 'mac' }) // '⌘ S'
formatForDisplay({ code: 'KeyS', mod: true }, { platform: 'windows' }) // 'Ctrl+S'
formatForDisplay('Mod+[Digit2]', { platform: 'windows' }) // 'Ctrl+2'The same label can stand for different hotkeys. Mod+S follows the character S, and Mod+[KeyS] follows the key in the S position. See Match a key's position instead. Labels for positions shorten KeyS to S and Digit2 to 2, keep readable numpad labels, and reuse punctuation and special-key symbols. Formatting doesn't change the stored hotkey or guess the user's keyboard layout.
Omit platform to detect it in the browser. On macOS, the label joins modifier symbols with spaces. Windows and Linux use names joined with +. macOS orders modifiers as Control, Option, Shift, Command, so Mod+Shift+S displays as ⇧ ⌘ S, while the hotkey itself stays Mod+Shift+S.
formatForDisplay accepts hotkeys only, including single keys such as 'Escape' and modifiers on their own such as 'Shift'. A key from a KeyboardEvent, such as a held key, can be outside the key types: pass it as { key }.
Render individual keycaps#
Set parts: true to get one label per key instead of a joined string:
const saveHotkey = 'Mod+[KeyS]'
const parts = formatForDisplay(saveHotkey, { platform: 'mac', parts: true })
// ['⌘', 'S'], one label per <kbd>With parts omitted or false, the result is a string. A runtime boolean returns string | string[]. Parts keep literal plus keys and ignore separatorToken.
Format sequences#
Pass a sequence to format each step and join the steps with sequenceSeparator, a space by default:
formatForDisplay(['Mod+[KeyK]', 'C'], { platform: 'mac' }) // '⌘ K C'
formatForDisplay(['G', 'G'], { sequenceSeparator: ' → ' }) // 'G → G'
formatForDisplay(['Mod+[KeyK]', 'C'], { platform: 'mac', parts: true })
// [['⌘', 'K'], ['C']], one array of keycaps per stepWith parts: true, a sequence returns one array per step, so you can render each step as a group of keycaps. To show the steps a user has pressed so far, see Show sequences in your UI.
Choose symbols and separators#
useSymbols accepts a boolean or separate modifiers and keys settings. Omitted fields default to true. Modifier symbols apply on macOS. Windows and Linux keep modifier names.
formatForDisplay('Shift+[ArrowUp]', {
platform: 'mac',
useSymbols: { modifiers: false, keys: true },
parts: true,
}) // ['Shift', '↑']
formatForDisplay('Mod+[KeyS]', {
platform: 'mac', useSymbols: false,
}) // 'Cmd+S'
formatForDisplay('Control++', {
platform: 'windows', separatorToken: ' · ',
}) // 'Ctrl · +'An empty separator joins labels directly. undefined or null uses the platform default. separatorToken applies within a chord, and sequenceSeparator applies between steps.
Supply layout labels#
A position, such as [KeyQ], gets a fallback label that may differ from the character printed on the user's keyboard. Pass an already-resolved layoutMap to label positions for a known layout:
const layoutMap = new Map([['KeyQ', 'a']])
formatForDisplay('Mod+[KeyQ]', { platform: 'mac', layoutMap }) // '⌘ A'
formatForDisplay('Mod+Q', { platform: 'mac', layoutMap }) // '⌘ Q'Any object with get(code): string | undefined works, including a browser KeyboardLayoutMap. Formatting stays synchronous. Your app owns loading, errors, and refreshing the map: render fallback labels while it loads, then pass the resolved map on the next render. The library never requests it. Hotkeys that follow characters ignore layoutMap.
Use keyLabels for your own labels, keyed by physical code or by key name:
formatForDisplay('Mod+[KeyQ]', {
platform: 'mac', layoutMap, keyLabels: { KeyQ: 'Action' },
}) // '⌘ Action'keyLabels wins over a layout entry, which wins over the fallback label. Layout entries get normal letter casing and key symbols, and missing or empty entries fall back. All of these options affect display only.
Format labels in server-rendered apps#
With Lit SSR (@lit-labs/ssr), the server doesn't know the user's platform. A label formatted on the server without a platform uses 'linux', so a Mac user gets Ctrl+S in the server's HTML. Hydration doesn't fix it: Lit expects the browser's first render to match the server's HTML, and keeps the server's text.
Lit has no provider, so pass the platform yourself. Give the element a platform property, and pass it to formatForDisplay:
import { LitElement, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { formatForDisplay } from '@tanstack/lit-hotkeys'
import type { HotkeyPlatform } from '@tanstack/lit-hotkeys'
@customElement('save-button')
class SaveButton extends LitElement {
@property() platform?: HotkeyPlatform
render() {
return html`
<button>
Save <kbd>${formatForDisplay('Mod+S', { platform: this.platform })}</kbd>
</button>
`
}
}On the server, set the attribute from the request's User-Agent header:
import { render } from '@lit-labs/ssr'
import { html } from 'lit'
import { detectPlatform } from '@tanstack/lit-hotkeys'
const platform = detectPlatform(userAgent)
const result = render(html`<save-button platform=${platform}></save-button>`)The attribute is part of the server's HTML, so the element reads the same platform when it upgrades in the browser. Pass it on to child elements that show labels, such as .platform=${this.platform}. An element that renders only in the browser can call formatForDisplay(hotkey) without a platform.
The browser's KeyboardLayoutMap loads only in the browser, after hydration. Render fallback labels first and pass the map once it loads. keyLabels works anywhere, including on the server.
Pages rendered this way depend on the request's User-Agent header. If a CDN or another shared cache stores them, it can serve one user's labels, and the platform that Mod resolves to, to other users. Keep these pages out of shared caches, or add the platform to the cache key. Vary: User-Agent also works, but it splits the cache into many small entries.
Parse, compare, and store hotkeys#
parseHotkey returns each chord's key, as either a logical key or a physical code, plus its modifiers with Mod resolved. Check which one you have before reading it:
import { formatForDisplay, normalizeHotkey, parseHotkey } from '@tanstack/lit-hotkeys'
const parsed = parseHotkey('Mod+[KeyS]', 'mac')
if (parsed.code !== undefined) {
console.log(parsed.code) // 'KeyS'; parsed.key is undefined
}
const stored = normalizeHotkey(parsed, 'mac') // 'Mod+[KeyS]'
formatForDisplay(stored, { platform: 'windows' }) // 'Ctrl+S'A parsed chord's modifiers are already resolved for one platform. To store it as a hotkey that works on every platform, normalize it with that same platform, as above. normalizeHotkey also accepts chord strings, chord objects, and sequences. Store hotkeys, never display labels. Write a chord as a string or an object covers the forms a hotkey can take.
areHotkeysEqual(a, b) compares chords or sequences after resolving modifiers, so Mod+S equals Meta+S on macOS. Sequences compare step by step. isHotkeySequence narrows a Hotkey to a sequence, and simplifyHotkey turns a one-step sequence into its chord.
Use validateHotkey when you accept a hotkey from somewhere else, such as a settings file. It returns valid, errors, and warnings, and names the failing step of a sequence. It can't promise that the browser or operating system delivers the shortcut. To check hotkeys as users record them, see Validate and check conflicts.
For a full list of your app's shortcuts, see Cheat Sheet UI Helpers.