Ver todas las publicaciones
Captura del demo accesible de Pretext mostrando un layout de texto estilo revista con cinco orbes de colores arrastrables que fluyen alrededor de las columnas de texto

Pretext: demo accessible

Qué hice

Rehice el demo de Pretext, creado por Cheng Lou, donde se muestra un "editorial engine" (motor editorial?) e hice mi propio demo completamente accessible. Mi demo incluye estructura HTML semántica, operabilidad por teclado, soporte para lectores de pantalla y soporte de prefers-reduced-motion. El resultado es una demo accesible y performante que cumple con los criterios de la WCAG 2.2 sin comprometer la estética ni el rendimiento. Las orbes siguen siendo arrastrables con el mouse, pero con el agregado de que ahora también se pueden mover usando el teclado.

Este demo utiliza un sistema de renderizado dual. Cuando no hay zoom, pretext crea una capa visual donde las líneas de texto se adaptan dinámicamente a la posición de las orbes que se mueven por todo el documento. Por debajo de esta capa, hay un elemento de HTML nativo <article> que con la ayuda de la propiedad de CSS column-count permanece de forma estática en el DOM para que lectores de pantalla puedan anunciar su contenido de manera fluída, para que los usuarios puedan copiar el texto, pegarlo en otras aplicaciones o usar la funcionalidad de búsqueda en la página, etc. Cuando el usuario hace zoom en el navegador por encima del 150% o el ancho de la pantalla es menor a 500px, la capa visual de pretext se elimina completamente y el artículo HTML nativo se convierte en el diseño visible. Finalmente, la letra inicial del texto cuando no hay zoom o el viewport es amplio utiliza pretext para su estilado, mientras que cuando hay zoom o la pantalla es pequeña, el estilado de la letra inicial se maneja utilizando la propiedad nativa de CSS ::first-letter.

Uso de la librería Pretext

La librería Pretext es el protagonista detrás del renderizado y el layout del texto. Usé las siguientes APIs:

APIPropósitoDonde se usa
prepareWithSegments()Mide y almacena el ancho de las palabras para cada párrafoPreparar el texto cuando se carga la fuente
layoutNextLine()Cada línea de texto se acomoda en un ancho máximo dadoOrganiza las columnas del layout, se llama por línea o por espacio disponible
layoutWithLines()Acomoda todas las líneas de texto en un bloque de texto ya preparadoOrdena los encabezados
walkLineRanges()Itera sobre los rangos de líneas sin asignar objetos de líneaBúsqueda binaria de encabezados, ancho de capítulos iniciales

Mejoras de accesibilidad

Estructura de HTML semántica

Esta aplicación utiliza elementos semánticos y landmarks de HTML5:

  • <header>: elemento que agrupa los controles de interacción de la página
  • <main>: contenido principal (texto y orbes)
  • <article lang="es">: elemento independiente que contiene el texto legible para usuarios visuales y el texto accessible para usuarios de tecnologías asistivas con el idioma español
  • <section>: región donde se contienen las orbes
  • <footer>: elemento que contiene las estadísticas de rendimiento y los créditos
  • <kbd>: representación semántica en HTML de un dispositivo de entrada de texto (teclado, dispositivo de voz, etc).
  • <cite>: cita el título de una obra.
  • <nav>: sección de atajos de teclado y navegación de créditos.

Accessibilidad por teclado

Cada elemento interactivo es operable y navegable con el teclado

AcciónTeclaCon lector de pantalla
Pausar globalmente las orbesEscEsc
Navegar a las orbesTabTab
Mover la orbe seleccionadaArrow keysOption + Arrow keys
Pausar/continuar orbe individualmenteSpaceSpace

Las orbes son elementos <button> nativos, por lo que se anuncian como botones, reciben foco acorde con el orden lógico de tabulación se pueden operar con Space/Enter para pausar/reanudar y mover usando las Arrow keys. aria-roledescription="draggable orb" complementa el anuncio del rol para que el usuario entienda que no son botones ordinarios. El aria-label de cada orbe es dinámico y codifica su posición en el conjunto, las interacciones disponibles y su estado actual de pausa.

Accessibilidad con lectores de pantalla

