Hemos construido la base semántica (04-01), los formularios (04-02) y la capa ARIA declarativa (04-03). Falta la parte más visual del principio Perceptible, y es casi toda CSS: que el texto tenga suficiente contraste, que no se dependa solo del color para transmitir información, que el contenido se pueda ampliar hasta el 200% sin romperse, que fluya a pantallas estrechas sin scroll horizontal, que respete el espaciado de texto y que el foco sea siempre visible. Son los criterios 1.4.3, 1.4.11, 1.4.1, 1.4.4, 1.4.10, 1.4.12 y 2.4.7/2.4.11 que en el Módulo 3 estudiamos como requisito y que ahora implementamos en CSS.

Lo aplicaremos a la paleta de marca de Cursalia —revisando un color que no cumple y corrigiéndolo— y pensando muy en particular en Sofía, que tiene baja visión y navega con el zoom al 300%.

Contenido

  1. Ratios de contraste WCAG (1.4.3 y 1.4.11)
  2. Cómo medir el contraste
  3. No depender solo del color (1.4.1)
  4. Redimensionado de texto al 200% y unidades relativas (1.4.4)
  5. Reflow a 320 CSS px (1.4.10)
  6. Espaciado de texto (1.4.12)
  7. Indicador de foco visible (2.4.7 / 2.4.11)
  8. Revisión de la paleta de marca de Cursalia
  9. Errores comunes y consejos
  10. Ejercicios
  11. Conclusión

Ratios de contraste WCAG (1.4.3 y 1.4.11)

El contraste es la diferencia de luminancia entre dos colores, expresada como una razón de 1:1 (nulo) a 21:1 (negro sobre blanco). WCAG fija mínimos en nivel AA:

Elemento Ratio mínimo AA Criterio
Texto normal (< 24 px, o < 18.66 px si es negrita) 4.5 : 1 1.4.3
Texto grande (≥ 24 px, o ≥ 18.66 px negrita) 3 : 1 1.4.3
Componentes de interfaz y gráficos (bordes de input, iconos, límites de un botón) 3 : 1 1.4.11

El criterio 1.4.11 (contraste no textual) es el que suele olvidarse: el borde de un campo de formulario, el icono de una tarjeta o la línea de un gráfico de progreso también deben distinguirse del fondo con al menos 3:1. Un input cuyo único borde es un gris clarísimo sobre blanco es invisible para Sofía aunque su texto contraste bien.

