Compila un componente de información sobre la herramienta

Una descripción general fundamental de cómo compilar un elemento personalizado de la información sobre herramientas accesible y adaptable al color.

En esta publicación, quiero compartir mis ideas sobre cómo compilar un elemento personalizado <tool-tip> accesible y adaptable al color. Prueba la demo y consulta la fuente.

Se muestra una información sobre herramientas que funciona en una variedad de ejemplos y esquemas de colores.

Si prefieres ver un video, aquí tienes una versión de esta publicación en YouTube:

Descripción general

Una información sobre herramientas es una superposición no modal, no bloqueante ni interactiva que contiene información complementaria para las interfaces de usuario. Está oculto de forma predeterminada y se muestra cuando se coloca el cursor sobre un elemento asociado o se enfoca. No se puede seleccionar una información sobre herramientas ni interactuar con ella directamente. Las indicaciones sobre herramientas no reemplazan a las etiquetas ni a otra información de alto valor. Un usuario debe poder completar su tarea por completo sin una indicación sobre herramientas.

Qué hacer: Etiqueta siempre tus entradas.
No confíes en los cuadros de información en lugar de las etiquetas.

Toggletip en comparación con Tooltip

Al igual que con muchos componentes, hay diferentes descripciones de lo que es una información sobre herramientas, por ejemplo, en MDN, WAI ARIA, Sarah Higley y Componentes inclusivos. Me gusta la separación entre las indicaciones sobre herramientas y las indicaciones de activación. Una información sobre herramientas debe contener información complementaria no interactiva, mientras que una información sobre herramientas con botón de activación puede contener interactividad y información importante. El motivo principal de la división es la accesibilidad, es decir, cómo se espera que los usuarios naveguen a la ventana emergente y tengan acceso a la información y los botones que contiene. Los botones de activación se vuelven complejos rápidamente.

Este es un video de un botón de activación del sitio Designcember, una superposición con interactividad que un usuario puede fijar y explorar, y luego cerrar con la tecla de escape o la tecla de escape:

Este desafío de GUI siguió la ruta de una herramienta de ayuda, con el objetivo de hacer casi todo con CSS. A continuación, te mostramos cómo compilarla.

Marca

Elegí usar un elemento personalizado <tool-tip>. Los autores no necesitan convertir elementos personalizados en componentes web si no quieren. El navegador tratará a <foo-bar> como una <div>. Puedes pensar en un elemento personalizado como una clase de nombre con menos especificidad. No se usa JavaScript.

<tool-tip>A tooltip</tool-tip>

Es como un div con texto dentro. Podemos vincularnos al árbol de accesibilidad de los lectores de pantalla compatibles si agregamos [role="tooltip"].

<tool-tip role="tooltip">A tooltip</tool-tip>

Ahora, para los lectores de pantalla, se reconoce como una información sobre la herramienta. En el siguiente ejemplo, ¿ves cómo el primer elemento de vínculo tiene un elemento de información sobre herramientas reconocido en su árbol y el segundo no? El segundo no tiene el rol. En la sección de estilos, mejoraremos esta vista de árbol.

Una captura de pantalla del árbol de accesibilidad de Chrome DevTools que representa el código HTML. Muestra un vínculo con el texto &quot;top ; Has tooltip: Hey, a tooltip!&quot; que se puede enfocar. Dentro de él, hay texto estático de &quot;top&quot; y un elemento de información sobre herramientas.

A continuación, necesitamos que la información sobre herramientas no se pueda enfocar. Si un lector de pantalla no comprende el rol de la información sobre herramientas, permitirá que los usuarios enfoquen el <tool-tip> para leer el contenido, y la experiencia del usuario no lo necesita. Los lectores de pantalla adjuntarán el contenido al elemento superior y, por lo tanto, no es necesario que se enfoque para que sea accesible. Aquí podemos usar inert para garantizar que ningún usuario encuentre accidentalmente este contenido de la herramienta de ayuda en su flujo de pestañas:

