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.
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.
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.
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>
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>
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>
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 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:
- En espacios limitados o interfaces desordenadas, oculta los mensajes complementarios.
- 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.
- Cuando termines de colocar el cursor sobre el mensaje, enfocarlo o tocarlo, vuelve a ocultarlo.
- 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.
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.
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: ".
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()
:
: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:
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%;
}
}
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
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
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
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
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
- Código fuente en GitHub