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.
"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.
npx shadcn@latest add https://www.gassierconsulting.com/r/base-tooltipEjemplo de uso de Tooltip accesible
Después de copiar base-tooltip.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
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.
| Propiedad | Tipo | Descripción |
|---|---|---|
label | string | Obligatoria. |
placement? | "arriba" | "abajo" | "izquierda" | "derecha" | Opcional. |
delay? | number | Opcional. |
children | ReactElement<{ "aria-describedby"?: string }> | Obligatoria. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
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".