<tool-tip inert role="tooltip">A tooltip</tool-tip>

Otra captura de pantalla del árbol de accesibilidad de Chrome DevTools. Esta vez, falta el elemento de la información sobre herramientas.

Luego, elegí usar atributos como la interfaz para especificar la posición de la información sobre herramientas. De forma predeterminada, todos los <tool-tip> asumirán una posición "superior", pero se puede personalizar la posición en un elemento agregando tip-position:

<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>

Captura de pantalla de un vínculo con un cuadro de información a la derecha que dice &quot;Un cuadro de información&quot;.

Suelo usar atributos en lugar de clases para este tipo de situaciones, de modo que <tool-tip> no pueda tener varias posiciones asignadas al mismo tiempo. Solo puede haber uno o ninguno.

Por último, coloca elementos <tool-tip> dentro del elemento para el que deseas proporcionar una información sobre herramientas. Aquí comparto el texto alt con los usuarios videntes colocando una imagen y un <tool-tip> dentro de un elemento <picture>:

<picture>
  <img alt="The GUI Challenges skull logo" width="100" src="...">
  <tool-tip role="tooltip" tip-position="bottom">
    The <b>GUI Challenges</b> skull logo
  </tool-tip>
</picture>

Captura de pantalla de una imagen con una información sobre herramientas que dice &quot;Logotipo de calavera de The GUI Challenges&quot;.

Aquí coloco un <tool-tip> dentro de un elemento <abbr>:

<p>
  The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>

Una captura de pantalla de un párrafo con el acrónimo HTML subrayado y una información sobre herramientas sobre él que dice “Lenguaje de marcado de hipertexto”.

Accesibilidad

Como elegí crear ventanas de información y no botones de activación, esta sección es mucho más simple. En primer lugar, permíteme describir nuestra experiencia del usuario deseada:

  1. En espacios limitados o interfaces desordenadas, oculta los mensajes complementarios.
  2. Cuando un usuario coloca el cursor sobre un elemento, lo enfoca o usa la función táctil para interactuar con él, se revela el mensaje.
  3. Cuando termines de colocar el cursor sobre el mensaje, enfocarlo o tocarlo, vuelve a ocultarlo.
  4. Por último, asegúrate de que se reduzca cualquier movimiento si un usuario especificó una preferencia por el movimiento reducido.

Nuestro objetivo es ofrecer mensajes complementarios a pedido. Un usuario con visión que usa el mouse o el teclado puede colocar el cursor sobre el mensaje para revelarlo y leerlo con los ojos. Un usuario de lector de pantalla ciego puede enfocarse para revelar el mensaje y recibirlo de forma audible a través de su herramienta.

Captura de pantalla de VoiceOver de macOS leyendo un vínculo con una información sobre herramientas

En la sección anterior, analizamos el árbol de accesibilidad, el rol de la información sobre herramientas y su inercia. Lo que queda es probarlo y verificar que la experiencia del usuario revele correctamente el mensaje de la información sobre herramientas. Después de la prueba, no está claro qué parte del mensaje audible es una información sobre herramientas. También se puede ver durante la depuración en el árbol de accesibilidad, el texto del vínculo de “top” se ejecuta junto, sin vacilación, con “Look, tooltips!”. El lector de pantalla no divide ni identifica el texto como contenido de la información sobre herramientas.

Captura de pantalla del árbol de accesibilidad de las Herramientas para desarrolladores de Chrome en la que el texto del vínculo dice &quot;Hey, a tooltip!&quot;.

Agrega un pseudoelemento solo para lectores de pantalla a <tool-tip> y podemos agregar nuestro propio texto de instrucciones para los usuarios con discapacidad visual.

&::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

A continuación, puedes ver el árbol de accesibilidad actualizado, que ahora tiene un punto y coma después del texto del vínculo y un mensaje para la información sobre herramientas "Tiene información sobre herramientas: ".

Captura de pantalla actualizada del árbol de accesibilidad de Chrome DevTools en la que el texto del vínculo tiene una mejor redacción: &quot;top ; Has tooltip: Hey, a tooltip!&quot;.

