Keyboard Shortcuts Help Overlay — Press "?" Without Hijacking Text Input

Keyboard Shortcuts Help Overlay — Press "?" to Open · Misc · Plain HTML, CSS & JS · Live preview

What's included

Features

Correctly distinguishes a "?" keyboard shortcut from a literal question mark typed into a text field or contenteditable element
Single declarative SHORTCUT_GROUPS data structure drives the entire rendered overlay, with no hardcoded markup per shortcut
isTypingContext() guard pattern is reusable for any additional global hotkey a real app needs to add safely
Escape key and outside-click both close the overlay through the same shared close function
Focus management on open/close follows standard accessible modal conventions — focus moves in, then correctly returns to its origin
Grouped, scannable shortcut reference layout with visually distinct key badges for multi-key combinations
role="dialog" and aria-labelledby correctly identify the overlay to assistive technology

About this UI Snippet

Keyboard Shortcuts Overlay — Getting the "?" Trigger Actually Correct

Screenshot of the Keyboard Shortcuts Help Overlay — Press "?" to Open snippet rendered live

Binding a help overlay to the "?" key is a beloved pattern from tools like Gmail, Linear, and GitHub — but naively listening for keydown with key === '?' anywhere on the page breaks the moment a user is typing into a search box, a comment field, or any text input and genuinely wants to type a literal question mark. This snippet implements the one guard that makes the pattern actually safe to ship: checking whether the currently focused element is a text-input context before treating "?" as a shortcut trigger.

`isTypingContext()` — the one check that makes global hotkeys safe

Before treating any keypress as a shortcut, isTypingContext() checks document.activeElement's tag name against INPUT and TEXTAREA, and also checks isContentEditable (which catches rich-text editors built on a contenteditable div rather than a native form element). Only when none of these match does the "?" keypress get treated as "open the shortcuts overlay" — otherwise, the keypress is left completely alone, letting the browser's normal text-input behavior insert the literal "?" character exactly as the user intended.

This same guard pattern applies to every global shortcut, not just "?"

