Ver tu sitio con otros ojos
TL;DR
- Construí un lab para ver este blog con otros ojos: daltonismo, visión baja, cataratas, glaucoma, degeneración macular, y un modo sin visión con lector de voz.
- La accesibilidad no es un feature que agregas al final. Es una propiedad de haber hecho bien lo básico — y mi blog pasó casi todo sin que yo lo trabajara.
- Lo único que reprobó fue el consejo que yo mismo escribí en otro post: “no te escondas detrás de colores”. Mi portada se escondía detrás del color.
- De las 4 barreras reales que encontré, 3 ya estaban resueltas en mi repo y nunca las subí a global.
Por qué me metí en esto
Programo de noche, muchas veces con la luz que hay y no la que debería. Y de un tiempo para acá, cuando llevo horas pegado a la pantalla, la vista se me pone borrosa — como si trajera cataratas encima. Me pasa cada vez más seguido. No necesito un doctor para saber a dónde va la cosa: diecisiete años de pantalla no se devuelven.
Eso me hizo pensar en mis papás. Para ellos, y para cualquier adulto mayor, moverse en un mundo donde todo está en línea —el banco, la cita del doctor, el recibo de la luz— ya es un pedo de todos los días. Y la mayoría de las páginas no están pensadas para unos ojos de setenta años.
Aquí la parte incómoda: la empatía es de las palabras más fáciles de decir y de las que menos se aplican de verdad. A mí tampoco me sale sola. Pero tengo una razón medio egoísta que la hace real — mis ojos van para el mismo lado. Así que en lugar de leerme otra checklist de WCAG, quise verlo. Literal.
Construí un simulador
/lab/a11y carga un sitio en un iframe y le aplica una condición visual encima. Ocho modos: los tres tipos de daltonismo, visión baja, cataratas, visión de túnel, degeneración macular, y sin visión.
Cada punto del slider es un estadio clínico con nombre: “catarata madura”, “glaucoma severo”, “deuteranopia”. Un porcentaje abstracto no me decía nada. Cada condición trae los estadios de una escala clínica publicada: LOCS III para cataratas, Hodapp-Parrish-Anderson para glaucoma, la clasificación de la OMS para visión baja, AREDS para mácula. Todas citadas en el lab.
Quise ir más lejos y poner percentiles —“de los que tienen cataratas, tal % está en este nivel”—. Ese dato no existe limpio: los estudios reportan prevalencia total, no distribución por estadio. Inventarlo habría sido justo el humo que este lab jura no vender. Donde no hay dato, el lab lo dice.
Una cicatriz técnica, corta: las matrices de daltonismo que copias de internet casi siempre están mal. Las buenas son las de Machado et al. (2009), y operan en RGB lineal — aplicarlas en sRGB da colores plausibles y equivocados. SVG usa linearRGB por default, pero lo dejé explícito:
<filter id="cvd" color-interpolation-filters="linearRGB">
<feColorMatrix type="matrix" values="..." />
</filter>
Sin visión: el modo que me quedó grande
El modo 9 apaga la pantalla y te deja con un lector de voz casero. Recorre el DOM, narra con speechSynthesis, y usa las teclas de NVDA: flechas para leer, H para saltar encabezados, K para enlaces.
Le puse una caption en vivo sobre el negro, que muestra lo que se está narrando: la idea es que un vidente vea lo que un ciego oiría. Terminó siendo lo más útil del lab.
Dos cosas que confiesa en su propia cara. Solo lee este sitio — la Same-Origin Policy sella el DOM de un iframe de otro origen, así que con una URL externa el modo se apaga y explica por qué. Y no es un screen reader: NVDA y VoiceOver tienen rotor, modo formularios, décadas de heurísticas. Lo mío es una aproximación para que alguien que ve sienta el problema.
Construyéndolo me cayó el veinte de algo: la tecla H salta encabezados. Si tu página brinca de h1 a h3, le rompes esa navegación a quien la usa para moverse. El orden de los headings es, literalmente, cómo alguien recorre tu página sin verla.
Pero un lector casero no se juzga solo. Hasta no ponerlo al lado del de verdad, no sabía si había construido algo decente o un juguete. Eso lo dejo para el final, porque terminó siendo lo que más me marcó.
Pruébalo tú
⚗ Zona de experimento — todo aquí está roto a propósito
Cada bloque se rompe bajo alguna condición. Llevan descripción en texto de qué falla, porque si lees esto con un lector de pantalla la falla visual no te llega — y sería irónico que el post sobre ceguera te dejara fuera justo aquí.
La paleta que se colapsa. Bajo deuteranomalía, el rojo, el verde y el café se vuelven el mismo mostaza; el naranja apenas se despega.
El semáforo solo-color. Bajo protanopia, “deploy ok” y “prod caído” son la misma pastilla parduzca. Aquí hice trampa a mi favor: llevan texto, así que el lector te salva — el que se jode es el daltónico que ve la pantalla.
El link que solo es link por su color.
En este párrafo hay una liga importante sin subrayado. Con visión típica brinca; con daltonismo rojo-verde es texto normal. Guárdate este, que en un rato me lo como yo.
El texto que castiga.
Esto está en 11px gris claro sobre blanco: contraste ~2.8:1 cuando WCAG pide 4.5:1. Súbele visión baja o cataratas y desaparece. Así se ven los disclaimers y la mitad de los captions de internet.
Y el que no se arregla con contraste. Con visión de túnel o degeneración macular, ninguna regla de color te salva: sin periferia no sabes dónde sigue la línea, sin centro no hay dónde enfocar. Lo único que ayuda es estructura — headings seguidos, párrafos cortos, una columna.
Lo apunté a mi propio blog
Iba con una teoría bien armada: mi verde terminal iba a reprobar bajo deuteranopia. Dos problemas — mi accent no es verde (es #4c4ee4, un índigo, llevaba meses en mi cara), y no reprobó, porque el índigo vive en un matiz que no depende de discriminación rojo-verde. Corrí los tokens a mano: oklch → sRGB lineal, matriz de Machado encima, ratio WCAG.
| Par | Normal | Protanopia | Deuteranopia | Tritanopia |
|---|---|---|---|---|
| Texto / fondo | 19.83 | 19.80 | 19.84 | 19.82 |
| Texto tenue / fondo | 6.44 | 6.37 | 6.48 | 6.43 |
| Links / fondo | 5.88 | 4.88 | 5.69 | 5.19 |
El daltonismo casi no mueve la aguja. Mi color de marca no falla, pero no fue decisión de accesibilidad: fue suerte con buen gusto.
Y las categorías que suelen doler pasaron todas, sin que yo les dedicara un sprint: cero <div onclick>, cero inputs sin label, landmarks reales, lang puesto, ningún focus ring destruido. Eso me lo dio la plataforma. Un <button> ya es focusable, ya responde a Enter, ya se anuncia como botón. Por eso digo que los frameworks son tus amigos: cada <div onclick> que escribes reimplementa —peor— algo que ya tenías.
Pero cuatro cosas ningún default te las regala, y las cuatro pegan justo donde no ves:
- Skip link. No tenía. Con teclado te comías 6 o 7 tabs por el header sticky antes de tocar el contenido. En cada navegación.
- Live region. El buscador escribía “3 resultados” en un
<p>mudo. Con lector de pantalla no oías nada, ni el “nada para «xyz»”. - Motion guard. El parallax corría aunque pidieras
prefers-reduced-motion— y el layout ya calculaba esa variable 60 líneas arriba, para otra cosa. - Una pista que no sea color. Los links del hero. Aquí me como el mío.
Porque en el post de Google Stitch escribí, palabras mías:
“diseña en escala de grises primero. Te obliga a crear jerarquía con spacing y contraste, no escondiéndote detrás de colores.”
Los links de mi hero eran índigo, sin subrayado, dentro de un párrafo gris. El detalle: el índigo del link y el gris del texto son casi igual de oscuros, lo único que cambia es el color. Si no distingues esos dos colores —por daltonismo, o en una pantalla en grises— el link se pierde en el párrafo como texto normal. Medido, el contraste entre link y texto es de 1.09:1; la regla pide al menos 3:1. Di el consejo en mi propio blog y no lo seguí en mi portada.
Y lo peor: ningún linter me lo iba a decir. axe le dio verde a ese contraste, Lighthouse también — contra el fondo estaba perfecto en 5.88:1. Lo agarré poniéndole protanopia encima y no encontrando mis propios links. Por eso construí un simulador y no otra checklist.
De esas cuatro, tres ya estaban resueltas en mi código: .prose a subrayaba, el lab usaba aria-live, BaseLayout calculaba reduceMotion. Los patrones buenos ya estaban escritos; solo que nunca subieron a global.
Y aquí es donde no quiero que se me malentienda: el framework hace muchísimo, pero no lo hace todo. Astro me dio los elementos nativos; el criterio para usarlos bien no lo da ninguna librería. Mis cuatro barreras son solo la punta. La lista sigue con cosas que dependen de tu contenido, no de la herramienta: el alt text que de verdad describa una imagen (y “logo” no describe nada), el orden de tus headings, devolver el foco cuando cierras un modal, el contraste de tus propios tokens. Ninguna versión mejor de la librería te resuelve eso — lo resuelve la atención al detalle que pones encima.
Mi blog pasó porque usé <button> cuando quería un botón, y el resto lo puso Astro. No por virtuoso.
Qué arreglé
| Categoría | Qué cambió |
|---|---|
| Bypass (2.4.1) | Skip link al #main como primer foco del body |
| Contenido dinámico | role="status" + aria-live en el buscador |
| Color como único canal (1.4.1) | Subrayado en los links del hero, igual que .prose a |
| Motion (2.2.2) | Guard global de prefers-reduced-motion |
| Foco | :focus-visible branded, promovido desde el lab |
| Landmarks | aria-label en los dos <nav> |
| Headings | Tres saltos h1→h3 corregidos |
| Alt text | Alt redundante fuera del logo, avatar e íconos |
El del alt merece nota: el logo tenía alt="notdefined.dev logo" junto al texto “notdefined”, así que el lector anunciaba “notdefined.dev logo notdefined” en cada página. alt="" y listo — el texto de al lado ya la nombra. Poner el alt vacío ahí es intencional: le dice al lector que esa imagen no aporta y que siga de largo.
Y para que no se degrade en tres meses, la accesibilidad quedó escrita como regla del repo en AGENTS.md, con su tabla de verificación, para que se revise y no se quede en buena intención.
Escuché mi propio sitio
Todo lo de arriba lo medí en la pantalla. Pero el lab entero apuntaba a otra cosa, así que hice lo que llevaba semanas construyendo para otros: prendí VoiceOver —el lector de pantalla de verdad, el de macOS— y lo puse a leerme mi blog con los ojos cerrados.
Lo primero, incómodo y honesto: el español suena horrible. Una voz de robot masacrando los acentos, comiéndose palabras. Pero la voz robótica sale del text-to-speech del sistema, no de VoiceOver. Mi lector casero usa la misma API del navegador y suena igual de mal. El español masacrado lo compartimos los dos; ahí no hay diferencia entre mi juguete y la herramienta real.
Lo que sí los separa es la navegación. VoiceOver hace mucho más que leer: te dice “enlace”, “artículo”, “encabezado nivel 2”, en qué posición vas —“3 de 12”—, y con el rotor te abre la lista de todos los enlaces, o todos los landmarks, para saltar directo adonde quieras. Mi lector lee el texto de corrido y ya. Hace como cuatro de las cosas que hace VoiceOver; el rotor y los landmarks son una liga mayor que ni intenté tocar. Ahí está la distancia, y es grande.
Y aquí va lo que no esperaba llevarme. Sé que quien de verdad necesita esto no es mi lector: escribo este blog para mí, para el Adrian de dentro de seis meses, y el público de mi industria no navega con lector de pantalla. Aun así creo que todos deberían oír su propio sitio una vez. No por WCAG, no por la multa del European Accessibility Act. Porque hasta que no oyes tu página leída por una voz que apenas pronuncia el español, tropezando en tu navegación, no tienes idea de qué construiste. Ponte los audífonos, cierra los ojos, y déjalo leer. Una vez.
Nota de caducidad
Todo esto se evaluó a mediados de 2026: WCAG 2.2, matrices de Machado 2009, datos de la OMS 2020, mis tokens de hoy. La accesibilidad caduca más lento que la IA, pero caduca. Si lees esto mucho después, corre el lab contra tu propio sitio en vez de creerme. Las fuentes están todas en el lab, en la sección de research.