Ahora, cuando un usuario de lector de pantalla enfoca el vínculo, dice "top" y hace una pequeña pausa, y luego anuncia "has tooltip: look, tooltips". Esto le brinda al usuario de un lector de pantalla algunas sugerencias de UX útiles. La pausa proporciona una buena separación entre el texto del vínculo y la información sobre herramientas. Además, cuando se anuncia "tiene información sobre herramientas", un usuario de lector de pantalla puede cancelarlo fácilmente si ya lo escuchó antes. Es muy similar a colocar el cursor sobre un elemento y quitarlo rápidamente, como ya viste el mensaje complementario. Esto se sintió como una buena paridad de UX.

Estilos

El elemento <tool-tip> será secundario del elemento para el que representa los mensajes complementarios, por lo que primero comencemos con lo esencial para el efecto de superposición. Sácalo del flujo de documentos con position absolute:

tool-tip {
  position: absolute;
  z-index: 1;
}

Si el elemento superior no es un contexto de apilamiento, la información sobre herramientas se posicionará en el más cercano que sí lo sea, lo que no es lo que queremos. Hay un selector nuevo en el bloque que puede ayudarte, :has():

Navegadores compatibles

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Origen

:has(> tool-tip) {
  position: relative;
}

No te preocupes demasiado por la compatibilidad con navegadores. Primero, recuerda que estas indicaciones son complementarias. Si no funcionan, no hay problema. En segundo lugar, en la sección de JavaScript, implementaremos una secuencia de comandos para polyfill la funcionalidad que necesitamos para los navegadores sin compatibilidad con :has().

A continuación, hagamos que las ventanas de información no sean interactivas para que no roben eventos del puntero de su elemento superior:

tool-tip {
  
  pointer-events: none;
  user-select: none;
}

Luego, oculta la información sobre herramientas con opacidad para que podamos realizar la transición con una transición suave:

tool-tip {
  opacity: 0;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
}

:is() y :has() hacen el trabajo pesado aquí, lo que hace que tool-tip que contiene elementos superiores esté al tanto de la interactividad del usuario para activar o desactivar la visibilidad de una información sobre herramientas secundaria. Los usuarios de mouse pueden colocar el cursor sobre el elemento, los usuarios de teclado y lector de pantalla pueden enfocarlo, y los usuarios táctiles pueden presionarlo.

Ahora que la superposición para ocultar y mostrar funciona para los usuarios videntes, es hora de agregar algunos estilos para aplicar temas, posicionar y agregar la forma de triángulo a la burbuja. Los siguientes estilos comienzan a usar propiedades personalizadas, se basan en lo que tenemos hasta ahora, pero también agregan sombras, tipografía y colores para que se vea como una información sobre herramientas flotante:

Captura de pantalla de la información sobre herramientas en modo oscuro, que flota sobre el vínculo &quot;block-start&quot;.

