React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

keyboard-eventskeydownshortcutsaccessibilityreact-19typescript

Keyboard Events

Respond to key presses, releases, and keyboard shortcuts in React components.

Keyboard Event Reference

React PropTypeScript TypeFires WhenNotes
onKeyDownReact.KeyboardEvent<T>A key is pressed downPrimary keyboard event -- use this one
onKeyUpReact.KeyboardEvent<T>A key is releasedFires after the key action completes
onKeyPressReact.KeyboardEvent<T>A character key is pressedDeprecated -- do not use in new code

Recipe

Quick-reference recipe card -- copy-paste ready.

// Enter key to submit
function EnterSubmit() {
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") {
      e.preventDefault();
      console.log("Submitted:", e.currentTarget.value);
    }
  };
 
  return <input onKeyDown={handleKeyDown} placeholder="Press Enter" />;
}
 
// Escape key to clear / close
function EscapeToClear() {
  const [value, setValue] = React.useState("");
 
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Escape") {
      setValue("");
      e.currentTarget.blur();
    }
  };
 
  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
      onKeyDown={handleKeyDown}
      placeholder="Escape to clear"
    />
  );
}
 
// Keyboard shortcut with modifier (Cmd/Ctrl + S)
function SaveShortcut() {
  React.useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "s") {
        e.preventDefault();
        console.log("Save triggered");
      }
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, []);
 
  return <div>Press Cmd+S (Mac) or Ctrl+S (Windows) to save</div>;
}

When to reach for this: You need to respond to keyboard input -- submitting on Enter, closing on Escape, navigating with arrow keys, or registering global keyboard shortcuts.

Working Example

"use client";
 
import { useState, useEffect, useRef, useCallback } from "react";
 
type SearchResult = { id: string; title: string };
 
const MOCK_RESULTS: SearchResult[] = [
  { id: "1", title: "Getting Started with React" },
  { id: "2", title: "React Hooks in Depth" },
  { id: "3", title: "Server Components Explained" },
  { id: "4", title: "TypeScript with React" },
  { id: "5", title: "React Performance Patterns" },
];
 
export default function SearchWithShortcuts() {
  const [query, setQuery] = useState("");
  const [isOpen, setIsOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);
 
  const results = query
    ? MOCK_RESULTS.filter((r) =>
        r.title.toLowerCase().includes(query.toLowerCase())
      )
    : [];
 
  // Global keyboard shortcut: Cmd+K to open search
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") {
        e.preventDefault();
        setIsOpen(true);
        // Focus after state update renders the input
        setTimeout(() => inputRef.current?.focus(), 0);
      }
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, []);
 
  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>) => {
      switch (e.key) {
        case "ArrowDown":
          e.preventDefault();
          setSelectedIndex((i) => Math.min(i + 1, results.length - 1));
          break;
        case "ArrowUp":
          e.preventDefault();
          setSelectedIndex((i) => Math.max(i - 1, 0));
          break;
        case "Enter":
          e.preventDefault();
          if (results[selectedIndex]) {
            console.log("Selected:", results[selectedIndex].title);
            setIsOpen(false);
            setQuery("");
          }
          break;
        case "Escape":
          setIsOpen(false);
          setQuery("");
          inputRef.current?.blur();
          break;
      }
    },
    [results, selectedIndex]
  );
 
  if (!isOpen) {
    return (
      <button
        onClick={() => {
          setIsOpen(true);
          setTimeout(() => inputRef.current?.focus(), 0);
        }}
        style={{
          padding: "8px 16px",
          border: "1px solid #e5e7eb",
          borderRadius: "8px",
          background: "#fff",
          cursor: "pointer",
        }}
      >
        Search...{" "}
        <kbd style={{ color: "#9ca3af", fontSize: "0.85em" }}>Cmd+K</kbd>
      </button>
    );
  }
 
  return (
    <div
      style={{
        border: "1px solid #e5e7eb",
        borderRadius: "12px",
        overflow: "hidden",
        boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
        width: 400,
      }}
    >
      <input
        ref={inputRef}
        value={query}
        onChange={(e) => {
          setQuery(e.target.value);
          setSelectedIndex(0);
        }}
        onKeyDown={handleKeyDown}
        placeholder="Type to search... (Esc to close)"
        style={{
          width: "100%",
          padding: "12px 16px",
          border: "none",
          outline: "none",
          fontSize: "16px",
          boxSizing: "border-box",
        }}
      />
      {results.length > 0 && (
        <ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
          {results.map((result, index) => (
            <li
              key={result.id}
              style={{
                padding: "10px 16px",
                background: index === selectedIndex ? "#f3f4f6" : "#fff",
                cursor: "pointer",
              }}
              onClick={() => {
                console.log("Selected:", result.title);
                setIsOpen(false);
                setQuery("");
              }}
            >
              {result.title}
            </li>
          ))}
        </ul>
      )}
      {query && results.length === 0 && (
        <p style={{ padding: "10px 16px", color: "#9ca3af", margin: 0 }}>
          No results found.
        </p>
      )}
    </div>
  );
}