Cuando un usuario de lector de pantalla navega a una orbe, percibe: "Golden orb, 1 of 5. Use Option plus arrow keys to move. Press Space to pause." (traducción: "Orbe dorada, 1 de 5. Use la tecla Option más flechas para mover. Presione Espacio para pausar.") La instrucción está integrada en el aria-label de las orbes porque los lectores de pantalla tienen su propia navegación con las teclas de fecha y no podemos sobreescribirlas, por eso debemos crear una nueva combinación de teclas para evitar conflictos en la navegación. En este caso, para mover las orbes, utilizamos la combinación Option + Arrow. Esto mantiene el modelo de interacción estándar del lector de pantalla intacto y hace que las capacidades de las orbes sean auto-descriptivas, sin sacrificar el control por teclado.

Cuando la orbe esta pausada, la etiqueta cambia a "Press Space to resume." ("Presione Espacio para reanudar.") para que el usuario siempre sepa la acción actual disponible sin tener que adivinar.

Soporte de lectores de pantalla y ARIA

  • aria-live="polite": anuncia cualquier cambio en el estado de la orbe (pausa/reanudación) sin interrumpir al usuario.
  • aria-pressed: comunica el estado de los botones de alternancia (encendido/apagado)
  • aria-label: cada elemento interactivo tiene una etiqueta descriptiva y dinámica

Soporte para prefers-reduced-motion

Cuando el usuario tiene habilitada la preferencia de reducción de movimiento (prefers-reduced-motion) en su sistema operativo:

  • Todas las orbes dejan de moverse
  • El texto deja de adaptarse dinámicamente a la posición de las orbes y se mantiene fijo
  • CSS scroll-snap-type y scroll-behavior: smooth están deshabilitados
  • Las transiciones y animaciones de CSS son suprimidas globalmente via animation-duration: 0.01ms !important
  • El usuario puede anular la reducción de movimiento en la aplicación volviendo a activar la animación con el botón de toggle

Control de pausado de las animaciones

  • Pausa global (botón o Esc): Pausa todas las animaciones. Las orbes quedan fijas donde estaban en el documento y no se mueven si el usuario desplaza la página.
  • Pausa individual (click o Space en una orbe individual): Solo la orbe seleccionada deja de moverse por el documento. Las demás orbes continúan moviéndose. Aún así la orbe pausada puede ser arrastrada por el usuario através del documento mediante las arrow keys o el mouse.
  • El botón de pausa refleja el estado agregado. Si todas las orbes están pausadas individualmente, muestra "Play"; si todas están en movimiento, muestra "Pause".

Comportamiento del texto y pantalla según el tamaño de la pantalla y el nivel de zoom

Renderizado dual con pretext y HTML nativo

  • Capa visual de pretext (aria-hidden="true"): Cada línea es un <div> con una posición absoluta manejada por layoutNextLine() que envuelve a las orbes como si fuesen obstáculos circulares. La altura de la capa se calcula a partir de la posición más baja de la línea.
  • elemento nativo de HTML: <article>: cuando la capa de pretext está activa, el <article lang="es"> se oculta visualmente con position: absolute; left: -9999px (no display: none, para que los lectores de pantalla puedan leerlo).
  • el estado useNativeLayout se obtiene a partir de la división entre window.outerWidth / window.innerWidth para decidir cuál de los dos modos es el que se renderizará.
Ancho de la pantallaColumnas
> 1000px3 columnas
641-1000px2 columnas
< 640px1 columna

Reubicación dinámica del texto alrededor de las orbes

Cuando la capa de Pretext está activa, la posición de cada orbe y su radio se convierten en un obstáculo circular a través de orbToObstacle(). El motor de layout (layoutAllText()) los alimenta dinámicamente a través de la función layoutNextLine(), que acorta o desplaza las líneas para que puedan acomodarse alrededor de las orbes.

Encabezados dinámicos con fitHeadline()

Los encabezados se acomodan dinámicamente utilizando la función fitHeadline() que utiliza layoutWithLines() de pretext para encontrar el tamaño de fuente más grande que permita que el encabezado se ajuste dentro del ancho disponible y una altura máxima (35% del viewport, o 20% en pantallas pequeñas). Cada línea del encabezado se renderiza como un elemento separado con posición absoluta.

Primera letra decorativa (drop cap) del primer párrafo

Cuando la capa de Pretext está activa, la primera letra del primer párrafo se renderiza como un <div> posicionado que abarca 3 líneas de altura. El motor de layout reserva una región rectangular para el drop cap y hace que el texto del primer párrafo fluya alrededor de él. En modo nativo,se utiliza la propiedad nativa de CSS ::first-letter que proporciona el mismo efecto.

