useEscapeKeydown

Listen for Escape key presses with optional owner document tracking.

useEscapeKeydown

useEscapeKeydown is a custom React hook that attaches a listener for Escape key presses. It's commonly used in modals, overlays, and dismissable components to close on Escape.

This hook is used internally by Dialog, Sheet, DropdownMenu, and other dismissable components.


API

function useEscapeKeydown(
  onEscapeKeyDownProp?: (event: KeyboardEvent) => void,
  ownerDocument: Document = globalThis?.document
): void

Parameters

Prop

Type


Return value

Returns nothing (void). The hook manages the event listener lifecycle automatically.


Features

  • Automatic cleanup: Removes listener when component unmounts
  • Callback ref: Uses useCallbackRef internally for stable callback handling
  • Capture phase: Attaches listener in capture phase for reliable event detection
  • Optional document: Supports custom owner documents (useful for iframes, portals)

Usage

Close modal on Escape

import { useEscapeKeydown } from '@loke/ui/use-escape-keydown';
import { useState } from 'react';

export function ModalExample() {
  const [open, setOpen] = useState(false);

  useEscapeKeydown(() => {
    setOpen(false);
  });

  return (
    <>
      <button onClick={() => setOpen(true)}>Open Modal</button>
      {open && (
        <div className="modal">
          <h2>Modal</h2>
          <button onClick={() => setOpen(false)}>Close</button>
        </div>
      )}
    </>
  );
}

With custom owner document

import { useEscapeKeydown } from '@loke/ui/use-escape-keydown';
import { useRef } from 'react';

export function IframeModal() {
  const iframeRef = useRef<HTMLIFrameElement>(null);

  useEscapeKeydown(
    () => {
      console.log('Escape pressed in iframe');
    },
    iframeRef.current?.contentDocument
  );

  return <iframe ref={iframeRef} />;
}

Conditional attachment

import { useEscapeKeydown } from '@loke/ui/use-escape-keydown';
import { useState } from 'react';

export function ConditionalEscape() {
  const [open, setOpen] = useState(false);

  useEscapeKeydown(open ? () => setOpen(false) : undefined);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open</button>
      {open && <div>Content (close with Escape)</div>}
    </>
  );
}

Common patterns

With dismissable overlay

import { useEscapeKeydown } from '@loke/ui/use-escape-keydown';

export function Overlay({ isOpen, onClose }) {
  useEscapeKeydown(() => {
    if (isOpen) onClose();
  });

  if (!isOpen) return null;

  return (
    <div className="overlay" onClick={onClose}>
      <div className="overlay-content" onClick={(e) => e.stopPropagation()}>
        Content
      </div>
    </div>
  );
}

With focus trap

import { useEscapeKeydown } from '@loke/ui/use-escape-keydown';
import { useRef, useState } from 'react';

export function FocusTrapOverlay() {
  const [open, setOpen] = useState(false);
  const contentRef = useRef<HTMLDivElement>(null);

  useEscapeKeydown(() => setOpen(false));

  return (
    <>
      <button onClick={() => setOpen(true)}>Open</button>
      {open && (
        <div ref={contentRef} role="dialog">
          Focus is trapped here. Press Escape to close.
        </div>
      )}
    </>
  );
}

Implementation details

The hook:

  1. Wraps the callback with useCallbackRef to ensure a stable reference
  2. Sets up a keydown event listener on the document in capture phase
  3. Filters for event.key === "Escape"
  4. Calls the callback when Escape is detected
  5. Cleans up the listener when the component unmounts

The capture phase ensures the listener fires before other handlers, allowing proper dismissal even if child elements would normally prevent event propagation.


Notes

  • The callback is optional; passing undefined is safe and no listener is attached
  • The listener is in capture phase, so it fires before bubbling handlers
  • The hook doesn't prevent default behavior; you control what happens on Escape
  • Works in all modern browsers and SSR environments (with globalThis?.document checks)

Import

import { useEscapeKeydown } from '@loke/ui/use-escape-keydown';