
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:
| API | Propósito | Donde se usa |
|---|---|---|
prepareWithSegments() | Mide y almacena el ancho de las palabras para cada párrafo | Preparar el texto cuando se carga la fuente |
layoutNextLine() | Cada línea de texto se acomoda en un ancho máximo dado | Organiza 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 preparado | Ordena los encabezados |
walkLineRanges() | Itera sobre los rangos de líneas sin asignar objetos de línea | Bú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ón | Tecla | Con lector de pantalla |
|---|---|---|
| Pausar globalmente las orbes | Esc | Esc |
| Navegar a las orbes | Tab | Tab |
| Mover la orbe seleccionada | Arrow keys | Option + Arrow keys |
| Pausar/continuar orbe individualmente | Space | Space |
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-typeyscroll-behavior: smoothestá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
Spaceen 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 porlayoutNextLine()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 conposition: absolute; left: -9999px(nodisplay: none, para que los lectores de pantalla puedan leerlo). - el estado
useNativeLayoutse obtiene a partir de la división entrewindow.outerWidth / window.innerWidthpara decidir cuál de los dos modos es el que se renderizará.
| Ancho de la pantalla | Columnas |
|---|---|
| > 1000px | 3 columnas |
| 641-1000px | 2 columnas |
| < 640px | 1 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:
| Criterion | Level | Status | Notes |
|---|---|---|---|
| 1.3.1 Info and Relationships | A | Pasa | La estructura semántica transmite la información de forma programática |
| 1.3.2 Meaningful Sequence | A | Pasa | El orden del DOM coincide con el orden de lectura visual |
| 1.3.4 Orientation | AA | Pasa | El diseño se adapta a los modos vertical y horizontal |
| 1.4.1 Use of Color | A | Pasa | El estado de pausa utiliza opacidad + anuncio SR, no solo color |
| 1.4.3 Contrast (Minimum) | AA | Pasa | Todo el texto cumple con la proporción 4.5:1 |
| 1.4.4 Resize Text | AA | Pasa | El texto se reubica al hacer zoom al 200%; las orbes se eliminan al hacer zoom al 150%+ para prevenir obstrucción |
| 1.4.10 Reflow | AA | Pasa | Single column at narrow viewports, no horizontal scroll |
| 1.4.11 Non-text Contrast | AA | Pasa | Los indicadores de foco cumplen con la proporción 3:1 contra los colores adyacentes |
| 2.1.1 Keyboard | A | Pasa | todo es accesible por teclado |
| 2.1.2 No Keyboard Trap | A | Pasa | scroll-snap-type: proximity (not mandatory) previene el focus trap |
| 2.2.2 Pause, Stop, Hide | A | Pasa | manejo global de la pausa |
| 2.3.3 Animation from Interactions | AAA | Pasa | contempla prefers-reduced-motion |
| 2.4.1 Bypass Blocks | A | Pasa | Landmark navigation via <header>, <main>, <footer> |
| 2.4.3 Focus Order | A | Pasa | Orden de tabulación lógico |
| 3.1.1 Language of Page | A | Pasa | El texto en inglés tiene el atributo lang="en" y el documento tiene el atributo lang="en" |
| 3.1.2 Language of Parts | AA | Pasa | El texto en español tiene el atributo lang="es" |
| 4.1.2 Name, Role, Value | A | Pasa | Los botones tienen etiquetas, los botones de alternancia tienen aria-pressed, las regiones vivas anuncian cambios |
Tech stack
- Claude calculos.
- React 19
- TypeScript 5.9
- Vite 8
- @chenglou/pretext 0.0.3
- Atkinson Hyperlegible, font diseñado por el Braille Institute para obtener la máxima legibilidad.