What this demonstrates:

  • Global keyboard shortcut (Cmd+K) using useEffect with document.addEventListener
  • Local onKeyDown handler for arrow key navigation, Enter to select, and Escape to close
  • Tracking selectedIndex state for keyboard-driven list navigation
  • Combining React keyboard events with controlled input state
  • Properly cleaning up global listeners in the useEffect return function

Deep Dive

How It Works

  • React keyboard events wrap the native KeyboardEvent in a Synthetic Event. The handler receives a React.KeyboardEvent<T> with all the standard properties: key, code, altKey, ctrlKey, metaKey, shiftKey, repeat.
  • onKeyDown fires when a key is pressed. It fires repeatedly if the key is held down (the repeat property is true on subsequent fires).
  • onKeyUp fires once when the key is released. Use it when you need the action only after the key is fully pressed and released.
  • onKeyPress is deprecated and removed from the DOM spec. It does not fire for non-character keys (Escape, Arrow, Ctrl, etc.). Always use onKeyDown instead.
  • Keyboard events only fire on focused elements. To capture global shortcuts, attach a listener to document inside a useEffect.

Variations

Single key handler with e.key:

function KeyLogger() {
  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
    console.log(`Key: ${e.key}, Code: ${e.code}, Repeat: ${e.repeat}`);
  };
 
  return (
    <div tabIndex={0} onKeyDown={handleKeyDown}>
      Focus me and press any key
    </div>
  );
}

Modifier key combinations (Ctrl/Cmd+S to save):

function SaveHandler({ onSave }: { onSave: () => void }) {
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      const isMac = navigator.userAgent.includes("Mac");
      const modifier = isMac ? e.metaKey : e.ctrlKey;
 
      if (modifier && e.key === "s") {
        e.preventDefault();
        onSave();
      }
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, [onSave]);
 
  return null;
}

Arrow key navigation in a list:

function ArrowNavList({ items }: { items: string[] }) {
  const [activeIndex, setActiveIndex] = useState(0);
 
  const handleKeyDown = (e: React.KeyboardEvent<HTMLUListElement>) => {
    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        setActiveIndex((i) => (i + 1) % items.length);
        break;
      case "ArrowUp":
        e.preventDefault();
        setActiveIndex((i) => (i - 1 + items.length) % items.length);
        break;
      case "Home":
        e.preventDefault();
        setActiveIndex(0);
        break;
      case "End":
        e.preventDefault();
        setActiveIndex(items.length - 1);
        break;
    }
  };
 
  return (
    <ul tabIndex={0} onKeyDown={handleKeyDown} role="listbox">
      {items.map((item, i) => (
        <li
          key={item}
          role="option"
          aria-selected={i === activeIndex}
          style={{ background: i === activeIndex ? "#e0e7ff" : "transparent" }}
        >
          {item}
        </li>
      ))}
    </ul>
  );
}

Global keyboard shortcuts with a hook:

function useKeyboardShortcut(
  key: string,
  callback: () => void,
  options: { ctrl?: boolean; meta?: boolean; shift?: boolean } = {}
) {
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (options.ctrl && !e.ctrlKey) return;
      if (options.meta && !e.metaKey) return;
      if (options.shift && !e.shiftKey) return;
      if (e.key.toLowerCase() !== key.toLowerCase()) return;
 
      e.preventDefault();
      callback();
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, [key, callback, options.ctrl, options.meta, options.shift]);
}
 
// Usage
function App() {
  useKeyboardShortcut("k", () => openSearch(), { meta: true });
  useKeyboardShortcut("/", () => openSearch());
  useKeyboardShortcut("Escape", () => closeModal());
 
  return <div>...</div>;
}

