Eventos de Teclado
Responde a pulsaciones de teclas, liberaciones y atajos de teclado en componentes React.
Busca en todas las páginas de la documentación
Responde a pulsaciones de teclas, liberaciones y atajos de teclado en componentes React.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| React Prop | Tipo TypeScript | Se dispara cuando | Notas |
|---|---|---|---|
onKeyDown | React.KeyboardEvent<T> | Se presiona una tecla | Evento de teclado principal -- usa este |
onKeyUp | React.KeyboardEvent<T> | Se libera una tecla | Se dispara después de que se completa la acción de la tecla |
onKeyPress | React.KeyboardEvent<T> | Se presiona una tecla de carácter | Deprecado -- no uses en código nuevo |
Tarjeta de receta de referencia rápida -- lista para copiar y pegar.
// Tecla Enter para enviar
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="Presiona Enter" />;
}
// Tecla Escape para limpiar / cerrar
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 para limpiar"
/>
);
}
// Atajo de teclado con modificador (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>Presiona Cmd+S (Mac) o Ctrl+S (Windows) para guardar</div>;
}Cuándo usarlo: Necesitas responder a la entrada del teclado -- enviar al presionar Enter, cerrar al presionar Escape, navegar con las teclas de flecha o registrar atajos de teclado globales.
"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())
)
: [];
// Atajo de teclado global: Cmd+K para abrir búsqueda
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setIsOpen(true);
// Enfoca después de que la actualización de estado renderiza la entrada
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",
}}
>
Buscar...{" "}
<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="Escribe para buscar... (Esc para cerrar)"
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 se encontraron resultados.
</p>
)}
</div>
);
}Lo que esto demuestra:
Cmd+K) usando useEffect con document.addEventListeneronKeyDown para navegación con teclas de flecha, Enter para seleccionar y Escape para cerrarselectedIndex para navegación de lista impulsada por tecladouseEffectKeyboardEvent nativo en un Synthetic Event. El manejador recibe un React.KeyboardEvent<T> con todas las propiedades estándar: key, code, altKey, ctrlKey, metaKey, shiftKey, repeat.onKeyDown se dispara cuando se presiona una tecla. Se dispara repetidamente si la tecla se mantiene presionada (la propiedad repeat es true en los disparos posteriores).onKeyUp se dispara una vez cuando se libera la tecla. Úsalo cuando necesites la acción solo después de que la tecla se haya presionado y liberado completamente.onKeyPress está deprecado y se eliminó de la especificación del DOM. No se dispara para teclas que no son de carácter (Escape, Arrow, Ctrl, etc.). Siempre usa onKeyDown en su lugar.document dentro de un useEffect.Manejador de tecla única con 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}>
Enfócame y presiona cualquier tecla
</div>
);
}Combinaciones de teclas modificadoras (Ctrl/Cmd+S para guardar):
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;
}Navegación con teclas de flecha en una lista:
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>
);
}Atajos de teclado globales con un 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]);
}
// Uso
function App() {
useKeyboardShortcut("k", () => openSearch(), { meta: true });
useKeyboardShortcut("/", () => openSearch());
useKeyboardShortcut("Escape", () => closeModal());
return <div>...</div>;
}Prevención de atajos de navegador predeterminados:
function PreventBrowserDefault() {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
// Prevenir Ctrl+P (imprimir) para abrir cuadro de diálogo de impresión personalizado
if ((e.metaKey || e.ctrlKey) && e.key === "p") {
e.preventDefault();
console.log("Custom print dialog");
}
// Prevenir Ctrl+F (buscar) para abrir búsqueda personalizada
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>Atajos de navegador sobrescritos</div>;
}// React.KeyboardEvent<T> -- T es el tipo de elemento
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: el carácter producido ("a", "A", "/", "Enter")
// -- afectado por el diseño del teclado y las teclas modificadoras
// e.code: la tecla física ("KeyA", "Slash", "Enter")
// -- consistente independientemente del diseño
// Propiedades de teclas modificadoras (todas booleanas)
// e.altKey -- Alt (Option en Mac)
// e.ctrlKey -- Control
// e.metaKey -- Cmd en Mac, tecla de Windows en Windows
// e.shiftKey -- Shift
// e.repeat -- true si la tecla se mantiene presionada
// KeyboardEvent nativo (para escuchadores useEffect)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
// Este es el KeyboardEvent nativo del DOM, NOT React.KeyboardEvent
// Mismas propiedades, pero diferente tipo
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
// Patrón común: escribir un mapa de manejador de teclas
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);
};onKeyPress está deprecado -- No se dispara para teclas que no son de carácter (Escape, flechas, teclas de función, modificadores). Se ha eliminado de la especificación del DOM. Solución: Siempre usa onKeyDown en su lugar. La única razón para usar onKeyUp es cuando específicamente necesites la acción en la liberación de la tecla.
Los valores de e.key difieren de e.code -- e.key devuelve "a" o "A" dependiendo de Shift; e.code siempre devuelve "KeyA". Para atajos de teclado, usa e.key con .toLowerCase() para hacer coincidir independientemente de mayúsculas. Para controles de juegos o posición de tecla física, usa e.code.
Los eventos de teclado requieren foco -- Un <div> no recibe eventos de teclado a menos que tenga tabIndex={0} (o -1 solo para foco programático). Solución: Agrega tabIndex a elementos no interactivos que necesiten manejadores de teclado, o usa document.addEventListener global para atajos que deben funcionar independientemente del foco.
Los atajos globales se filtran entre componentes -- Si registras Cmd+K en un useEffect pero el componente se desmonta, el escuchador persiste y se dispara en un cierre obsoleto. Solución: Siempre devuelve una función de limpieza de useEffect que elimine el escuchador.
e.repeat se dispara continuamente cuando se mantiene presionada una tecla -- Mantener presionada una tecla dispara onKeyDown repetidamente. Si tu manejador realiza una acción costosa (como una llamada a API), se disparará docenas de veces. Solución: Verifica if (e.repeat) return; en la parte superior de tu manejador si solo quieres la primera pulsación.
Anular atajos de navegador es poco confiable -- Algunos atajos de navegador (como Ctrl+T, Ctrl+W, Ctrl+N) no se pueden interceptar por JavaScript porque el navegador los maneja antes de que se ejecute tu código. Solución: Solo anula atajos que sabes que son interceptables (Ctrl+S, Ctrl+P, Ctrl+F, etc.) y prueba en todos los navegadores.
Comparar e.key con literales de cadena es frágil para teclas especiales -- Los valores key como "Enter", "Escape", "ArrowDown" distinguen mayúsculas y minúsculas y siguen la especificación UI Events. No compares contra "enter" o "esc". Solución: Usa los valores exactos de la especificación: "Enter", "Escape", "ArrowDown", "ArrowUp", "Tab", etc.
| Alternativa | Úsalo cuando | No lo uses cuando |
|---|---|---|
Atributo HTML accessKey | Activación simple de una tecla para botones o enlaces | Necesitas combinaciones de modificadores o lógica compleja |
document.addEventListener en useEffect | Atajos globales que funcionan independientemente del elemento enfocado | El atajo se limita a una entrada o componente específico |
| Librerías de terceros (react-hotkeys-hook, tinykeys) | Muchos atajos, secuencias de acordes o gestión de alcance | Tienes uno o dos atajos simples |
| Patrones de teclado ARIA | Navegación de widgets compuestos (menús, pestañas, listboxes) | Entradas de formulario simples que manejan teclas de forma nativa |
contentEditable | Edición de texto enriquecido con control de teclado completo | Entradas de formulario estándar o navegación |
onKeyDown como tu evento de teclado principal -- se dispara para todas las teclas incluyendo teclas que no son de carácteronKeyUp solo cuando necesites la acción en la liberación de la teclaonKeyPress -- está deprecado y no se dispara para Escape, flechas, o teclas modificadorase.key devuelve el carácter producido ("a", "A", "Enter") y es afectado por el diseño del teclado y los modificadorese.code devuelve la tecla física ("KeyA", "Enter") y es consistente independientemente del diseñoe.key para atajos; usa e.code para controles de juegos o posición de tecla físicauseEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
openSearch();
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);Los eventos de teclado solo se disparan en elementos enfocados. Un <div> no es enfocable por defecto. Agrega tabIndex={0} para hacerlo navegable por teclado, o tabIndex={-1} solo para foco programático. Alternativamente, usa document.addEventListener para atajos globales.
onKeyDown se dispara repetidamente cuando se mantiene presionada una tecla. La propiedad e.repeat es true en los disparos posteriores. Si tu manejador hace algo costoso (como una llamada a API), agrega if (e.repeat) return; en la parte superior para solo manejar la primera pulsación.
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>;Si registras document.addEventListener en useEffect pero olvidas la función de limpieza, el escuchador persiste después del desmontaje y se dispara en un cierre obsoleto. Siempre devuelve una función de limpieza: return () => document.removeEventListener("keydown", handler);.
No. Algunos atajos (Ctrl+T, Ctrl+W, Ctrl+N) son manejados por el navegador antes de que se ejecute tu código y no se pueden interceptar. Solo anula atajos que sabes que son interceptables (Ctrl+S, Ctrl+P, Ctrl+F) y prueba en todos los navegadores.
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();
}
};Sí. Los valores de tecla como "Enter", "Escape", "ArrowDown" siguen la especificación UI Events y distinguen mayúsculas y minúsculas. Comparar contra "enter" o "esc" no coincidirá. Siempre usa los valores exactos de la especificación.
React.KeyboardEvent<T> es el tipo de evento sintético usado en manejadores JSX (p. ej., onKeyDown)KeyboardEvent (sin prefijo React) es el tipo nativo del DOM usado en useEffect con document.addEventListenertype 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);
};Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