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
): voidParameters
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
useCallbackRefinternally 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:
- Wraps the callback with
useCallbackRefto ensure a stable reference - Sets up a
keydownevent listener on the document in capture phase - Filters for
event.key === "Escape" - Calls the callback when Escape is detected
- 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
undefinedis 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?.documentchecks)
Import
import { useEscapeKeydown } from '@loke/ui/use-escape-keydown';