Preventing default browser shortcuts:

function PreventBrowserDefault() {
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      // Prevent Ctrl+P (print) to open custom print dialog
      if ((e.metaKey || e.ctrlKey) && e.key === "p") {
        e.preventDefault();
        console.log("Custom print dialog");
      }
 
      // Prevent Ctrl+F (find) to open custom search
      if ((e.metaKey || e.ctrlKey) && e.key === "f") {
        e.preventDefault();
        console.log("Custom search");
      }
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, []);
 
  return <div>Browser shortcuts overridden</div>;
}

TypeScript Notes

// React.KeyboardEvent<T> -- T is the element type
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  e.currentTarget; // HTMLInputElement
  e.key;           // "Enter", "Escape", "a", "ArrowDown", etc.
  e.code;          // "Enter", "Escape", "KeyA", "ArrowDown", etc.
};
 
// e.key vs e.code
// e.key: the character produced ("a", "A", "/", "Enter")
//   -- affected by keyboard layout and modifier keys
// e.code: the physical key ("KeyA", "Slash", "Enter")
//   -- consistent regardless of layout
 
// Modifier key properties (all boolean)
// e.altKey   -- Alt (Option on Mac)
// e.ctrlKey  -- Control
// e.metaKey  -- Cmd on Mac, Windows key on Windows
// e.shiftKey -- Shift
// e.repeat   -- true if key is held down
 
// Native KeyboardEvent (for useEffect listeners)
useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    // This is the native DOM KeyboardEvent, NOT React.KeyboardEvent
    // Same properties, but different type
  };
  document.addEventListener("keydown", handler);
  return () => document.removeEventListener("keydown", handler);
}, []);
 
// Common pattern: typing a key handler map
type KeyHandlerMap = Record<string, (e: React.KeyboardEvent) => void>;
 
const handlers: KeyHandlerMap = {
  Enter: (e) => submit(),
  Escape: (e) => close(),
  ArrowDown: (e) => { e.preventDefault(); moveDown(); },
  ArrowUp: (e) => { e.preventDefault(); moveUp(); },
};
 
const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
  handlers[e.key]?.(e);
};

Gotchas

  • onKeyPress is deprecated -- It does not fire for non-character keys (Escape, arrows, function keys, modifiers). It has been removed from the DOM spec. Fix: Always use onKeyDown instead. The only reason to use onKeyUp is when you specifically need the action on key release.

  • e.key values differ from e.code -- e.key returns "a" or "A" depending on Shift; e.code always returns "KeyA". For keyboard shortcuts, use e.key with .toLowerCase() to match regardless of case. For game controls or physical key position, use e.code.

  • Keyboard events require focus -- A <div> does not receive keyboard events unless it has tabIndex={0} (or -1 for programmatic focus only). Fix: Add tabIndex to non-interactive elements that need keyboard handlers, or use global document.addEventListener for shortcuts that should work regardless of focus.

  • Global shortcuts leak across components -- If you register Cmd+K in a useEffect but the component unmounts, the listener persists and fires on a stale closure. Fix: Always return a cleanup function from useEffect that removes the listener.

  • e.repeat fires continuously when a key is held -- Holding down a key fires onKeyDown repeatedly. If your handler performs an expensive action (like an API call), it will fire dozens of times. Fix: Check if (e.repeat) return; at the top of your handler if you only want the first press.

  • Overriding browser shortcuts is unreliable -- Some browser shortcuts (like Ctrl+T, Ctrl+W, Ctrl+N) cannot be intercepted by JavaScript because the browser handles them before your code runs. Fix: Only override shortcuts you know are interceptable (Ctrl+S, Ctrl+P, Ctrl+F, etc.) and test across browsers.

  • Comparing e.key with string literals is fragile for special keys -- The key values like "Enter", "Escape", "ArrowDown" are case-sensitive and follow the UI Events spec. Do not compare against "enter" or "esc". Fix: Use the exact spec values: "Enter", "Escape", "ArrowDown", "ArrowUp", "Tab", etc.

Alternatives

