Compila un componente de información sobre la herramienta

Descripción general fundamental de cómo compilar un elemento personalizado de información sobre la herramienta que se adapte a los colores y sea accesible.

En esta publicación, quiero compartir mis ideas sobre cómo crear un elemento personalizado de <tool-tip> que se adapte a los colores y sea accesible. Prueba la demostración y mira la fuente.

Se muestra información sobre la herramienta funcionando en una variedad de ejemplos y esquemas de colores

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

Descripción general

Un cuadro de información es una superposición no modal, no bloqueada y no interactiva que contiene información complementaria a las interfaces de usuario. Se oculta de forma predeterminada y se muestra cuando se coloca el cursor sobre un elemento asociado o se enfoca. No se puede seleccionar un cuadro de información ni interactuar directamente con él. La información sobre la herramienta no reemplaza las etiquetas ni otra información de alto valor; un usuario debería poder completar su tarea sin esa información.

Qué hacer: Siempre etiqueta tus entradas.
No uses el cuadro de información en lugar de las etiquetas

Activar o desactivar información sobre la herramienta

Al igual que muchos componentes, hay diferentes descripciones de lo que es un cuadro de información, por ejemplo, en MDN, WAI ARIA, Sarah Higley y Componentes inclusivos. Me gusta la separación entre información sobre herramientas y sugerencias. La información sobre la herramienta debe contener información complementaria no interactiva, mientras que una sugerencia puede contener información importante y interactividad. 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 a los botones que contiene. Las teclas de activación se vuelven complejas rápidamente.

Este es un video de un botón de activación del sitio de Designcember; una superposición con interactividad que un usuario puede fijar y explorar y, luego, cerrarla con la opción para descartar con luz o la tecla Escape:

Este desafío de la GUI recorrió la ruta de un cuadro de información, en el que se buscaba hacer casi todo con CSS, y a continuación se muestra cómo compilarlo.

Marca

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

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

Es como un elemento div con un poco de texto en su interior. Podemos agregar [role="tooltip"] para integrar el árbol de accesibilidad de los lectores de pantalla compatibles.

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

Para los lectores de pantalla, se reconoce como información sobre la herramienta. En el siguiente ejemplo, puedes observar cómo el primer elemento de vínculo tiene un elemento de información sobre la herramienta reconocido en su árbol y el segundo no. El segundo no tiene ese rol. En la sección de diseños, 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. Se muestra un vínculo con el texto “arriba”; contiene información sobre la herramienta: “¡Hola, un cuadro de información” que es enfocable. Dentro de él, hay texto estático de “top” y un elemento de información sobre la herramienta.

A continuación, necesitamos que la información sobre la herramienta no sea enfocable. Si un lector de pantalla no comprende el rol de la información sobre la herramienta, permitirá que los usuarios enfoquen la <tool-tip> para leer el contenido. 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 enfocarlo para que sea accesible. Aquí, podemos usar inert para garantizar que ningún usuario encuentre accidentalmente este contenido de información sobre la herramienta en su flujo de pestaña:

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

Otra captura de pantalla del Árbol de accesibilidad de las Herramientas para desarrolladores de Chrome; esta vez falta el elemento de información sobre la herramienta.

Luego, elegí usar atributos como interfaz para especificar la posición del cuadro de información. De forma predeterminada, todos los objetos <tool-tip> 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 a la derecha que dice “Información sobre la herramienta”.

Tiendo a usar atributos en lugar de clases para este tipo de tareas, de modo que <tool-tip> no pueda tener varias posiciones asignadas al mismo tiempo. Puede haber una sola o ninguna.

Por último, coloca los elementos <tool-tip> dentro del elemento para el que deseas proporcionar información sobre la herramienta. Aquí comparto el texto alt con 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 información que dice “El logotipo de la calavera de los desafíos de la GUI”

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 un cuadro de información sobre él que dice &quot;Lenguaje de marcado de hipertexto&quot;.

Accesibilidad

Como elegí crear información sobre la herramienta en lugar de sugerencias de activación, esta sección es mucho más sencilla. Primero, permítanme describir cuál es la experiencia del usuario que deseamos:

  1. En espacios restringidos o interfaces desordenadas, oculta los mensajes complementarios.
  2. Cuando un usuario coloca el cursor sobre un elemento, lo enfoca o usa el tacto para interactuar con él, revela el mensaje.
  3. Cuando colocas el cursor sobre él, lo enfocas o lo tocas, vuelve a ocultar el mensaje.
  4. Por último, asegúrate de que se reduzca cualquier movimiento si un usuario especificó una preferencia por este tipo de movimiento.

Nuestro objetivo es enviar mensajes complementarios a pedido. Los usuarios de teclados o mouses videntes pueden desplazarse para revelar el mensaje y leerlo con los ojos. Los usuarios de lectores de pantalla sin visión pueden enfocarse para revelar el mensaje y escucharlo de forma audible a través de la herramienta.

Captura de pantalla de VoiceOver en macOS leyendo un vínculo con información sobre la herramienta

En la sección anterior, vimos el árbol de accesibilidad, la función de información sobre la herramienta y el inerte, lo que queda es probarlo y verificar la experiencia del usuario para que este le muestre correctamente el mensaje de la información sobre la herramienta. Después de la prueba, no está claro qué parte del mensaje sonoro es información sobre la herramienta. Se puede ver mientras se realiza la depuración en el árbol de accesibilidad, el texto del vínculo de "top" se ejecuta en conjunto, sin dudarlo, con "Look, tooltips!". El lector de pantalla no rompe ni identifica el texto como contenido de información sobre la herramienta.

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