While this snippet only wires up one shortcut for real (the rest are documented in the overlay but not implemented, since they're illustrative), the isTypingContext() check is exactly the pattern any additional global hotkey in a real app would need — a "G then H" go-to-home shortcut, a "/" focus-search shortcut, anything bound at the document level has to make this same distinction, or it will break text entry the same way an unchecked "?" binding would.

One declarative shortcut list drives both the displayed reference and (in a real app) the actual bindings

SHORTCUT_GROUPS is a plain nested data structure — group titles, and within each group, a description paired with an array of key names. renderShortcuts() builds the entire overlay's HTML from this one array. In a real application, this same array is exactly what you'd also iterate over to *register* each shortcut's actual keyboard handler — keeping the reference documentation and the real key bindings sourced from one place means they can never drift apart from each other (a common real bug: a shortcuts overlay that lists a shortcut which was since removed from the actual app, or vice versa).

Focus management on open and close, matching standard modal conventions

openOverlay() records document.activeElement before showing the overlay and moves focus to its close button; closeOverlay() restores focus to whatever had it before. This is the same focus-restoration convention any accessible modal needs — a keyboard user who opened the overlay from anywhere on the page (not necessarily a specific "help" button) has their exact keyboard position preserved and correctly returned to them once the overlay closes.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain why checking the focused element's context before treating a keypress as a global shortcut is necessary, and to list which other HTML elements or attributes (beyond input, textarea, and contenteditable) might also need to be excluded in a more thorough implementation. It's also worth asking for a version that actually wires up and executes each documented shortcut's real action (not just displaying it as reference text), reusing the same SHORTCUT_GROUPS data structure to both render the overlay and register the real keydown handlers from one single source of truth.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "?"-triggered keyboard shortcuts help overlay in HTML, CSS, and vanilla JavaScript — no external library.

Requirements:
- A page with at least one visible text input field, and a global keydown listener that opens a shortcuts reference overlay when the "?" key is pressed.
- Before treating a "?" keypress as a shortcut trigger, check whether the currently focused element is a text-input context (an <input>, a <textarea>, or any element with isContentEditable true) — if so, do NOT open the overlay or call preventDefault, allowing the character to be typed normally into that field instead.
- Define the entire list of documented shortcuts as one single declarative data structure (grouped by category, each entry with a description and its key combination), and generate all of the overlay's HTML content from that one data structure — no shortcut entry should be hardcoded directly into the markup.
- The overlay must be closable via the Escape key, a visible close button, and a click on its backdrop outside the modal content — all three should call the same shared close function.
- Implement standard accessible modal focus management: record whatever element has focus immediately before the overlay opens, move focus into the overlay when it opens, and restore focus to that originally recorded element when the overlay closes.
- Give the overlay appropriate ARIA attributes (role="dialog", aria-modal, aria-labelledby pointing to its heading) for correct assistive technology support.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="demo">
  <div class="app-mock">
    <div class="app-mock-header">Docs Editor</div>
    <p class="app-mock-hint">Press <kbd>?</kbd> anywhere on this page (as long as you're not typing in a field) to open the shortcuts overlay.</p>
    <input type="text" class="app-mock-input" placeholder="Try typing here — ? types a literal question mark instead of opening the overlay" />
    <button class="app-mock-btn" id="openShortcutsBtn">Or click here to open it</button>
  </div>

  <div class="shortcuts-overlay" id="shortcutsOverlay" hidden>
    <div class="shortcuts-modal" role="dialog" aria-modal="true" aria-labelledby="shortcutsTitle">
      <div class="shortcuts-header">
        <h2 id="shortcutsTitle">Keyboard shortcuts</h2>
        <button class="shortcuts-close" id="shortcutsClose" aria-label="Close shortcuts overlay">×</button>
      </div>

      <div class="shortcuts-groups" id="shortcutsGroups"></div>
    </div>
  </div>
</div>

Step by step

How to Use

  1. 1
    Press "?" anywhere on the pageAs long as focus isn't inside a text field, the shortcuts overlay opens immediately, listing every documented shortcut grouped by category.
  2. 2
    Click into the text input and try "?"It types a literal question mark instead of opening the overlay — isTypingContext() correctly detects the input has focus and skips the shortcut handling entirely.
  3. 3
    Press Escape or click outside the overlayBoth close it, restoring keyboard focus to whatever element had it before the overlay opened.
  4. 4
    Add a new shortcut to the referenceAdd an entry to the relevant group in SHORTCUT_GROUPS (or a new group) — renderShortcuts() picks it up automatically with no other markup changes needed.
  5. 5
    Wire a real shortcut's actual behaviorFor any implemented (not just documented) shortcut, apply the same isTypingContext() guard before running its handler, exactly as the "?" trigger does.

Real-world uses

Common Use Cases

PRODUCTIVITY
Power-user productivity tools
Any app with meaningful keyboard shortcuts (project management tools, editors, dashboards) benefits from a discoverable, "?"-triggered reference.
EDITOR
Text and document editors
The exact context where getting the "?" guard right matters most, since users are constantly typing and genuinely need literal question marks to work.
ADMIN
Internal admin and ops tools
Internal tools used daily by the same power users benefit disproportionately from keyboard shortcuts and a quick way to recall them.
ONBOARDING
New user shortcut discovery
A shortcuts overlay is often how users first discover keyboard shortcuts exist at all in a product, rather than stumbling onto them by accident.
Related: Before/After Image Slider — CSS Only Radio Steps (No JavaScript)
See the Before/After Image Slider — CSS Only Radio Steps (No JavaScript) for a related misc pattern worth pairing with this one.
Related: Product Image Magnify Lens
See the Product Image Magnify Lens for a related misc pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

It correctly types a literal "?" character and does NOT open the shortcuts overlay. isTypingContext() checks whether the currently focused element is an input, textarea, or contenteditable element before treating any keypress as a shortcut, and skips the shortcut handling entirely when it is.

The isTypingContext() check is written as a reusable pattern specifically because every global keyboard shortcut a real app adds needs the same guard — any additional hotkey (like a "/" focus-search shortcut) should call this same check before running its own handler.

Entirely from the SHORTCUT_GROUPS data structure — a plain array of groups, each containing a title and a list of {keys, description} entries. renderShortcuts() builds the full HTML from this array, so adding, removing, or reordering shortcuts only requires editing that one data structure.

No — most of the listed shortcuts (like Ctrl+B for bold, or "G then H" for navigation) are illustrative reference entries in this demo, not wired to real handlers. In a real app, you would iterate the same SHORTCUT_GROUPS data to also register each shortcut's actual keydown handler, applying the same isTypingContext() guard to each one.

Rich text editors are frequently built on a contenteditable div rather than a native <input> or <textarea>, so checking only tag names would miss that case entirely and incorrectly treat "?" typed into a rich text editor as a shortcut trigger instead of a literal character.

openOverlay() records whatever element currently has focus before showing the overlay and moves focus to its close button. closeOverlay() restores focus back to that recorded element — the same focus-preservation convention any accessible modal should follow.