AlternativeUse WhenDon't Use When
accessKey HTML attributeSimple one-key activation for buttons or linksYou need modifier combinations or complex logic
document.addEventListener in useEffectGlobal shortcuts that work regardless of focused elementThe shortcut is scoped to a specific input or component
Third-party libraries (react-hotkeys-hook, tinykeys)Many shortcuts, chord sequences, or scope managementYou have one or two simple shortcuts
ARIA keyboard patternsNavigating composite widgets (menus, tabs, listboxes)Simple form inputs that handle keys natively
contentEditableRich text editing with full keyboard controlStandard form inputs or navigation

FAQs

Which keyboard event should you use in React: onKeyDown, onKeyUp, or onKeyPress?
  • Use onKeyDown as your primary keyboard event -- it fires for all keys including non-character keys
  • Use onKeyUp only when you need the action on key release
  • Never use onKeyPress -- it is deprecated and does not fire for Escape, arrows, or modifier keys
What is the difference between e.key and e.code?
  • e.key returns the character produced ("a", "A", "Enter") and is affected by keyboard layout and modifiers
  • e.code returns the physical key ("KeyA", "Enter") and is consistent regardless of layout
  • Use e.key for shortcuts; use e.code for game controls or physical key position
How do you register a global keyboard shortcut like Cmd+K?
useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    if ((e.metaKey || e.ctrlKey) && e.key === "k") {
      e.preventDefault();
      openSearch();
    }
  };
  document.addEventListener("keydown", handler);
  return () => document.removeEventListener("keydown", handler);
}, []);
Why does a div not receive keyboard events by default?

Keyboard events only fire on focused elements. A <div> is not focusable by default. Add tabIndex={0} to make it keyboard-navigable, or tabIndex={-1} for programmatic focus only. Alternatively, use document.addEventListener for global shortcuts.

Gotcha: What happens if you hold down a key -- does onKeyDown fire once or repeatedly?

onKeyDown fires repeatedly when a key is held down. The e.repeat property is true on subsequent fires. If your handler does something expensive (like an API call), add if (e.repeat) return; at the top to only handle the first press.

How do you implement arrow key navigation in a list?
const handleKeyDown = (e: React.KeyboardEvent<HTMLUListElement>) => {
  switch (e.key) {
    case "ArrowDown":
      e.preventDefault();
      setIndex((i) => (i + 1) % items.length);
      break;
    case "ArrowUp":
      e.preventDefault();
      setIndex((i) => (i - 1 + items.length) % items.length);
      break;
  }
};
return <ul tabIndex={0} onKeyDown={handleKeyDown} role="listbox">...</ul>;
Gotcha: Why does my global shortcut listener fire after the component unmounts?

If you register a document.addEventListener in useEffect but forget the cleanup function, the listener persists after unmount and fires on a stale closure. Always return a cleanup function: return () => document.removeEventListener("keydown", handler);.

Can you intercept all browser keyboard shortcuts with e.preventDefault()?

No. Some shortcuts (Ctrl+T, Ctrl+W, Ctrl+N) are handled by the browser before your code runs and cannot be intercepted. Only override shortcuts you know are interceptable (Ctrl+S, Ctrl+P, Ctrl+F) and test across browsers.

How do you handle cross-platform modifier keys (Cmd on Mac, Ctrl on Windows)?
const handler = (e: KeyboardEvent) => {
  const isMac = navigator.userAgent.includes("Mac");
  const modifier = isMac ? e.metaKey : e.ctrlKey;
  if (modifier && e.key === "s") {
    e.preventDefault();
    save();
  }
};
Are the e.key string values case-sensitive?

Yes. Key values like "Enter", "Escape", "ArrowDown" follow the UI Events spec and are case-sensitive. Comparing against "enter" or "esc" will not match. Always use the exact spec values.

What is the TypeScript type difference between React.KeyboardEvent and the native KeyboardEvent?
  • React.KeyboardEvent<T> is the synthetic event type used in JSX handlers (e.g., onKeyDown)
  • KeyboardEvent (no React prefix) is the native DOM type used in useEffect with document.addEventListener
  • They have the same properties but are different TypeScript types
How do you create a typed key handler map in TypeScript?
type KeyHandlerMap = Record<string, (e: React.KeyboardEvent) => void>;
 
const handlers: KeyHandlerMap = {
  Enter: () => submit(),
  Escape: () => close(),
  ArrowDown: (e) => { e.preventDefault(); moveDown(); },
};
 
const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
  handlers[e.key]?.(e);
};
  • Mouse Events -- Handling clicks, hovers, and mouse movement
  • Form Events -- Handling form submissions and input changes