Compila un componente de información sobre la herramienta

Descripción general básica de cómo compilar un elemento personalizado de Tooltip 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 demostración y consulta el código fuente.

Se muestra una sugerencia que funciona en una variedad de ejemplos y esquemas de color.

Si prefieres un video, aquí tienes una versión de este artículo en YouTube:

Descripción general

Una sugerencia es una capa superpuesta no modal, no bloqueante y no 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 en él. No se puede seleccionar ni interactuar directamente con una sugerencia. Las sugerencias no reemplazan las etiquetas ni otra información valiosa. Un usuario debe poder completar su tarea sin una sugerencia.

Qué hacer: Siempre etiqueta tus entradas.
No: Confiar en las sugerencias en lugar de las etiquetas

Comparación entre Toggletip y Tooltip

Al igual que muchos componentes, existen varias descripciones de lo que es una sugerencia, por ejemplo, en MDN, WAI ARIA, Sarah Higley y Componentes inclusivos. Me gusta la separación entre las sugerencias y las sugerencias activables. Una sugerencia debe contener información complementaria no interactiva, mientras que una sugerencia activable puede contener interactividad e información importante. La razón principal de esta división es la accesibilidad, ya que se espera que los usuarios naveguen hasta la ventana emergente y tengan acceso a la información y los botones que contiene. Las sugerencias interactivas se vuelven complejas rápidamente.

A continuación, se muestra un video de una sugerencia activable del sitio de Designcember, una superposición con interactividad que el usuario puede fijar y explorar, y, luego, cerrar con un descarte ligero o la tecla de escape:

Este desafío de GUI se centró en una sugerencia, con el objetivo de hacer casi todo con CSS. A continuación, te mostramos cómo compilarlo.

Marca

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

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

Esto es como un div con algo de texto dentro. Podemos vincularnos al árbol de accesibilidad de los lectores de pantalla compatibles agregando [role="tooltip"].

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

Ahora, los lectores de pantalla lo reconocen como información sobre herramientas. En el siguiente ejemplo, ¿puedes ver cómo el primer elemento de vínculo tiene un elemento de sugerencia 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.

Captura de pantalla del árbol de accesibilidad de las Herramientas para desarrolladores de Chrome 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. En su interior, hay texto estático de &quot;superior&quot; y un elemento de sugerencia.

A continuación, necesitamos que la sugerencia no se pueda enfocar. Si un lector de pantalla no comprende el rol de la sugerencia, permitirá que los usuarios enfoquen el <tool-tip> para leer el contenido, y la experiencia del usuario no necesita esto. Los lectores de pantalla agregarán el contenido al elemento principal 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 el contenido de esta sugerencia en el 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 sugerencia.

Luego, decidí usar atributos como la interfaz para especificar la posición de la sugerencia. De forma predeterminada, todos los <tool-tip>s adoptarán una posición "superior", pero la posición se puede personalizar 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 cosas, de modo que el <tool-tip> no pueda tener varias posiciones asignadas al mismo tiempo. Puede haber solo uno o ninguno.

Por último, coloca elementos <tool-tip> dentro del elemento para el que deseas proporcionar una sugerencia. 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 sugerencia que dice &quot;El logotipo de calavera de 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>

Captura de pantalla de un párrafo con el acrónimo HTML subrayado y una sugerencia sobre él que dice &quot;Hyper Text Markup Language&quot;.

Accesibilidad

Como elegí crear Tooltips y no Toggletips, esta sección es mucho más simple. Primero, permítanme describir la experiencia del usuario que deseamos:

  1. En espacios limitados o interfaces desordenadas, oculta los mensajes complementarios.
  2. Cuando un usuario coloque el cursor sobre un elemento, lo enfoque o lo toque para interactuar con él, revela el mensaje.
  3. Cuando finaliza el desplazamiento, el enfoque o el toque, vuelve a ocultar el mensaje.
  4. Por último, asegúrate de reducir cualquier movimiento si el usuario especificó una preferencia por la reducción de movimiento.

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 sus ojos. Un usuario de lector de pantalla no vidente puede enfocar 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 sugerencia

En la sección anterior, analizamos el árbol de accesibilidad, el rol de la sugerencia y el atributo inert. Lo que queda es probarlo y verificar que la experiencia del usuario revele el mensaje de la sugerencia de forma adecuada. Después de realizar pruebas, no queda claro qué parte del mensaje audible es una información sobre la herramienta. También se puede ver mientras se depura en el árbol de accesibilidad, el texto del vínculo de "arriba" se ejecuta junto, sin dudar, con "¡Mira, sugerencias!". El lector de pantalla no interrumpe ni identifica el texto como contenido de la sugerencia.

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

