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.
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.
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.
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>
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>
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>
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>
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:
- En espacios restringidos o interfaces desordenadas, oculta los mensajes complementarios.
- Cuando un usuario coloca el cursor sobre un elemento, lo enfoca o usa el tacto para interactuar con él, revela el mensaje.
- Cuando colocas el cursor sobre él, lo enfocas o lo tocas, vuelve a ocultar el mensaje.
- 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.
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.
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:”.
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()
:
: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:
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%;
}
}
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
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
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
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
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
- Código fuente en GitHub