Ir al contenido
Evaluar mi proyecto
Componentes · Componentes baseDisponible

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.

base-teclas.tsx63 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/base-teclas

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
shortcuts?{ keys: string[]; label: string }[]Cada grupo es un atajo. Usa nombres de event.key ("Meta", "Shift", "k").
className?stringOpcional.
style?CSSPropertiesOpcional.

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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)