Uso dinámico del header

El elemento <article> tiene un padding-top como valor tiene una propiedad de CSS customizada --header-h. Esta propiedad depende de ResizeObserver. Cuando la capa de Pretext está activa, el header se calcula con la fórmula Math.max(GUTTER, headerHeight + 8) para asegurar que el contenido del <article> nunca quede oculto detrás del header fijo, sin importar el nivel de zoom o el tamaño del viewport.

Comportamiento en pantallas pequeñas y con zoom >= 150%

Las orbes y la capa de Pretext se eliminan completamente

Cuando el ancho de la pantalla es menor a 500px o el zoom del navegador es mayor o igual al 150%, React desmonta condicionalmente la sección de las orbes y la etapa de pretext por completo (no display: none), y useNativeLayout también detiene el loop de renderFrame para que no se ejecute ningún trabajo de layout en segundo plano.

El zoom se detecta mediante window.outerWidth / window.innerWidth: cuando el usuario hace zoom in, innerWidth se reduce mientras que outerWidth permanece constante, lo que da la relación de zoom real.

El header cuando el nivel de zoom es alto o la pantalla es pequeña

Cuando el ancho de la pantalla es menor a 500px o el zoom del navegador es mayor o igual a 200%, el header se compacta y se convierte en un elemento colapsable utilizando los elementos nativos de HTML <details> y <summary> para prevenir que el header consuma espacio necesario para el contenido principal.

La API ResizeObserver sigue activa incluso en esta vista, por lo que el padding-top del artículo se ajusta dinámicamente a la altura del header incluso cuando está colapsado, asegurando que el contenido principal siempre sea visible y no quede oculto detrás del header.

El footer y el header en pantallas pequeñas

Cuando el ancho de la pantalla es menor a 640px:

  • El header se convierte en un elemento colapable utilizando <details>/<summary>
  • El footer se ordena verticalmente (solo créditos)
  • El texto de los fotos y el tamaño de las letras de las recomendaciones se reducen
  • El texto se convierte en una sola columna

Comportamiento de desplazamiento (scrolling)

html { scroll-snap-type: y proximity; scroll-behavior: smooth; } hace que el desplazamiento se acerque a alineaciones de snap sin atrapar el desplazamiento por teclado (Space, Page Down, flechas siguen funcionando porque el tipo es proximity, no mandatory). Si prefers-reduced-motion: reduce está activado a nivel de sistema operativo, ambas propiedades vuelven a auto / none.

WCAG success criteria

The following WCAG 2.2 success criteria are relevant to this project:

CriterionLevelStatusNotes
1.3.1 Info and RelationshipsAPasaLa estructura semántica transmite la información de forma programática
1.3.2 Meaningful SequenceAPasaEl orden del DOM coincide con el orden de lectura visual
1.3.4 OrientationAAPasaEl diseño se adapta a los modos vertical y horizontal
1.4.1 Use of ColorAPasaEl estado de pausa utiliza opacidad + anuncio SR, no solo color
1.4.3 Contrast (Minimum)AAPasaTodo el texto cumple con la proporción 4.5:1
1.4.4 Resize TextAAPasaEl texto se reubica al hacer zoom al 200%; las orbes se eliminan al hacer zoom al 150%+ para prevenir obstrucción
1.4.10 ReflowAAPasaSingle column at narrow viewports, no horizontal scroll
1.4.11 Non-text ContrastAAPasaLos indicadores de foco cumplen con la proporción 3:1 contra los colores adyacentes
2.1.1 KeyboardAPasatodo es accesible por teclado
2.1.2 No Keyboard TrapAPasascroll-snap-type: proximity (not mandatory) previene el focus trap
2.2.2 Pause, Stop, HideAPasamanejo global de la pausa
2.3.3 Animation from InteractionsAAAPasacontempla prefers-reduced-motion
2.4.1 Bypass BlocksAPasaLandmark navigation via <header>, <main>, <footer>
2.4.3 Focus OrderAPasaOrden de tabulación lógico
3.1.1 Language of PageAPasaEl texto en inglés tiene el atributo lang="en" y el documento tiene el atributo lang="en"
3.1.2 Language of PartsAAPasaEl texto en español tiene el atributo lang="es"
4.1.2 Name, Role, ValueAPasaLos botones tienen etiquetas, los botones de alternancia tienen aria-pressed, las regiones vivas anuncian cambios

Tech stack