Teclas skeuomórficas de atajos
Teclas de plástico en relieve que se hunden al pulsar la tecla real o al hacer clic, para mostrar atajos.
Abrir buscador
Paleta de comandos
Cerrar
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useEffect, useState, type CSSProperties } from "react";
/** Teclas skeuomórficas de teclado: relieve de plástico que se hunde cuando se pulsa de verdad la tecla correspondiente (o al hacer clic). Para mostrar atajos. React + TypeScript, sin dependencias. */
export type BaseTeclasProps = {
/** Cada grupo es un atajo. Usa nombres de event.key ("Meta", "Shift", "k"). */
shortcuts?: { keys: string[]; label: string }[];
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcbtc-root { box-sizing: border-box; display: grid; gap: 18px; width: fit-content; max-width: 100%; padding: 22px 26px; border-radius: 18px; background: #e7e5e0; color: #3b3a36; font: 600 14px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; box-shadow: inset 0 1px 0 #fff, 0 10px 20px -12px rgba(0, 0, 0, .4); }
.gcbtc-root *, .gcbtc-root *::before, .gcbtc-root *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { .gcbtc-root * { transition-duration: .01ms !important; } }
.gcbtc-row { display: flex; align-items: center; justify-content: space-between; gap: 28px; }
.gcbtc-keys { display: flex; gap: 6px; }
.gcbtc-key { display: inline-grid; place-items: center; min-width: 40px; height: 40px; padding: 0 12px; border: 0; border-radius: 8px; background: linear-gradient(180deg, #fdfdfb, #e8e6e0); color: #3b3a36; font: 700 14px/1 ui-sans-serif, system-ui, sans-serif; cursor: pointer; box-shadow: inset 0 1px 0 #fff, 0 1px 0 #b9b6ac, 0 3px 0 #a9a69b, 0 5px 6px rgba(0, 0, 0, .25); transition: transform .06s, box-shadow .06s; -webkit-tap-highlight-color: transparent; }
.gcbtc-key[data-down="true"] { transform: translateY(3px); box-shadow: inset 0 1px 0 #fff, 0 1px 0 #a9a69b, 0 1px 2px rgba(0, 0, 0, .25); background: linear-gradient(180deg, #f1efe9, #dedbd3); }
.gcbtc-key:focus-visible { outline: 3px solid #2563eb; outline-offset: 2px; }`;
const LABEL: Record<string, string> = { Meta: "⌘", Shift: "⇧", Alt: "⌥", Control: "Ctrl", Enter: "↵", Escape: "Esc", ArrowUp: "↑", ArrowDown: "↓", ArrowLeft: "←", ArrowRight: "→", " ": "Espacio" };
const DEFAULT = [
{ keys: ["Meta", "k"], label: "Abrir buscador" },
{ keys: ["Meta", "Shift", "p"], label: "Paleta de comandos" },
{ keys: ["Escape"], label: "Cerrar" },
];
export function BaseTeclas({ shortcuts = DEFAULT, className = "", style }: BaseTeclasProps) {
const [down, setDown] = useState<Set<string>>(new Set());
useEffect(() => {
const dn = (event: KeyboardEvent) => setDown((prev) => new Set(prev).add(event.key.length === 1 ? event.key.toLowerCase() : event.key));
const up = (event: KeyboardEvent) => setDown((prev) => { const next = new Set(prev); next.delete(event.key.length === 1 ? event.key.toLowerCase() : event.key); return next; });
const clear = () => setDown(new Set());
window.addEventListener("keydown", dn);
window.addEventListener("keyup", up);
window.addEventListener("blur", clear);
return () => { window.removeEventListener("keydown", dn); window.removeEventListener("keyup", up); window.removeEventListener("blur", clear); };
}, []);
const [click, setClick] = useState<string | null>(null);
return (
<div className={`gcbtc-root ${className}`} style={style}>
<style>{CSS}</style>
{shortcuts.map((shortcut) => (
<div key={shortcut.label} className="gcbtc-row">
<span>{shortcut.label}</span>
<kbd style={{ display: "contents" }} className="gcbtc-keys" aria-label={shortcut.keys.map((key) => LABEL[key] ?? key).join(" más ")}>
<span className="gcbtc-keys">
{shortcut.keys.map((key) => (
<button key={key} className="gcbtc-key" type="button" tabIndex={-1} aria-hidden="true" data-down={down.has(key) || click === `${shortcut.label}-${key}`} onPointerDown={() => setClick(`${shortcut.label}-${key}`)} onPointerUp={() => setClick(null)} onPointerLeave={() => setClick(null)}>
{LABEL[key] ?? key.toUpperCase()}
</button>
))}
</span>
</kbd>
</div>
))}
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/base-teclasEjemplo de uso de Teclas skeuomórficas de atajos
Después de copiar base-teclas.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { BaseTeclas } from "./base-teclas";
export default function Ejemplo() {
return <BaseTeclas />;
}Propiedades de BaseTeclas
BaseTeclas acepta 3 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
shortcuts? | { keys: string[]; label: string }[] | Cada grupo es un atajo. Usa nombres de event.key ("Meta", "Shift", "k"). |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 63 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Teclas skeuomórficas de atajos por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Responde al teclado, no solo al ratón.
- Es un componente de cliente de Next.js: incluye la directiva "use client".