LEARNING OBJECTIVES ⌵
- Differentiate between semantic key values (
event.key), physical key positions (event.code), and legacy key codes (event.keyCode). - Handle internationalized text input and IME (Input Method Editor) character composition using
event.isComposing. - Implement robust, accessible modal focus traps compliant with WCAG 2.1.1 (Keyboard) and 2.1.2 (No Keyboard Trap).
- Build high-performance keyboard shortcut dispatchers using modifier flags (
shiftKey,ctrlKey,altKey,metaKey).
📖 The Mental Model & Story (Intuitive Foundation)
Imagine navigating a skyscraper where all elevators and stairs have been sealed, and your only method of movement is a sequential optical scanner stepping through one doorway at a time:
[ ENTRY DOOR (Trigger Button) ] ──> Clicks Open
|
v (Focus moves inside room)
+-------------------------------------------------------------------------------+
| ISOLATED MODAL ROOM |
| |
| [First Input] <── (Tab) ──> [Checkbox] <── (Tab) ──> [Close Button (Last)] |
| ^ | |
| +──────────────── (Tab loops forward) ─────────────────+ |
| +────────────── (Shift+Tab loops backward) ────────────+ |
+-------------------------------------------------------------------------------+
|
v (Presses ESC or Close Button)
[ ENTRY DOOR (Trigger Button) ] <── Focus Restored!
Millions of users—including screen reader users, motor-impaired individuals, power users, and developers—navigate web applications exclusively using keyboards. If a modal dialog opens and tabbing moves focus behind the modal into the obscured page, the application fails fundamental accessibility standards.
Technical Deep Dive & Specifications
1. event.key vs event.code vs event.keyCode
| Property | Description | Example (US Layout) | Example (French AZERTY) | Usage Recommendation |
|---|---|---|---|---|
event.key |
The printable character or semantic function produced by the keypress. | "a", "A", "Enter", "Escape" |
"a", "A", "Enter", "Escape" |
✅ Standard for UI Actions & Shortcuts |
event.code |
The physical hardware key location on the physical keyboard layout. | "KeyQ" |
"KeyA" (Same physical key slot) |
✅ Standard for Games (WASD Movement) |
event.keyCode |
Deprecated numerical ASCII code. | 65 |
65 |
❌ Do NOT Use (Deprecated) |
window.addEventListener('keydown', (e) => {
console.log({
key: e.key, // "Escape", "Enter", "ArrowDown", "a", "A"
code: e.code, // "Escape", "Enter", "ArrowDown", "KeyA"
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
metaKey: e.metaKey // Command on Mac, Windows key on PC
});
});
2. IME Composition (event.isComposing)
When users type in languages requiring character composition (Japanese Kanji, Chinese Pinyin, Korean Hangul), the operating system displays an intermediate composition window.
- During composition, the user presses
Enterto confirm phonetic character choices, not to submit the form! - Rule: Always check
if (event.isComposing || event.keyCode === 229) return;before processingEnterorEscapekeys.
3. The Accessible Modal Focus Trap Algorithm
To conform to WCAG 2.2 Success Criterion 2.1.2 (No Keyboard Trap):
- Save Previous Active Element: Before opening the dialog, save
const previousActiveElement = document.activeElement;. - Find Focusable Descendants: Query all focusable nodes inside the modal:
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'; const focusableElements = modal.querySelectorAll(FOCUSABLE_SELECTOR); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; - Trap Focus on
Tab/Shift+Tab:- If
e.key === 'Tab'ande.shiftKey(backward tab) onfirstElement, prevent default and focuslastElement. - If
e.key === 'Tab'and!e.shiftKey(forward tab) onlastElement, prevent default and focusfirstElement.
- If
- Listen for
Escape: Close modal and restore focus topreviousActiveElement.focus(). - Background Inertness: Add the
inertattribute to background siblings (<main inert>,<header inert>) so assistive tech cannot interact with background elements while the modal is open.
💻 Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Line 57 (
previousActiveElement = document.activeElement;): Saves the button that launched the modal so focus can be returned when the dialog closes. - Line 59 (
mainContent.setAttribute('inert', '')): Standard HTMLinertattribute disables pointer events, tab focusing, and screen reader access for background elements. - Lines 82–98 (
handleKeyDown): InterceptsTabnavigation. When a user pressesTabon the "Save Changes" button, focus wraps seamlessly to the first<input>. - Line 72 (
previousActiveElement.focus()): WCAG requirement: restoring focus back to the opener button ensures users do not lose their place in the document.
Expected Browser Render Output
- Pressing
Tabinside the modal endlessly cycles between the two input fields and two buttons. - Pressing
Escapecloses the modal immediately and places the focus outline back on "Open Account Settings".
🏋️ Hands-On Exercise
🎯 The Challenge: Build a Roving Tabindex Toolbar
Instructions:
- Create a rich text editor toolbar (
<div role="toolbar" id="toolbar">) containing 4 buttons: Bold, Italic, Underline, and Code. - Implement the Roving Tabindex Pattern:
- Only the currently active toolbar button has
tabindex="0"; all other buttons havetabindex="-1". - Pressing ArrowRight moves focus to the next button (wrapping from last to first).
- Pressing ArrowLeft moves focus to the previous button (wrapping from first to last).
- Pressing Home focuses the first button, and End focuses the last button.
- Only the currently active toolbar button has
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Using
event.keyCode:keyCodeis deprecated. Writingif (e.keyCode === 27)fails in modern TypeScript strict mode. Always useif (e.key === 'Escape'). - Ignoring IME Input: Submitting a form on
Enterwithout checkinge.isComposingdisrupts Asian language users composing characters. - Creating Accidental Infinite Focus Loops: Tabbing into non-focusable elements will break focus cycling. Always filter focusable queries with
:not([disabled]):not([tabindex="-1"]).
💡 Pro Tips
- The Native
<dialog>Element: In modern HTML,<dialog>.showModal()automatically implements backdrop isolation, Escape key closing, and focus restoration out of the box! - Mac vs Windows Shortcut Normalization: Support both platforms by checking
const isCmdOrCtrl = e.metaKey || e.ctrlKey;for shortcuts likeCmd+S/Ctrl+S.
📌 Key Takeaways
- Use
event.keyfor semantic character checks ("Enter","Escape") andevent.codefor physical key slots ("KeyW"). - Always guard with
if (event.isComposing) return;to support international IME composition. - Modal dialogs must trap focus with
Tab/Shift+Tab, close onEscape, and restore focus to the opening element. - Roving
tabindex(0on active,-1on siblings) provides accessible arrow key navigation inside toolbars, tabs, and menus. - --