tool-tip {
  --_p-inline: 1.5ch;
  --_p-block: .75ch;
  --_triangle-size: 7px;
  --_bg: hsl(0 0% 20%);
  --_shadow-alpha: 50%;

  --_bottom-tip: conic-gradient(from -30deg at bottom, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) bottom / 100% 50% no-repeat;
  --_top-tip: conic-gradient(from 150deg at top, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) top / 100% 50% no-repeat;
  --_right-tip: conic-gradient(from -120deg at right, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) right / 50% 100% no-repeat;
  --_left-tip: conic-gradient(from 60deg at left, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) left / 50% 100% no-repeat;

  pointer-events: none;
  user-select: none;

  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;

  position: absolute;
  z-index: 1;
  inline-size: max-content;
  max-inline-size: 25ch;
  text-align: start;
  font-size: 1rem;
  font-weight: normal;
  line-height: normal;
  line-height: initial;
  padding: var(--_p-block) var(--_p-inline);
  margin: 0;
  border-radius: 5px;
  background: var(--_bg);
  color: CanvasText;
  will-change: filter;
  filter:
    drop-shadow(0 3px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
    drop-shadow(0 12px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}

/* create a stacking context for elements with > tool-tips */
:has(> tool-tip) {
  position: relative;
}

/* when those parent elements have focus, hover, etc */
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

/* prepend some prose for screen readers only */
tool-tip::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

/* tooltip shape is a pseudo element so we can cast a shadow */
tool-tip::after {
  content: "";
  background: var(--_bg);
  position: absolute;
  z-index: -1;
  inset: 0;
  mask: var(--_tip);
}

/* top tooltip styles */
tool-tip:is(
  [tip-position="top"],
  [tip-position="block-start"],
  :not([tip-position]),
  [tip-position="bottom"],
  [tip-position="block-end"]
) {
  text-align: center;
}

Ajustes del tema

La información sobre herramientas solo tiene algunos colores para administrar, ya que el color del texto se hereda de la página a través de la palabra clave del sistema CanvasText. Además, como creamos propiedades personalizadas para almacenar los valores, podemos actualizar solo esas propiedades personalizadas y dejar que el tema controle el resto:

@media (prefers-color-scheme: light) {
  tool-tip {
    --_bg: white;
    --_shadow-alpha: 15%;
  }
}

Captura de pantalla en paralelo de las versiones clara y oscura de la información sobre herramientas.

Para el tema claro, adaptamos el fondo al blanco y hacemos que las sombras sean mucho menos intensas ajustando su opacidad.

De derecha a izquierda

Para admitir modos de lectura de derecha a izquierda, una propiedad personalizada almacenará el valor de la dirección del documento en un valor de -1 o 1, respectivamente.

tool-tip {
  --isRTL: -1;
}

tool-tip:dir(rtl) {
  --isRTL: 1;
}

Esto se puede usar para ayudar a posicionar la información sobre herramientas:

tool-tip[tip-position="top"]) {
  --_x: calc(50% * var(--isRTL));
}

Además, te ayuda a encontrar el triángulo:

tool-tip[tip-position="right"]::after {
  --_tip: var(--_left-tip);
}

tool-tip[tip-position="right"]:dir(rtl)::after {
  --_tip: var(--_right-tip);
}

Por último, también se puede usar para transformaciones lógicas en translateX():

--_x: calc(var(--isRTL) * -3px * -1);

Posicionamiento de cuadros de información

Posiciona la información sobre herramientas de forma lógica con las propiedades inset-block o inset-inline para controlar las posiciones físicas y lógicas de la información sobre herramientas. En el siguiente código, se muestra cómo se aplica un diseño a cada una de las cuatro posiciones para las direcciones de izquierda a derecha y de derecha a izquierda.

Alineación superior y de inicio de bloque

Una captura de pantalla que muestra la diferencia de posición entre la posición superior de izquierda a derecha y la posición superior de derecha a izquierda.

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position])) {
  inset-inline-start: 50%;
  inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))::after {
  --_tip: var(--_bottom-tip);
  inset-block-end: calc(var(--_triangle-size) * -1);
  border-block-end: var(--_triangle-size) solid transparent;
}

Alineación a la derecha y en línea

Captura de pantalla que muestra la diferencia de posición entre la posición de la derecha de izquierda a derecha y la posición de final intercalado de derecha a izquierda.

tool-tip:is([tip-position="right"], [tip-position="inline-end"]) {
  inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"])::after {
  --_tip: var(--_left-tip);
  inset-inline-start: calc(var(--_triangle-size) * -1);
  border-inline-start: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"]):dir(rtl)::after {
  --_tip: var(--_right-tip);
}

Alineación inferior y al final del bloque

Captura de pantalla que muestra la diferencia de posición entre la posición inferior de izquierda a derecha y la posición de final de bloque de derecha a izquierda.