Agrega un seudoelemento solo para lectores de pantalla al <tool-tip> y podremos agregar nuestro propio texto de mensaje para los usuarios no videntes.

&::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 sugerencia "Tiene sugerencia: ".

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

Ahora, cuando un usuario de lector de pantalla enfoca el vínculo, se dice "arriba", se hace una pequeña pausa y, luego, se anuncia "tiene información sobre la herramienta: mira, información sobre la herramienta". Esto le brinda al usuario de un lector de pantalla algunas sugerencias útiles sobre la UX. La vacilación genera una separación agradable entre el texto del vínculo y la sugerencia. Además, cuando se anuncia que hay una sugerencia, el usuario de un lector de pantalla puede cancelarla fácilmente si ya la escuchó. Es muy similar a pasar el cursor y quitarlo rápidamente, ya que 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 mensajes complementarios, por lo que primero comenzaremos con los aspectos básicos del efecto de superposición. Quítalo del flujo de documentos con position absolute:

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

Si el elemento principal 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 nuevo selector en el bloque que puede ayudar, :has():

Browser Support

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

Source

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

No te preocupes demasiado por la compatibilidad con los navegadores. Primero, recuerda que estas sugerencias son complementarias. Si no funcionan, no debería haber problemas. En segundo lugar, en la sección de JavaScript, implementaremos una secuencia de comandos para proporcionar la funcionalidad que necesitamos para los navegadores sin compatibilidad con :has().

A continuación, haremos que las sugerencias no sean interactivas para que no quiten los eventos del puntero de su elemento principal:

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

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

tool-tip {
  opacity: 0;
}

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

:is() y :has() hacen el trabajo pesado aquí, ya que hacen que tool-tip que contiene elementos superiores conozca la interactividad del usuario para alternar la visibilidad de una sugerencia secundaria. Los usuarios de mouse pueden colocar el cursor sobre el elemento, los usuarios de teclado y lectores de pantalla pueden enfocarlo, y los usuarios de dispositivos táctiles pueden presionarlo.

Ahora que la superposición de mostrar y ocultar funciona para los usuarios con visión, es hora de agregar algunos estilos para la tematización, el posicionamiento y la incorporación de la forma de triángulo a la burbuja. Los siguientes estilos comienzan a usar propiedades personalizadas, basándose en lo que hemos hecho hasta ahora, pero también agregan sombras, tipografía y colores para que parezca una sugerencia flotante:

Captura de pantalla de la sugerencia en modo oscuro, que flota sobre el vínculo “block-start”.

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 de temas

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, solo podemos actualizar esas propiedades y dejar que el tema se encargue del resto:

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

Captura de pantalla comparativa de las versiones clara y oscura de la sugerencia.

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

De derecha a izquierda

Para admitir los 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;
}

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

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

Además de ayudar a determinar dónde se encuentra 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 la información sobre herramientas

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

Alineación en la parte superior y al inicio del bloque

Captura de pantalla que muestra la diferencia de posición entre la parte superior de izquierda a derecha y la parte 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 al final de la línea

Captura de pantalla que muestra la diferencia de posición entre la posición derecha de izquierda a derecha y la posición inline-end 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 al inicio de la línea

Captura de pantalla que muestra la diferencia de posición entre la posición izquierda de izquierda a derecha y la posición inline-start 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 sugerencia. 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 sugerencia aparezca como si se deslizara desde el elemento principal.

Una transición predeterminada segura y significativa

Aplica un estilo al elemento de información sobre herramientas para que realice una 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 lado en el que puede aparecer una sugerencia, si el usuario acepta el movimiento, posiciona ligeramente la propiedad translateX dándole una pequeña distancia para recorrer desde:

@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;
  }
}

Observa que se establece el estado "fuera", ya que el estado "dentro" está en translateX(0).

JavaScript

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

El script de polyfill hace dos cosas, 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 principales de los <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, inyecta un conjunto de estilos que usen ese nombre de clase, simulando el selector :has() para 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 sugerencias sin problemas si no se admite :has().

Conclusión

Ahora que sabes cómo lo hice, ¿cómo lo harías tú? 🙂 Tengo muchas ganas de usar la API de popup para facilitar las sugerencias emergentes, la capa superior para evitar conflictos con el índice Z y la API de anchor para posicionar mejor los elementos en la ventana. Hasta entonces, crearé tooltips.

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

Crea una demostración, envíame por Twitter los vínculos y la agregaré a la sección de remixes de la comunidad que se encuentra a continuación.

Remixes de la comunidad

Aún no hay nada para ver aquí.

Recursos