Los tooltips son el equivalente en UI a “solo un pequeño cambio”. Añades un icono de interrogación, pegas algo de texto al pasar el cursor, lo envías y luego la producción te enseña lo que olvidaste: contenedores con desplazamiento, overflow recortado, taps en móviles, infierno de z-index y usuarios con teclado que no pueden hacer hover.
Este es el camino pragmático: construir tooltips sin una biblioteca, pero con rigurosidad de nivel biblioteca. Si tu tooltip sobrevive a un encabezado sticky, un padre transformado, un modal, un zoom al 200% y un lector de pantalla, sobrevivirá a tu próximo rediseño.
Qué es un tooltip (y qué no es)
Un tooltip es información suplementaria ligada a un control o elemento inline. Debe aclarar, no portar significado crítico. Si ocultar el tooltip hace la UI inutilizable, no construiste un tooltip: construiste una etiqueta de formulario rota.
Los tooltips tampoco son popovers, diálogos o notificaciones toast:
- Tooltip: pequeño, efímero, anclado a un objetivo, normalmente sin contenido interactivo.
- Popover: anclado como un tooltip pero puede contener UI interactiva (enlaces, botones, campos). Necesita manejo de teclado más completo.
- Diálogo: modal o no modal, toma el foco, suele tener backdrop y debe poder cerrarse con teclado.
Decide esto temprano, porque controla todo: roles ARIA, comportamiento del foco, cierre y si puedes usar disparadores solo por hover.
Opinión: Si el contenido contiene un enlace, una casilla de verificación o un botón “Copiar”, deja de llamarlo tooltip. Estás construyendo un popover; trátalo como tal.
Hechos e historia que conviene conocer
- Hecho 1: El atributo HTML
titlefue el “tooltip gratis” original, pero es inconsistente entre navegadores, difícil de estilizar y poco fiable para accesibilidad. - Hecho 2: Los tooltips son uno de los patrones de UI heredados de las GUIs de escritorio (piensa en los clásicos globos de ayuda de Windows), luego trasplantados torpemente a la web.
- Hecho 3: Los primeros tooltips web usaban a menudo
onmouseoverydocument.write. Sobrevivimos esa era. Apenas. - Hecho 4: El “problema del z-index” es anterior a muchos frameworks front-end; los contextos de apilamiento causaban problemas mucho antes de que “componente” fuera un puesto laboral.
- Hecho 5: La guía ARIA para tooltips maduró más tarde que la de diálogos y botones, por eso todavía ves patrones desajustados en apps en producción.
- Hecho 6: Las interfaces táctiles obligaron a los tooltips a madurar: hover no existe en la mayoría de los teléfonos, así que tu “simple pista por hover” se convierte en una decisión de diseño.
- Hecho 7: El impulso hacia patrones “portal al body” vino de fallos reales de layout: overflow clipping, transforms y contenedores scroll anidados volvieron frágiles a los tooltips inline.
- Hecho 8: Los navegadores modernos introdujeron primitivas que ayudan (como
ResizeObserver), pero también nuevos campos minados (como “contain” y transforms generalizados).
Requisitos innegociables para tooltips de producción
Antes de escribir código, anota qué significa “funcionar”. Aquí está el listón que uso en producción:
1) Debe ser descubrible sin hover
Los usuarios de teclado deben activar tooltips con el foco. Eso suele implicar que el disparador sea enfocables (<button>, <a> o tabindex="0" como último recurso).
2) No debe robar el foco
Los tooltips son no interactivos. El foco permanece en el disparador. Si el tooltip necesita interacción, conviértelo en un popover con un plan de gestión de foco.
3) No debe causar cambios de layout
No empujar contenido. Los tooltips deben ser overlays posicionados. El CLS no es solo para páginas de marketing; los dashboards internos también son juzgados—por humanos enfadados.
4) Debe sobrevivir scroll, zoom y resize
No “más o menos”. No “la mayoría de las veces”. Los usuarios hacen scroll mientras hacen hover, existen los trackpads y el zoom es común en entornos empresariales.
5) No debe ser recortado
Contenedores con overflow: hidden, ancestros transformados y paneles scrolleables son donde los tooltips ingenuos vienen a morir.
6) No debe bloquear lo que intentas usar
Tooltips que cubren su disparador crean bucles de parpadeo e ira del usuario. Necesitas offset sensato, manejo de pointer-events y cierre con retardo.
7) Debe ser testeable
Si tu tooltip no puede seleccionarse de forma fiable en tests, alguien lo “arreglará” con un martillo. Proporciona atributos estables (como data-tooltip-id) y comportamiento determinista.
Broma #1: Un tooltip que bloquea el botón es como un cartel de seguridad soldado sobre la salida de emergencia. Técnicamente presente, prácticamente cruel.
Arquitectura: inline vs portal, y por qué “position: absolute” no basta
Hay dos arquitecturas sensatas cuando te niegas a usar bibliotecas:
A) Tooltip inline (mismo árbol DOM)
Renderizas el tooltip cerca del disparador y lo posicionas con CSS (position: absolute) dentro de un contenedor con posición relativa.
Pros: simple, menos operaciones DOM, CSS predecible si el contenedor es estable.
Contras: recortado por overflow: hidden/auto; rarezas con contextos de apilamiento; transforms anidados pueden hacer cosas sorprendentes.
B) Tooltip portaleado (renderizado bajo document.body)
Renderizas el tooltip en una capa de overlay de nivel superior (a menudo un <div id="overlays"> dedicada) y lo posicionas usando coordenadas del viewport. Esto es lo que hacen bibliotecas maduras porque los navegadores y el layout no son sentimentales.
Pros: evita recorte por overflow local; manejo de z-index más sencillo; colocación consistente en modales y drawers.
Contras: debes calcular coordenadas; debes rastrear scroll/resize; debes manejar containing blocks y visual viewport en móvil.
Decisión: Si tienes paneles scrolleables o modales (los tienes), predetermina un portal. Los tooltips inline están bien para páginas estáticas, docs y herramientas internas pequeñas donde el riesgo de recorte es bajo.
Contextos de apilamiento: por qué tu z-index “no hace nada”
Los tooltips fallan silenciosamente cuando aparecen detrás de algo. Culpables típicos:
- Un padre con
transform,filter,opacity < 1,containoisolationcreando un nuevo contexto de apilamiento. - Elementos posicionados con sus propias capas z-index.
- Encabezados fixed y elementos sticky con z-index alto.
Los tooltips basados en portal evitan la mayoría de estos al vivir en una capa superior conocida. Pero incluso entonces, tu capa de overlay debe estar por encima del resto de la UI.
Algoritmo de posicionamiento: medir, colocar, voltear, desplazar, limitar
Este es el modelo mental confiable: cada colocación de tooltip es una negociación entre el rectángulo del disparador, el tamaño del tooltip, el viewport y tu lado preferido.
El algoritmo mínimo viable
- Medir el disparador:
targetRect = target.getBoundingClientRect(). - Medir el tooltip: renderízalo fuera de pantalla o oculto, luego lee
tooltipRect. - Colocar en el lado preferido con un offset (p. ej., 8px).
- Voltear si desbordaría (p. ej., arriba no cabe, así que colocas abajo).
- Desplazar a lo largo del eje cruzado para mantenerlo dentro del viewport (p. ej., empujar a la izquierda/derecha).
- Limitar las coordenadas finales para que se mantenga visible, con algo de padding respecto a los bordes.
Eso es todo. Todo lo demás son correcciones de errores disfrazadas.
Consideraciones de viewport: layout viewport vs visual viewport
Los navegadores móviles complican el concepto de “viewport”. Cuando el teclado en pantalla está activo o la página está ampliada, el visual viewport puede diferir del layout viewport. Si posicionas solo con innerWidth/innerHeight, puedes terminar con tooltips flotando en el universo equivocado.
Si quieres ser serio: usa window.visualViewport cuando esté disponible (y cae hacia atrás cuando no lo esté). Ten en cuenta visualViewport.offsetLeft/offsetTop al calcular coordenadas.
Contenedores con scroll: el bug clásico
getBoundingClientRect() devuelve coordenadas relativas al viewport, lo cual es bueno. Pero si renderizas el tooltip dentro de un contenedor scrolleado (arquitectura inline), necesitarás traducir desde coordenadas del viewport al espacio de coordenadas del contenedor. Aquí es donde comienzan muchas historias de “funciona en mi máquina”.
Una función de posicionamiento limpia y sin dependencias (pseudo-código)
No una biblioteca completa. Solo la estructura mínima para que no lamentos tus decisiones:
- Entradas: rect del target, tamaño del tooltip, preferencia de colocación, tamaño del viewport, padding, offset.
- Salida: x, y, colocación usada, offset de la flecha.
Impléntala como una función pura. Luego pruébala con unit tests usando rectángulos. No puedes probar geometry por integración solamente.
Tratar con transforms y posicionamiento fixed
Si portas al body, position: fixed en el tooltip suele ser lo más sencillo: tu x/y son coordenadas del viewport. Si usas position: absolute, deberás añadir offsets de scroll (window.scrollX, window.scrollY) y manejar el scroll del documento por separado. Fixed gana por cordura.
La posición de la flecha depende de la posición final desplazada
La flecha no es mera decoración; es una señal direccional. Si desplazas el tooltip a la izquierda para evitar recorte, la flecha también debe moverse. Si no, señalará a un espacio vacío, lo cual es un daño UX sutil pero real.
Flechas: triángulos, cuadrados rotados, SVG y “no engañar al usuario”
Las flechas parecen fáciles hasta que las envías. Aquí las implementaciones comunes:
Opción 1: triángulo con bordes CSS
El clásico: un elemento de tamaño cero con bordes, donde un borde tiene color y los otros son transparentes.
Pros: funciona en todas partes, poco DOM.
Contras: difícil añadir sombras limpiamente; el anti-aliasing puede verse dentado; escalar y tematizar es molesto.
Opción 2: cuadrado rotado (“diamante”) usando transform: rotate(45deg)
Este es mi predeterminado: un cuadrado pequeño, rotado, posicionado de modo que la mitad se solape con la caja del tooltip.
Pros: las sombras funcionan; posibles esquinas redondeadas; tematización más sencilla.
Contras: necesita solapamiento cuidadoso para evitar juntas; los transforms pueden crear contextos de apilamiento (sí, otra vez).
Opción 3: SVG inline
Pros: nítido, fácil de sombrear/filtrar, alineación precisa, puede coincidir con sistemas de diseño.
Contras: un poco más de marcado; si eres descuidado, acabarás con problemas de pointer-events.
Cómo mantener la flecha honesta
Calcula un offset de flecha a lo largo del eje cruzado del tooltip:
- Si el tooltip se coloca arriba/abajo del objetivo: la flecha se mueve izquierda/derecha dentro del tooltip.
- Si se coloca a la izquierda/derecha: la flecha se mueve arriba/abajo.
Luego limita ese offset también, para que la flecha no termine en la esquina redondeada del tooltip. Esquinas redondeadas + flecha en el radio = glitch visual.
Patrones compatibles con ARIA: teclado, foco y lectores de pantalla
La accesibilidad no es una lección moral; es una estrategia para prevenir incidentes. En el momento en que alguien no entiende un icono de error porque el tooltip no anuncia, llegarán tickets, escalaciones y un “arreglo rápido” que romperá otra cosa.
Usa el rol y la relación correctos
Para un tooltip verdadero (contenido no interactivo), usa:
role="tooltip"en el elemento tooltip.aria-describedby="tooltip-id"en el elemento disparador.
Esto le dice a la tecnología asistiva: “esto describe aquello”. Es simple. No te compliques.
Cuándo usar aria-label vs aria-describedby
- Usa
aria-labelcuando el disparador no tiene etiqueta visible (p. ej., botón solo icono). Esa etiqueta debe ser corta y estable. - Usa
aria-describedbypara texto suplementario, explicaciones de error, pistas de teclado o aclaraciones que no quieres como nombre primario.
No uses tooltips para parchear etiquetas faltantes. Los lectores de pantalla no deberían tener que “descubrir” el nombre de un control.
Mostrar en foco, ocultar en blur (con retardo)
Reglas básicas:
- En
focus: abrir tooltip. - En
blur: cerrar tooltip. - En
Escape: cerrar tooltip (incluso si “solo es un tooltip”).
Añade un pequeño retardo de cierre (como 100–200ms) para evitar parpadeo cuando el puntero transita entre disparador y tooltip. Y sí, incluso para tooltips no interactivos, los usuarios hacen esto. Los humanos son ingenieros del caos.
No encierres a los lectores de pantalla en un mundo solo por hover
Hover no es una interacción universal. Muchos usuarios no usan ratón. Muchos dispositivos no tienen hover. Proporciona disparadores por foco y asegúrate de que el contenido del tooltip se anuncie mediante la relación described-by.
Mantén el contenido del tooltip corto y aburrido
Los tooltips deben ser una o dos frases. Si necesitas un párrafo, estás escribiendo documentación. Si necesitas una tabla, estás construyendo un popover. Si necesitas un formulario, estás construyendo un diálogo. Las palabras tienen significado; tu UI también debería tenerlo.
Idea parafraseada (atrib.): “La esperanza no es una estrategia”, frase común en círculos de operaciones dirigida por líderes orientados a la fiabilidad como Gene Kranz. Trata la accesibilidad igual: plánifícala.
Modelo de eventos: hover, foco, tacto y cierre
Los tooltips fallan en las costuras entre métodos de entrada. Tu trabajo es hacer esas costuras aburridas.
Eventos pointer: úsalos, pero no confíes ciegamente
pointerenter/pointerleave pueden reemplazar la lógica separada de mouse y touch, pero aún necesitas decidir qué hacer con pointers groseros (tacto). Un tooltip que se abre al tocar puede estar bien si:
- El toque abre el tooltip.
- Un segundo toque (o toque fuera) lo cierra.
- El tooltip no bloquea la siguiente acción prevista.
Reglas de cierre que no fastidian a la gente
- Mouse: cerrar en pointerleave con un pequeño retardo; mantener abierto si el puntero entra al tooltip (opcional).
- Teclado: cerrar en blur o Escape.
- Tacto: cerrar en toque fuera; considera cerrar al hacer scroll.
Jitter y parpadeo: el problema de “moví el ratón un píxel”
El bucle de parpadeo suele ocurrir porque el tooltip se solapa con el disparador. Cuando aparece, el disparador ya no está hovered, así que desaparece, luego el disparador vuelve a estar hovered, etc.
Soluciones:
- Separa el tooltip con un offset para que no se solape.
- Aplica
pointer-events: noneen el tooltip si no necesita persistencia por hover. - O implementa un tooltip con hoverable con un “límite interactivo” y temporizadores.
Rendimiento y fiabilidad: jitter, reflow y manejo de scroll
Los tooltips son pequeños, pero pueden desencadenar trabajo costoso en el peor momento: scroll, resize y movimiento del puntero. En otras palabras: continuamente.
No reposiciones en cada mousemove
Posiciona al abrir, luego reposiciona en:
- scroll (throttled)
- resize (throttled)
- redimensionamiento del target (ResizeObserver)
- cambio de contenido del tooltip (ResizeObserver, o re-medición tras render)
Si reposicionas en mousemove, crearás micro-jank en máquinas de gama baja y en páginas con layout pesado.
Throttling con requestAnimationFrame
Para scroll/resize, recolecta eventos y haz una sola reposición por frame de animación. Eso te mantiene alineado con el render loop del navegador y evita cálculos de layout redundantes.
Minimiza reflows forzados
getBoundingClientRect() puede forzar layout si lees después de escribir estilos que afectan layout. Agrupa lecturas primero y luego escrituras. Una de las formas más sencillas: calcula toda la geometría y luego aplica style.transform = translate3d(x,y,0) de una sola vez.
Prefiere transform sobre top/left
Usa transform: translate3d() para posicionar, especialmente cuando el tooltip anima. Suele ser más suave y evita layout. “Suele” porque la web es una democracia de casos límite.
Broma #2: Si mides el layout en un bucle cerrado, Chrome te mostrará con gusto cómo se siente “eventual consistency”: eventualmente renderizará tu tooltip en otro lugar.
Guía de diagnóstico rápido
Cuando un tooltip está “roto”, los ingenieros suelen discutir CSS una hora. No lo hagas. Ejecuta la guía. Encuentra el cuello de botella rápido.
Primero: identifica la clase de fallo
- Invisible: no se renderiza, opacity 0, display none, o detrás de otra capa.
- Mal posicionado: coordenadas erróneas, espacio de coordenadas equivocado, offsets de scroll incorrectos.
- Recortado: overflow hidden/auto, o contenedor recorta por contexto de apilamiento/containment.
- Parpadeo: bucle de eventos (hover) o thrash de reposicionamiento (scroll/resize).
- No anunciado: el lector de pantalla no lo lee (cableado ARIA o comportamiento de foco).
Segundo: comprueba las suposiciones del espacio de coordenadas
- ¿Estás usando un portal? Si sí, prefiere
position: fixedy coordenadas de viewport. - Si no está portaleado: ¿cuál es el offset parent? ¿Hay un ancestro transformado?
- ¿Estás mezclando
pageX/pageYcon rects basados en viewport?
Tercero: confirma apilamiento y recorte
- ¿Está el tooltip dentro de un elemento con
overflow: hiddenooverflow: auto? - ¿Algún ancestro establece un contexto de apilamiento (transform/opacity/filter/contain)?
- ¿La z-index de tu capa overlay está realmente por encima del header/modal?
Cuarto: confirma el cableado de accesibilidad
- ¿El disparador tiene
aria-describedbyapuntando a un ID existente? - ¿El tooltip tiene
role="tooltip"? - ¿El tooltip se abre en foco y se cierra en blur/Escape?
Errores comunes: síntomas → causa raíz → solución
1) El tooltip aparece en (0,0) o en la esquina superior izquierda de la página
- Síntoma: tooltip fijado en la esquina, independientemente del disparador.
- Causa raíz: medición realizada antes de que el elemento esté en el DOM, o
getBoundingClientRect()llamado sobre un target nulo/oculto; a veces una referencia obsoleta. - Solución: renderiza el tooltip oculto (
visibility: hidden) primero, luego mide en el siguiente frame; protege contra refs faltantes; registra rects.
2) El tooltip es correcto hasta que desplazas un contenedor
- Síntoma: el tooltip se aleja del objetivo durante el scroll.
- Causa raíz: escuchas solo el scroll de window, no del ancestro scrolleable; o usas posicionamiento absolute con offsets de scroll incorrectos.
- Solución: portal + posicionamiento fixed; o detectar y escuchar el contenedor de scroll más cercano; reposicionar en eventos de scroll vía rAF.
3) El tooltip queda cortado dentro de una tarjeta/modal
- Síntoma: el tooltip se recorta visiblemente en el borde del contenedor.
- Causa raíz: el tooltip vive dentro de un elemento con
overflow: hidden/auto. - Solución: portal al layer de overlay de nivel superior; o relajar el overflow (rara vez aceptable); o usar
position: fixedcon apilamiento correcto.
4) El tooltip aparece detrás del encabezado
- Síntoma: el tooltip existe pero no es visible sobre elementos sticky.
- Causa raíz: la capa overlay está debajo de una capa con z-index mayor; o el tooltip mismo está en un contexto de apilamiento inferior por culpa de un ancestro.
- Solución: raíz de overlay central con z-index alto conocido; evita transforms en ancestros del overlay; audita contextos de apilamiento.
5) Parpadeo al hacer hover
- Síntoma: el tooltip se muestra/oculta rápidamente al mover el puntero cerca del disparador.
- Causa raíz: el tooltip se solapa con el objetivo y roba el hover; cierre inmediato en leave sin retardo.
- Solución: añade offset; aplica
pointer-events: nonepara tooltip no interactivo; añade retardo de cierre; considera un “puente de hover” (padding invisible) si es necesario.
6) El lector de pantalla no anuncia el tooltip
- Síntoma: tooltip visible visualmente, pero no leído cuando se enfoca el disparador.
- Causa raíz: falta
aria-describedby; ID del tooltip no coincide; contenido del tooltip no está presente en el DOM hasta después del evento de foco y el SR no reanuncia. - Solución: mantén el elemento tooltip en el DOM (oculto) para que described-by apunte a contenido real; verifica el cableado de IDs; abre en foco consistentemente.
7) El tooltip causa jank en el scroll
- Síntoma: el desplazamiento se vuelve entrecortado cuando el tooltip está abierto.
- Causa raíz: reposicionamiento en cada evento de scroll con layout forzado; sombras pesadas o filtros; demasiados observers.
- Solución: throttle vía rAF; reduce CSS costoso; evita efectos filter; reposiciona solo cuando está abierto.
Tareas prácticas (con comandos): depura como un SRE
Puedes depurar UI en el navegador, claro. Pero cuando el bug es “solo en prod” y “solo para algunos usuarios”, necesitas observabilidad a nivel sistema: qué build, qué cabeceras, qué CSP, qué assets, qué errores. Estas tareas son la memoria muscular aburrida que previene conjeturas heroicas.
Task 1: Confirmar qué HTML se desplegó (y si existe el marcado del tooltip)
cr0x@server:~$ curl -sS -D- https://app.example.internal/settings | sed -n '1,40p'
HTTP/2 200
date: Mon, 29 Dec 2025 10:11:12 GMT
content-type: text/html; charset=utf-8
content-security-policy: default-src 'self'; script-src 'self'
etag: W/"a1b2c3"
...
Qué significa la salida: Estás verificando el estado, las cabeceras (especialmente CSP) y si estás recibiendo HTML. CSP importa porque los patrones de tooltip a menudo dependen de estilos o scripts inline.
Decisión: Si CSP bloquea estilos/scripts inline y tu tooltip los usa, o refactorizas a assets externos o actualizas CSP de forma segura.
Task 2: Verificar que el bundle JS del tooltip esté presente y cacheable
cr0x@server:~$ curl -sS -I https://app.example.internal/assets/tooltip.js
HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=31536000, immutable
etag: "9f8e7d6c"
Qué significa la salida: El bundle existe y es apto para cacheo. Si ves 404 o tiempos de cache cortos, encontraste un problema de despliegue o invalidación de caché.
Decisión: Si el cache está mal, arregla fingerprinting de assets o la config del CDN antes de tocar el código del tooltip.
Task 3: Comprobar si los errores aumentan cuando los tooltips deberían renderizar
cr0x@server:~$ kubectl -n web logs deploy/frontend --since=15m | grep -E "TypeError|ReferenceError|CSP|tooltip" | tail -n 20
TypeError: Cannot read properties of null (reading 'getBoundingClientRect')
CSP: Refused to apply inline style because it violates the following Content Security Policy directive...
Qué significa la salida: Una referencia nula sugiere que el elemento disparador no existe cuando se mide (race), o el selector está mal en prod. El error CSP te dice que los estilos están bloqueados.
Decisión: Si ves violaciones de CSP, para. Arregla la política o la implementación. Si ves rect nulos, añade protecciones y difiere la medición.
Task 4: Confirmar la versión desplegada (porque “solo en prod” suele ser “solo ese canario”)
cr0x@server:~$ kubectl -n web describe deploy/frontend | sed -n '1,120p'
Name: frontend
Namespace: web
Labels: app=frontend
Annotations: deployment.kubernetes.io/revision: 42
Containers:
frontend:
Image: registry.internal/frontend:sha-8c1d2a7
Ports: 8080/TCP
Qué significa la salida: Verificas la imagen en ejecución y la revisión. Los bugs de tooltip frecuentemente se correlacionan con una release.
Decisión: Si el bug empezó con una revisión, haz bisect mediante rollback o compara cambios alrededor del código y CSS del tooltip.
Task 5: Comprobar si un proxy inverso está eliminando cabeceras necesarias
cr0x@server:~$ curl -sS -I https://app.example.internal/ | grep -i -E "content-security-policy|x-frame-options|referrer-policy"
content-security-policy: default-src 'self'; script-src 'self'
referrer-policy: strict-origin-when-cross-origin
Qué significa la salida: Las cabeceras de seguridad pueden afectar estrategias de tooltip (p. ej., si dependías de estilos inline). Las políticas de frame importan si tu app está embebida.
Decisión: Si las cabeceras difieren entre entornos, alinearlas; de lo contrario seguirás enviando tooltips que “funcionan en staging”.
Task 6: Encontrar qué ruta/versión del lado cliente están alcanzando los usuarios vía logs de acceso
cr0x@server:~$ sudo tail -n 200 /var/log/nginx/access.log | grep -E "GET /assets/|GET /settings" | tail -n 20
10.1.2.3 - - "GET /settings HTTP/2.0" 200 42103 "-" "Mozilla/5.0 ..."
10.1.2.3 - - "GET /assets/tooltip.js HTTP/2.0" 200 18342 "-" "Mozilla/5.0 ..."
Qué significa la salida: Puedes ver si el asset del tooltip se solicita y si responde 200 o 304. Falta de fetch suele significar que el HTML no lo referenció o un cliente lo bloqueó.
Decisión: Si no ves requests del asset, confirma bundling e inclusión en el HTML. Si ves 403/404, arregla el routing/CDN.
Task 7: Verificar que gzip/brotli no estén corrompiendo el payload JS (raro, pero real)
cr0x@server:~$ curl -sS -H 'Accept-Encoding: gzip' --compressed https://app.example.internal/assets/tooltip.js | head -n 5
(function(){'use strict';
var Tooltip=function(){...}
Qué significa la salida: Confirmas que la respuesta comprimida se descomprime a texto JS válido.
Decisión: Si obtienes basura binaria o contenido truncado, inspecciona la configuración de compresión del proxy.
Task 8: Medir errores del cliente en tiempo real vía logs del servidor (reportes CSP)
cr0x@server:~$ kubectl -n web logs deploy/csp-report-collector --since=30m | tail -n 20
{"blocked-uri":"inline","violated-directive":"style-src-elem","document-uri":"https://app.example.internal/settings"}
Qué significa la salida: Los usuarios están disparando violaciones CSP que podrían impedir el estilo o la visibilidad del tooltip.
Decisión: Quita estilos/scripts inline de la implementación del tooltip o actualiza CSP con nonces/hashes (con cuidado).
Task 9: Confirmar que la raíz del overlay existe en la plantilla DOM (modo fallo SSR/templating)
cr0x@server:~$ curl -sS https://app.example.internal/ | grep -n 'id="overlays"' | head -n 5
118:
Qué significa la salida: Si tu portal espera una raíz de overlay y falta, los tooltips fallarán silenciosamente o se adjuntarán al body incorrectamente.
Decisión: Si falta, arregla la plantilla base y añade una solución de fallback en tiempo de ejecución que cree el nodo.
Task 10: Detectar si un cambio de CSS introdujo overflow: hidden en un ancestro clave
cr0x@server:~$ git show HEAD~1:ui/styles/components/card.css | sed -n '1,120p'
.card {
position: relative;
overflow: hidden;
border-radius: 12px;
}
Qué significa la salida: Un cambio reciente añadió clipping por overflow. Esto es uno de los tres asesinos principales de tooltips.
Decisión: O portas el tooltip fuera de la card, o quitas overflow hidden si no es necesario (normalmente sí lo es, para esquinas redondeadas).
Task 11: Reproducir el recorte localmente con un entorno determinista
cr0x@server:~$ docker run --rm -p 8080:8080 registry.internal/frontend:sha-8c1d2a7
Listening on http://0.0.0.0:8080
Qué significa la salida: Ejecutas la misma imagen que en prod. No hay excusas de “funciona en mi laptop”.
Decisión: Si se reproduce, puedes depurar en el navegador con confianza. Si no, la diferencia es config, cabeceras o capas upstream.
Task 12: Comprobar si los agentes de usuario difieren (tacto vs hover)
cr0x@server:~$ sudo awk -F\" '{print $6}' /var/log/nginx/access.log | tail -n 200 | sort | uniq -c | sort -nr | head
83 Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 ...
52 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...
Qué significa la salida: Un pico de tráfico desde iPhone puede exponer fallos de tooltip causados por suposiciones de hover.
Decisión: Si los dispositivos táctiles dominan los reportes de error, asegúrate de tener comportamiento amigable para taps y no depender solo de hover.
Task 13: Identificar si el código del tooltip está causando picos de CPU (correlación server-side)
cr0x@server:~$ kubectl -n web top pods | head
NAME CPU(cores) MEMORY(bytes)
frontend-6c7b9c9f8d-2kq9m 120m 310Mi
frontend-6c7b9c9f8d-l8z2p 115m 305Mi
Qué significa la salida: Esto no mide CPU cliente, pero puede revelar picos correlacionados (p. ej., SSR rendering o generación excesiva de HTML).
Decisión: Si la CPU del servidor subió con el despliegue del tooltip, investiga bucles SSR, logging o cambios de plantillas ligados al render del tooltip.
Task 14: Confirmar que los sourcemaps no faltan en producción (tarea de depurabilidad)
cr0x@server:~$ ls -lh /srv/app/assets | grep -E 'tooltip\.js(\.map)?'
-rw-r--r-- 1 www-data www-data 18K Dec 29 09:58 tooltip.js
-rw-r--r-- 1 www-data www-data 61K Dec 29 09:58 tooltip.js.map
Qué significa la salida: Tienes sourcemaps disponibles (aunque restringidos). Depurar geometría de tooltip sin sourcemaps es autolesionarse.
Decisión: Si faltan mapas, decide si subirlos con seguridad (restringidos) o mejora el logging del lado servidor sobre decisiones de posicionamiento.
Tres micro-historias corporativas desde las minas de tooltips
Micro-historia 1: El incidente causado por una suposición equivocada
Un equipo construyó un componente de tooltip limpio para un dashboard de facturación. Funcionó en dev, en staging y en una demo donde todos cortésmente usaban ratón como si fuera 2008. La suposición: “los tooltips son UI por hover”. Lo enviaron.
En un día llegaron tickets de soporte: los usuarios no entendían por qué fallaban sus facturas. El icono de error tenía la única explicación, y la explicación vivía exclusivamente en el tooltip. Muchos usuarios navegan con teclado por RSI, y algunos usan un zoom alto donde la precisión del hover se vuelve más difícil.
El primer “arreglo” fue abrir el tooltip con click. Eso lo hizo algo accesible, pero también bloqueó el icono de error y enlaces adyacentes, creando una nueva clase de bug: clicks destinados al icono simplemente alternaban el tooltip, mientras que los lectores de pantalla seguían con anuncios inconsistentes porque el DOM del tooltip se creaba solo después de la interacción.
La reparación final fue aburrida y efectiva: etiquetas apropiadas para errores inline, tooltips degradados a aclaraciones cortas y aria-describedby conectado a un elemento tooltip siempre presente que se abre en foco. El incidente no fue de geometría. Fue de semántica. El titular del postmortem pudo haber sido: “Usamos un tooltip como etiqueta”.
Micro-historia 2: La optimización que salió mal
Otra organización decidió que los tooltips eran “caros” porque abrir uno provocaba una medición de layout. Alguien perfiló en una máquina lenta y concluyó: “Deberíamos precomputar posiciones para todos los tooltips en la carga de la página”. Sonaba eficiente. Era también erróneo de manera muy web.
Construyeron una prepasada: iterar todos los disparadores, medir rects y almacenar coordenadas. Luego, al hover, renderizar el tooltip en x/y almacenado. La página tenía cientos de filas, cada una con varios iconos. La carga de la página se volvió más pesada, pero aún aceptable en laboratorio.
En producción, los usuarios filtraron tablas, redimensionaron columnas y abrieron paneles laterales. Las coordenadas almacenadas quedaron obsoletas. Los tooltips derivaron, voltearon incorrectamente y a veces aparecían fuera de pantalla. Peor: la prepass misma causó reflows forzados, haciendo que la página se sintiera lenta antes de cualquier interacción.
La solución fue hacer lo opuesto: calcular posiciones solo cuando un tooltip está abierto y recalcular solo en cambios relevantes (scroll/resize/observer) con throttle por frames de animación. “Optimizar” se reemplazó por “hacer menos, más tarde y correctamente”.
Micro-historia 3: La práctica aburrida pero correcta que salvó el día
Una empresa preocupada por seguridad introdujo una política de Content Security Policy más estricta. Fue una buena decisión. También rompió un número sorprendente de elementos UI que habían dependido silenciosamente de estilos inline.
La implementación del tooltip fue uno de ellos. Usaba un truco rápido: poner style="top: ...px; left: ...px" en tiempo de ejecución. CSP tenía style-src 'self' sin permisos para estilos inline, así que los tooltips se renderizaban en su posición CSS por defecto—justo en la esquina superior izquierda de la capa overlay.
La mayoría de equipos pidieron excepciones CSP. Un equipo no lo hizo. Tenían un ítem en la checklist previa al despliegue: “Ejecutar CSP report-only en staging y revisar violaciones”. Habían construido un pequeño endpoint colector y realmente lo revisaban, porque estaban cansados de los viernes sorpresa.
Migraron el posicionamiento del tooltip a variables CSS actualizadas vía una regla en la hoja de estilos en lugar de atributos inline, y se aseguraron de que el CSS necesario estuviera en un asset versionado. Cuando la política más estricta entró en vigor, sus tooltips siguieron funcionando. El héroe fue una checklist y una cola de reportes. ¿Glamoroso? No. ¿Efectivo? Extremadamente.
Listas de verificación / plan paso a paso
Paso a paso: construye un tooltip que sobreviva a la realidad
- Decide la semántica: tooltip vs popover. Si es interactivo, es un popover—no lo finjas.
- Elige arquitectura: portal a una raíz overlay superior por defecto.
- Crea cableado estable: genera un ID de tooltip y pon
aria-describedbyen el disparador. - Mantén el DOM del tooltip presente: oculta con
visibilityyopacity; no lo crees/destruyas en cada hover si necesitas comportamiento SR consistente. - Posiciona con fixed + transform: calcula x/y del viewport; aplícalo con
translate3d. - Implementa flip + shift: no dejes que los tooltips se salgan de la pantalla. Limítalos con padding.
- La flecha sigue la posición final: calcula offset de flecha tras el shift; limita su posición lejos de esquinas redondeadas.
- Eventos: abre en hover y foco; cierra en leave/blur/Escape; cierra en pointerdown fuera para táctil.
- Throttling de actualizaciones: rAF para scroll/resize; observers solo cuando esté abierto.
- Pruebas: añade selectores deterministas y prueba la función de geometría con rectángulos de prueba.
- Endurecimiento: maneja raíz overlay faltante; protege refs nulos; trata CSP como una entrada de diseño.
Checklist: puertas de producción
- Funciona con navegación solo por teclado (foco abre, Escape cierra).
- Anunciado por lectores de pantalla (rol + described-by + DOM estable).
- Sobrevive contenedores de scroll anidados (reposicionar en scroll).
- No es recortado por overflow (portal).
- No hay bucles de parpadeo (offset + estrategia pointer-events + retardo).
- No causa jank de scroll notable (medición con throttle, no reposicionar en mousemove).
- Tiene estrategia definida de z-index (raíz overlay encima de headers/modales).
- Compatible con CSP (sin estilos inline si la política lo prohíbe).
FAQ
- 1) ¿Puedo usar simplemente el atributo HTML
title? - Puedes, pero perderás control de estilo, tendrás tiempos inconsistentes entre navegadores y a menudo accesibilidad débil. Úsalo solo como fallback de última instancia.
- 2) ¿El contenido del tooltip debe estar en el DOM cuando está oculto?
- Usualmente sí. Hace que
aria-describedbysea estable y mejora la consistencia con lectores de pantalla. Oculta convisibility: hiddenyopacity: 0, no condisplay: none, si necesitas que sea referenciable. - 3) ¿Qué rol ARIA debo usar?
role="tooltip"en el elemento tooltip. Luego adjúntalo al disparador usandoaria-describedby. Mantén el contenido no interactivo.- 4) ¿Los tooltips deberían abrirse con click?
- En dispositivos táctiles, click/tap puede ser un disparador razonable. En escritorio, prefiere hover + foco. Si abres con click, debes definir el cierre y asegurarte de que no bloquee la siguiente acción.
- 5) ¿Por qué no funciona mi z-index?
- Porque z-index solo compite dentro del mismo contexto de apilamiento. Transforms, opacity y ciertas propiedades CSS crean nuevos contextos. Portálalo a una raíz overlay conocida y gestiona z-index centralmente.
- 6) ¿Cómo evito el recorte en contenedores con overflow?
- Porta el tooltip fuera del contenedor (típicamente a
document.bodyo una raíz overlay) y posiciónalo con coordenadas del viewport usandoposition: fixed. - 7) ¿Con qué frecuencia debo recalcular la posición del tooltip?
- Al abrir, y luego cuando algo relevante cambie: scroll, resize, cambios en el tamaño del target o en el contenido del tooltip. Hazlo a un ritmo de actualización máximo por frame de animación.
- 8) ¿Puedo animar tooltips de forma segura?
- Sí: anima opacity y transform (pequeño translate). Evita animar top/left. Mantén las animaciones cortas y respeta las preferencias de reduced-motion si tu producto lo requiere.
- 9) ¿Debe ser hoverable un tooltip?
- Si no es interactivo, no necesita ser hoverable. Usa
pointer-events: noney evita bucles de parpadeo. Si necesitas persistencia para leer texto más largo, añade retardo de cierre y permite que el puntero entre al tooltip. - 10) ¿Cuál es la estrategia de colocación robusta más simple?
- Lado preferido + voltear si no cabe + desplazar para mantener en viewport + limitar con padding. No omitas shift/clamp a menos que tu UI nunca haga scroll ni resize—lo cual es un cuento de hadas.
Conclusión: siguientes pasos que no te traicionarán
Si quieres tooltips sin bibliotecas, el truco no es escribir código inteligente. Es escribir código que asuma que tu layout será hostil, tus usuarios serán diversos y tu CSS será “mejorado” por alguien que no conoce tu componente.
Siguientes pasos que pagan rápido:
- Elige portal + posicionamiento fixed como arquitectura por defecto.
- Implementa la geometría como función pura y pruébala con rectángulos.
- Conecta
aria-describedby+role="tooltip"y abre en foco, no solo en hover. - Añade reposicionamiento throttleado por rAF para scroll/resize y deja de medir en mousemove.
- Ejecuta la guía de diagnóstico en una página con modales, encabezados sticky y paneles de scroll anidados—porque ahí vive la verdad.
Mentalidad operacional: Trata los tooltips como cualquier otra característica de producción. Tienen dependencias (CSP, capas z-index, contenedores de scroll) y modos de fallo. Hazlos observables y aburridos.