Ir al contenido
Evaluar mi proyecto
Componentes · Componentes baseDisponible

Tooltip accesible

Aparece con retardo al apuntar o enfocar, se cierra con Escape y no desaparece al pasar sobre él (WCAG 1.4.13).

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

base-tooltip.tsx58 líneas
Descargar
"use client";

import { cloneElement, useEffect, useId, useRef, useState, type CSSProperties, type ReactElement } from "react";

/** Tooltip accesible: aparece con retardo al apuntar o enfocar, se cierra con Escape y se mantiene visible al pasar sobre él (WCAG 1.4.13). Cuatro posiciones con flecha. React + TypeScript, sin dependencias. */
export type BaseTooltipProps = {
  label: string;
  placement?: "arriba" | "abajo" | "izquierda" | "derecha";
  delay?: number;
  children: ReactElement<{ "aria-describedby"?: string }>;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcbtp-root { box-sizing: border-box; position: relative; display: inline-flex; }
.gcbtp-root *, .gcbtp-root *::before, .gcbtp-root *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { .gcbtp-tip { animation: none !important; } }
.gcbtp-tip { position: absolute; z-index: 20; padding: 7px 10px; border-radius: 8px; background: #18181b; color: #fff; font: 500 12.5px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; white-space: nowrap; box-shadow: 0 8px 20px -6px rgba(0, 0, 0, .4); animation: gcbtp-in .16s ease-out; }
.gcbtp-tip::after { content: ""; position: absolute; width: 8px; height: 8px; background: inherit; transform: rotate(45deg); }
.gcbtp-tip[data-p="arriba"] { bottom: calc(100% + 10px); left: 50%; translate: -50% 0; }
.gcbtp-tip[data-p="arriba"]::after { bottom: -4px; left: calc(50% - 4px); }
.gcbtp-tip[data-p="abajo"] { top: calc(100% + 10px); left: 50%; translate: -50% 0; }
.gcbtp-tip[data-p="abajo"]::after { top: -4px; left: calc(50% - 4px); }
.gcbtp-tip[data-p="izquierda"] { right: calc(100% + 10px); top: 50%; translate: 0 -50%; }
.gcbtp-tip[data-p="izquierda"]::after { right: -4px; top: calc(50% - 4px); }
.gcbtp-tip[data-p="derecha"] { left: calc(100% + 10px); top: 50%; translate: 0 -50%; }
.gcbtp-tip[data-p="derecha"]::after { left: -4px; top: calc(50% - 4px); }
@keyframes gcbtp-in { from { opacity: 0; } }`;

export function BaseTooltip({ label, placement = "arriba", delay = 250, children, className = "", style }: BaseTooltipProps) {
  const id = useId();
  const [open, setOpen] = useState(false);
  const timer = useRef(0);
  useEffect(() => () => window.clearTimeout(timer.current), []);
  useEffect(() => {
    if (!open) return;
    const esc = (event: KeyboardEvent) => event.key === "Escape" && setOpen(false);
    document.addEventListener("keydown", esc);
    return () => document.removeEventListener("keydown", esc);
  }, [open]);

  const show = () => { window.clearTimeout(timer.current); timer.current = window.setTimeout(() => setOpen(true), delay); };
  const hide = () => { window.clearTimeout(timer.current); timer.current = window.setTimeout(() => setOpen(false), 80); };

  return (
    <span className={`gcbtp-root ${className}`} style={style} onPointerEnter={show} onPointerLeave={hide} onFocus={show} onBlur={() => { window.clearTimeout(timer.current); setOpen(false); }}>
      <style>{CSS}</style>
      {cloneElement(children, { "aria-describedby": open ? id : undefined })}
      {open ? (
        <span id={id} role="tooltip" className="gcbtp-tip" data-p={placement} onPointerEnter={() => window.clearTimeout(timer.current)} onPointerLeave={hide}>
          {label}
        </span>
      ) : null}
    </span>
  );
}

Ú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-tooltip

Ejemplo de uso de Tooltip accesible

Después de copiar base-tooltip.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { BaseTooltip } from "./base-tooltip";

export default function Ejemplo() {
  return <BaseTooltip placement="arriba" label="Ejemplo">Contenido</BaseTooltip>;
}

Propiedades de BaseTooltip

BaseTooltip acepta 6 propiedades; 2 son obligatorias.

PropiedadTipoDescripción
labelstringObligatoria.
placement?"arriba" | "abajo" | "izquierda" | "derecha"Opcional.
delay?numberOpcional.
childrenReactElement<{ "aria-describedby"?: string }>Obligatoria.
className?stringOpcional.
style?CSSPropertiesOpcional.

Variantes disponibles

  • placement: arriba, abajo, izquierda, derecha

Accesibilidad y rendimiento

El archivo tiene 58 líneas (3 KB) y define 1 animación con @keyframes. Esto es lo que hace Tooltip accesible por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Declara roles ARIA para lectores de pantalla: tooltip.
  • 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)