Agregaremos un seudoelemento solo de lector de pantalla a <tool-tip>, y podremos agregar nuestro propio texto del mensaje para 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 el cuadro de información “Contiene información sobre la herramienta:”.

Captura de pantalla actualizada del árbol de accesibilidad de las Herramientas para desarrolladores de Chrome, en la que el texto del vínculo mejoró la frase &quot;parte superior&quot;; contiene información sobre la herramienta: ¡Hola, un cuadro de información!

Ahora, cuando el usuario de un lector de pantalla enfoque el vínculo, dice "parte superior" y hace una breve pausa. Luego, anuncia "con información sobre la herramienta: apariencia, información sobre la herramienta". Esto le da al usuario de lector de pantalla un par de buenas sugerencias de UX. La duda proporciona una buena separación entre el texto del vínculo y la información sobre la herramienta. Además, cuando se anuncia "tiene información sobre la herramienta", el usuario de un lector de pantalla puede cancelarla fácilmente si ya la escuchó antes. Esto se parece mucho a colocar el cursor sobre él y dejar de hacerlo rápidamente, dado que ya viste el mensaje complementario. Sentí como una buena paridad de UX.

Estilos

El elemento <tool-tip> será un elemento secundario del elemento para el que representa los mensajes complementarios, así que comencemos con los aspectos básicos del efecto de superposición. Quítalo del flujo del documento con position absolute:

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

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

Navegadores compatibles

  • 105
  • 105
  • 121
  • 15.4

Origen

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

No te preocupes demasiado por la compatibilidad con el navegador. En primer lugar, recuerda que estos cuadros de información son complementarios. Si no funcionan, no hay problema. En segundo lugar, en la sección de JavaScript, implementaremos una secuencia de comandos para polyfills de la funcionalidad que necesitamos para los navegadores que no son compatibles con :has().

A continuación, haremos que la información sobre la herramienta no sea interactiva para que no roben eventos de puntero de su elemento superior:

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

Luego, oculta el cuadro de información con opacidad para que podamos realizar la transición de este con un encadenado:

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 conozcan la interactividad del usuario para activar o desactivar la visibilidad de un cuadro de información secundario. Los usuarios del mouse pueden colocar el cursor sobre un elemento, el teclado y el lector de pantalla pueden enfocarse, y los usuarios táctiles pueden presionar.

Ahora que la superposición de mostrar y ocultar está funcionando para los usuarios videntes, es hora de agregar algunos diseños de temas, posicionar y agregar la forma de triángulo a la burbuja. En los siguientes estilos, se comienzan a usar propiedades personalizadas, que se basan en el lugar en el que estamos hasta ahora, pero también agregan sombras, tipografía y colores para que se vean como información sobre la herramienta flotante:

Captura de pantalla de la información sobre la herramienta en modo oscuro, flotando 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 tema

La información sobre la herramienta solo tiene algunos colores que se pueden 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 en paralelo de las versiones clara y oscura de la información sobre la herramienta.

Para el tema claro, adaptamos el fondo al blanco y hacemos que las sombras sean mucho menos fuertes 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;
}

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

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

Además, ayudan cuando el triángulo se encuentra en lo siguiente:

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 la herramienta

Posiciona la información sobre la herramienta 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. En el siguiente código, se muestra el diseño de cada una de las cuatro posiciones para las direcciones de izquierda a derecha y de derecha a izquierda.

Alineación superior e inicial con bloqueo

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 en los extremos derecho y en línea

Captura de pantalla que muestra la diferencia de posición entre la posición de izquierda a derecha y la posición del extremo 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 de bloques

Captura de pantalla que muestra la diferencia de posición entre la posición inferior de izquierda a derecha y la posición del extremo 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 de inicio en línea y a la izquierda

Captura de pantalla que muestra la diferencia de posición entre la posición de izquierda a derecha y la posición de inicio intercalado 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 cambiamos la visibilidad de la información sobre la herramienta. En esta sección, primero animaremos la opacidad para todos los usuarios, ya que, en general, es una transición de movimiento reducido que es segura. Luego, animaremos la posición de transformación para que la información sobre la herramienta parezca deslizarse desde el elemento superior.

Una transición predeterminada segura y significativa

Aplica diseño al elemento de información sobre la herramienta para hacer la transición de la opacidad y la 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

En cada uno de los lados, puede aparecer información sobre la herramienta. Si el usuario está de acuerdo con el movimiento, posiciona ligeramente la propiedad translateX proporcionando una distancia pequeña para 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 está configurando el estado “fuera”, ya que el estado “en” está en translateX(0).

JavaScript

En mi opinión, el JavaScript es opcional. Esto se debe a que ninguno de estos cuadros de información debería ser una lectura obligatoria para realizar una tarea en tu IU. Por lo tanto, si la información sobre la herramienta falla por completo, no debería ser un problema. Esto también significa que podemos tratar los cuadros de información como mejorados de forma progresiva. Eventualmente, todos los navegadores serán compatibles con :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 es compatible con :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 con el cual trabajar:

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 diseños que use ese nombre de clase, simulando el selector :has() para exactamente 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 la información sobre la herramienta de forma satisfactoria si :has() no es compatible.

Conclusión

Ahora que ya sabes cómo lo hice, ¿cómo lo harías? 🙂 Estoy ansioso por usar la API de popup para facilitar los botones de activación, la capa superior para no tener batallas con el índice z y la API de anchor para posicionar mejor los elementos en la ventana. Hasta entonces, seguiré haciéndoles información sobre la herramienta.

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

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

Remixes de la comunidad

Aún no hay nada que ver aquí.

Recursos