/* INCORRECTO: gris sobre blanco ≈ 2.8:1, no llega a 4.5:1 */
.texto { color: #999999; background: #ffffff; }

/* CORRECTO: gris oscuro sobre blanco ≈ 7:1 */
.texto { color: #595959; background: #ffffff; }

Cómo medir el contraste

No se estima "a ojo": se mide. Herramientas habituales (el detalle de testing es del Módulo 6):

  • DevTools del navegador: al inspeccionar texto, el selector de color muestra el ratio y si pasa AA/AAA.
  • WebAIM Contrast Checker: pegas los dos colores y da el ratio.
  • Extensiones como axe DevTools o Lighthouse, que auditan la página entera.

La regla práctica: cada par (color de texto, color de fondo) de la paleta debe verificarse. Y ojo con el texto sobre imágenes o degradados: si el fondo varía, hay que garantizar el contraste en la zona más desfavorable (una capa semitransparente detrás del texto lo resuelve).

No depender solo del color (1.4.1)

El criterio 1.4.1 exige que ninguna información se transmita únicamente por el color. Marta (que no ve) y cualquier persona con daltonismo perderían ese dato. Siempre hay que añadir un segundo canal: texto, icono, forma o patrón.

Casos típicos en Cursalia:

  • Errores de formulario (visto en 04-02): no solo borde rojo → añade texto "Error:" y un icono.
  • Enlaces dentro de un párrafo: si solo se distinguen por el color, quien no percibe ese color no sabe que son enlaces → subráyalos o dales otra pista visible.
  • Gráfico de progreso del curso: si "completado / en curso / bloqueado" se codifica solo con verde/amarillo/gris, añade etiqueta de texto, icono o patrón.
/* INCORRECTO: los enlaces solo se distinguen por el color */
a { color: #1a6feb; text-decoration: none; }

/* CORRECTO: además del color, subrayado (segundo canal visible) */
a { color: #1a6feb; text-decoration: underline; }
<!-- INCORRECTO: el estado del curso vive solo en la clase de color -->
<li class="verde">Módulo 1</li>
<li class="gris">Módulo 2</li>

<!-- CORRECTO: color + texto + icono (tres canales) -->
<li class="completado"><span aria-hidden="true">✔</span> Módulo 1 — Completado</li>
<li class="bloqueado"><span aria-hidden="true">🔒</span> Módulo 2 — Bloqueado</li>

Redimensionado de texto al 200% y unidades relativas (1.4.4)

El criterio 1.4.4 exige que el texto pueda ampliarse hasta el 200% sin perder contenido ni funcionalidad. La causa número uno de fallo es fijar tamaños de fuente en píxeles absolutos: font-size: 16px no responde bien a la preferencia de tamaño del usuario.

La solución es usar unidades relativas:

  • rem: relativo al tamaño base del documento (el que el usuario configura en su navegador). Ideal para tamaños de fuente.
  • em: relativo al tamaño del elemento padre. Útil para espaciados proporcionales.
  • Evita px para tipografía y para contenedores que deban crecer con el texto.
/* INCORRECTO: px absoluto, ignora la preferencia del usuario */
body { font-size: 16px; }
h1   { font-size: 32px; }

/* CORRECTO: rem/em respetan el tamaño base configurado */
html { font-size: 100%; }        /* respeta la preferencia del navegador */
body { font-size: 1rem; }        /* ≈ 16px, pero escalable */
h1   { font-size: 2rem; }        /* crece en proporción */

Si Sofía sube el tamaño de fuente base a 200%, con rem todo el texto crece de forma coherente. Con px no se movería. Además, no fijes alturas rígidas en px a contenedores de texto: un height: 40px en un botón hace que el texto ampliado se corte; usa min-height y deja que el padding respire.

Reflow a 320 CSS px (1.4.10)

El criterio 1.4.10 (Reflow) exige que el contenido se pueda ver sin scroll en dos dimensiones a un ancho equivalente a 320 CSS px (lo que ocurre, por ejemplo, cuando Sofía amplía al 400% en un monitor de 1280 px). Es decir: puede haber scroll vertical (normal), pero no horizontal. El contenido debe reajustarse (reflow), no salirse.

Esto se consigue con diseño responsive/fluido:

/* INCORRECTO: ancho fijo que provoca scroll horizontal al ampliar */
.catalogo { width: 1200px; }
.tarjeta  { width: 380px; float: left; }

/* CORRECTO: rejilla fluida que se reajusta a una columna en pantallas estrechas */
.catalogo {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1rem;
  max-width: 100%;
}
img { max-width: 100%; height: auto; }   /* imágenes que no desbordan */

minmax(280px, 1fr) con auto-fill hace que las tarjetas del catálogo pasen de varias columnas a una sola sin que nada se salga por el lateral. Usa también @media queries para reorganizar la maqueta en anchos pequeños. Excepción del criterio: contenido que necesita dos dimensiones (tablas grandes, mapas) puede requerir scroll, pero es la excepción, no la norma.

Espaciado de texto (1.4.12)

El criterio 1.4.12 exige que, si el usuario fuerza ciertos espaciados (para mejorar su legibilidad, algo que ayuda mucho a Hugo con dislexia), el contenido no se rompa ni se solape. Los valores que debe soportar tu CSS:

Propiedad Valor que el usuario puede imponer
line-height (interlineado) al menos 1.5 × tamaño de fuente
Espacio entre párrafos al menos 2 × tamaño de fuente
letter-spacing al menos 0.12 × tamaño de fuente
word-spacing al menos 0.16 × tamaño de fuente

La clave para cumplirlo: no fijes alturas rígidas ni uses overflow: hidden en contenedores de texto. Si un botón o una tarjeta tiene altura fija, al aumentar el interlineado el texto se corta.

/* CORRECTO: base cómoda y contenedores que crecen con el texto */
p       { line-height: 1.6; margin-bottom: 1.5em; }
.tarjeta { min-height: 8rem; overflow: visible; }  /* min-height, no height */

Indicador de foco visible (2.4.7 / 2.4.11)

Cuando Lucía tabula con el teclado, necesita ver dónde está el foco en cada momento (criterio 2.4.7, Foco visible; y 2.4.11 refuerza que ese indicador no quede tapado). El navegador dibuja un outline por defecto... y el error clásico es borrarlo por estética:

/* INCORRECTO Y GRAVE: elimina el foco sin dar nada a cambio */
:focus { outline: none; }

Con eso, Lucía queda "a ciegas": tabula sin saber en qué control está. Si por diseño quieres cambiar el estilo del foco, sustitúyelo, nunca lo elimines sin más. La herramienta moderna es :focus-visible, que muestra el anillo cuando se navega con teclado pero no al hacer clic con ratón:

/* CORRECTO: foco propio, bien visible y con buen contraste */
:focus-visible {
  outline: 3px solid #1a6feb;   /* grosor claro */
  outline-offset: 2px;          /* separación para que no lo tape el borde */
  border-radius: 4px;
}

Requisitos del indicador: debe tener contraste suficiente (mínimo 3:1 frente a lo que hay alrededor, en línea con 1.4.11), ser grueso y no quedar oculto tras otros elementos. Enlazamos con los criterios de teclado 2.4.7/2.4.11; el manejo programático del foco al abrir diálogos y widgets es del Módulo 5.

Revisión de la paleta de marca de Cursalia

Pongamos en práctica todo con la paleta de Cursalia. El color corporativo es un azul de marca que se usa para botones y enlaces. Auditamos:

Uso Color texto Fondo Ratio ¿Cumple AA (4.5:1)?
Botón primario #ffffff #4a9cf5 (azul claro de marca) 2.3 : 1 ❌ No
Botón primario (corregido) #ffffff #1a6feb (azul oscurecido) 4.6 : 1 ✅ Sí
Texto de ayuda gris #9a9a9a #ffffff 2.8 : 1 ❌ No
Texto de ayuda (corregido) #595959 #ffffff 7 : 1 ✅ Sí

El azul claro de marca queda bonito, pero con texto blanco encima no llega a 4.5:1: Sofía no lo lee con comodidad. La corrección no obliga a cambiar la identidad: se oscurece el azul lo justo para cumplir, reservando el tono claro original para fondos decorativos donde no haya texto encima.

/* INCORRECTO: azul de marca demasiado claro para texto blanco (2.3:1) */
.btn-primario { background: #4a9cf5; color: #fff; }

/* CORRECTO: azul oscurecido que cumple AA (4.6:1) manteniendo la identidad */
.btn-primario {
  background: #1a6feb;
  color: #fff;
  border: 1px solid #1a6feb;   /* además, contraste no textual del borde: 1.4.11 */
}
.btn-primario:focus-visible {
  outline: 3px solid #0b3d91;
  outline-offset: 2px;
}

Errores Comunes y Consejos

  • Fiarse del ojo para el contraste. Mídelo siempre con una herramienta; los grises claros engañan.
  • Olvidar el contraste no textual (1.4.11). Bordes de inputs, iconos y líneas de gráficos también necesitan 3:1.
  • Codificar información solo con color. Añade siempre un segundo canal: texto, icono, subrayado, patrón.
  • Tipografía en px. Impide el escalado; usa rem/em y respeta el tamaño base del usuario.
  • Alturas fijas en contenedores de texto. Rompen el redimensionado (1.4.4) y el espaciado (1.4.12): usa min-height y evita overflow: hidden.
  • Anchos fijos que provocan scroll horizontal. Diseña con rejillas fluidas y max-width: 100% para cumplir el reflow (1.4.10).
  • outline: none sin sustituto. Es de los errores más dañinos para quien usa teclado. Usa :focus-visible con un anillo contrastado.
  • Consejo: prueba tu página con el zoom del navegador al 200% y al 400%, y recórrela con el teclado. Si aparece scroll horizontal, el texto se corta o pierdes de vista el foco, tienes trabajo pendiente.

Ejercicios

Ejercicio 1: corregir un contraste insuficiente

La etiqueta de "descuento" de Cursalia usa texto blanco #ffffff sobre un naranja claro #ffb066, con un ratio de 1.8:1. Explica por qué falla y propón una corrección que mantenga el naranja como color, indicando qué canal adicional añadirías.

Ejercicio 2: de px a unidades relativas

Refactoriza este CSS para que el texto escale al 200% y el botón no corte el texto al ampliarlo.

body { font-size: 14px; }
.cta {
  font-size: 18px;
  height: 36px;
  line-height: 36px;
  overflow: hidden;
}

Ejercicio 3: foco visible accesible

Un diseñador ha puesto :focus { outline: none; } en toda la SPA del panel del alumno "porque el borde azul es feo". Explica el problema y escribe una alternativa accesible con :focus-visible.

Soluciones

Solución 1. Falla porque 1.8:1 está muy por debajo del mínimo AA de 4.5:1 para texto normal (y de 3:1 incluso para texto grande): Sofía no distingue el texto del fondo. Corrección: oscurecer el naranja hasta lograr contraste con el texto (o poner texto oscuro sobre el naranja claro), y no depender solo del color de la etiqueta añadiendo la palabra "Descuento" o un icono:

/* Texto oscuro sobre el naranja claro: ≈ 6:1 */
.descuento { background: #ffb066; color: #3d2400; }
<span class="descuento"><span aria-hidden="true">🏷️</span> Descuento -20%</span>

Solución 2. Se sustituye px por rem, y la altura fija por padding + min-height, quitando el overflow: hidden:

body { font-size: 100%; }     /* respeta el tamaño base del usuario */
.cta {
  font-size: 1.125rem;        /* ≈ 18px, pero escalable */
  min-height: 2.5rem;         /* crece si hace falta, no corta */
  line-height: 1.5;
  padding: 0.5rem 1rem;
  overflow: visible;
}

Ahora, al ampliar al 200% o al forzar más interlineado (1.4.12), el botón crece con el texto en vez de recortarlo.

Solución 3. El problema: outline: none deja a Lucía (y a cualquiera que use teclado) sin saber qué elemento tiene el foco, incumpliendo 2.4.7. En una SPA con muchos controles, es especialmente grave. Alternativa: un indicador propio, visible y contrastado, solo para navegación con teclado:

:focus-visible {
  outline: 3px solid #1a6feb;
  outline-offset: 2px;
  border-radius: 4px;
}

Se mantiene la estética (no aparece anillo al hacer clic con ratón) sin sacrificar la accesibilidad del teclado.

Conclusión

Con esta lección cerramos el Módulo 4. Has implementado la cara visual de "Perceptible" en CSS: ratios de contraste 4.5:1 / 3:1 (texto y no textual), no depender solo del color, texto escalable con unidades relativas hasta el 200%, reflow sin scroll horizontal a 320 CSS px, espaciado de texto que no rompe la maqueta y un foco visible con :focus-visible que nunca se elimina sin sustituto. Y has corregido la paleta de Cursalia para que Sofía, con su zoom al 300%, lea sin barreras.

Con esto ya tenemos una base sólida y estática: HTML semántico (04-01), formularios accesibles (04-02), ARIA declarativa (04-03) y CSS accesible (04-04). Todo lo que hemos hecho describe qué es cada cosa y cómo se ve, pero aún no le hemos dado comportamiento. Ese es el salto del Módulo 5: dar vida con JavaScript a los widgets interactivos (el desplegable de filtros, el acordeón de FAQ), gestionar el teclado y el foco de forma programática, y hacer accesible el multimedia (vídeo y audio) y las imágenes con el atributo alt en profundidad. Pasamos de la estructura al comportamiento.

© Copyright 2026. Todos los derechos reservados