Eventos de Teclado
Responde a pulsaciones de teclas, liberaciones y atajos de teclado en componentes React.
Referencia de Eventos de Teclado
| 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 |
Receta
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.
Ejemplo Funcional
"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:
- Atajo de teclado global (
Cmd+K) usandouseEffectcondocument.addEventListener - Manejador local
onKeyDownpara navegación con teclas de flecha, Enter para seleccionar y Escape para cerrar - Seguimiento del estado
selectedIndexpara navegación de lista impulsada por teclado - Combinación de eventos de teclado React con estado de entrada controlado
- Limpieza adecuada de escuchadores globales en la función de retorno de
useEffect
Análisis Profundo
Cómo Funciona
- Los eventos de teclado React envuelven el
KeyboardEventnativo en un Synthetic Event. El manejador recibe unReact.KeyboardEvent<T>con todas las propiedades estándar:key,code,altKey,ctrlKey,metaKey,shiftKey,repeat. onKeyDownse dispara cuando se presiona una tecla. Se dispara repetidamente si la tecla se mantiene presionada (la propiedadrepeatestrueen los disparos posteriores).onKeyUpse 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.onKeyPressestá 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 usaonKeyDownen su lugar.- Los eventos de teclado solo se disparan en elementos enfocados. Para capturar atajos globales, adjunta un escuchador a
documentdentro de unuseEffect.
Variaciones
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>;
}Notas TypeScript
// 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);
};Errores Comunes
-
onKeyPressestá 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 usaonKeyDownen su lugar. La única razón para usaronKeyUpes cuando específicamente necesites la acción en la liberación de la tecla. -
Los valores de
e.keydifieren dee.code--e.keydevuelve"a"o"A"dependiendo de Shift;e.codesiempre devuelve"KeyA". Para atajos de teclado, usae.keycon.toLowerCase()para hacer coincidir independientemente de mayúsculas. Para controles de juegos o posición de tecla física, usae.code. -
Los eventos de teclado requieren foco -- Un
<div>no recibe eventos de teclado a menos que tengatabIndex={0}(o-1solo para foco programático). Solución: AgregatabIndexa elementos no interactivos que necesiten manejadores de teclado, o usadocument.addEventListenerglobal para atajos que deben funcionar independientemente del foco. -
Los atajos globales se filtran entre componentes -- Si registras
Cmd+Ken unuseEffectpero el componente se desmonta, el escuchador persiste y se dispara en un cierre obsoleto. Solución: Siempre devuelve una función de limpieza deuseEffectque elimine el escuchador. -
e.repeatse dispara continuamente cuando se mantiene presionada una tecla -- Mantener presionada una tecla disparaonKeyDownrepetidamente. Si tu manejador realiza una acción costosa (como una llamada a API), se disparará docenas de veces. Solución: Verificaif (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.keycon literales de cadena es frágil para teclas especiales -- Los valoreskeycomo"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.
Alternativas
| 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 |
Preguntas Frecuentes
¿Qué evento de teclado debes usar en React: onKeyDown, onKeyUp u onKeyPress?
- Usa
onKeyDowncomo tu evento de teclado principal -- se dispara para todas las teclas incluyendo teclas que no son de carácter - Usa
onKeyUpsolo cuando necesites la acción en la liberación de la tecla - Nunca uses
onKeyPress-- está deprecado y no se dispara para Escape, flechas, o teclas modificadoras
¿Cuál es la diferencia entre e.key y e.code?
e.keydevuelve el carácter producido ("a","A","Enter") y es afectado por el diseño del teclado y los modificadorese.codedevuelve la tecla física ("KeyA","Enter") y es consistente independientemente del diseño- Usa
e.keypara atajos; usae.codepara controles de juegos o posición de tecla física
¿Cómo registras un atajo de teclado global como 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);
}, []);¿Por qué un div no recibe eventos de teclado por defecto?
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.
Error Común: ¿Qué sucede si mantienes presionada una tecla -- onKeyDown se dispara una vez o repetidamente?
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.
¿Cómo implementas navegación con teclas de flecha en una lista?
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>;Error Común: ¿Por qué mi escuchador de atajo global se dispara después de que el componente se desmonta?
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);.
¿Puedes interceptar todos los atajos de teclado del navegador con e.preventDefault()?
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.
¿Cómo manejas teclas modificadoras multiplataforma (Cmd en Mac, Ctrl en 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();
}
};¿Los valores de cadena e.key distinguen mayúsculas y minúsculas?
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.
¿Cuál es la diferencia de tipo TypeScript entre React.KeyboardEvent y el KeyboardEvent nativo?
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 enuseEffectcondocument.addEventListener- Tienen las mismas propiedades pero son diferentes tipos TypeScript
¿Cómo creas un mapa de manejador de teclas escrito en 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);
};Relacionado
- Eventos de Ratón -- Manejo de clics, desplazamientos y movimiento del ratón
- Eventos de Formulario -- Manejo de envíos de formularios y cambios de entrada