tool-tip:is([tip-position="bottom"], [tip-position="block-end"]) {
  inset-inline-start: 50%;
  inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="bottom"], [tip-position="block-end"])::after {
  --_tip: var(--_top-tip);
  inset-block-start: calc(var(--_triangle-size) * -1);
  border-block-start: var(--_triangle-size) solid transparent;
}

Alineación a la izquierda y a la izquierda de la línea

Una captura de pantalla que muestra la diferencia de posición entre la posición izquierda de izquierda a derecha y la posición de inicio intercalada de derecha a izquierda.

tool-tip:is([tip-position="left"], [tip-position="inline-start"]) {
  inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"])::after {
  --_tip: var(--_right-tip);
  inset-inline-end: calc(var(--_triangle-size) * -1);
  border-inline-end: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"]):dir(rtl)::after {
  --_tip: var(--_left-tip);
}

Animación

Hasta ahora, solo activamos o desactivamos la visibilidad de la información sobre herramientas. En esta sección, primero animaremos la opacidad para todos los usuarios, ya que es una transición de movimiento reducido generalmente segura. Luego, animaremos la posición de transformación para que la información sobre herramientas parezca deslizarse desde el elemento superior.

Una transición predeterminada segura y significativa

Aplica diseño al elemento del cuadro de información para que realice la transición de opacidad y transformación de la siguiente manera:

tool-tip {
  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

Cómo agregar movimiento a la transición

Para cada uno de los lados en los que puede aparecer una información sobre herramientas, si el usuario está de acuerdo con el movimiento, posiciona ligeramente la propiedad translateX dándole una pequeña distancia desde la que viajar:

@media (prefers-reduced-motion: no-preference) {
  :has(> tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: 3px;
  }

  :has(> tool-tip:is([tip-position="right"], [tip-position="inline-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: -3px;
  }

  :has(> tool-tip:is([tip-position="bottom"], [tip-position="block-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: -3px;
  }

  :has(> tool-tip:is([tip-position="left"], [tip-position="inline-start"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: 3px;
  }
}

Ten en cuenta que esto establece el estado "out", ya que el estado "in" está en translateX(0).

JavaScript

En mi opinión, el código JavaScript es opcional. Esto se debe a que ninguna de estas ayudas de pantalla debe ser obligatoria para realizar una tarea en tu IU. Por lo tanto, si las sugerencias de herramientas fallan por completo, no debería ser un gran problema. Esto también significa que podemos tratar las indicaciones sobre herramientas como mejoradas de forma progresiva. Con el tiempo, todos los navegadores admitirán :has() y esta secuencia de comandos puede desaparecer por completo.

La secuencia de comandos de polyfill realiza dos acciones y solo lo hace si el navegador no admite :has(). Primero, verifica la compatibilidad con :has():

if (!CSS.supports('selector(:has(*))')) {
  // do work
}

A continuación, busca los elementos superiores de <tool-tip> y asígnales un nombre de clase para trabajar con ellos:

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))
}

A continuación, inserta un conjunto de estilos que usen esa clase de nombre y simula el selector :has() para obtener el mismo comportamiento:

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))

  let styles = document.createElement('style')
  styles.textContent = `
    .has_tool-tip {
      position: relative;
    }
    .has_tool-tip:is(:hover, :focus-visible, :active) > tool-tip {
      opacity: 1;
      transition-delay: 200ms;
    }
  `
  document.head.appendChild(styles)
}

Eso es todo, ahora todos los navegadores mostrarán las indicaciones sobre herramientas si :has() no es compatible.

Conclusión

Ahora que sabes cómo lo hice, ¿cómo lo harías tú? 🙂 No veo la hora de probar la API de popup para facilitar los botones de activación, la capa superior para no tener que lidiar con el índice z y la API de anchor para posicionar mejor los elementos en la ventana. Hasta entonces, haré tooltips.

Diversifiquemos nuestros enfoques y aprendamos todas las formas de compilar en la Web.

Crea una demo, twittea los vínculos y los agregaré a la sección de remixes de la comunidad a continuación.

Remixes de la comunidad

Aún no hay nada que ver.

Recursos