Saltar al contenido
accesibilidad-css.css · devschool

CSS accesible

Lección 38 de 50 · 13 min de lectura · Actualizado el

En esta lección
  1. El foco tiene que verse
  2. Contraste de color
  3. Tamaño de texto en rem
  4. Objetivos táctiles grandes
  5. Respetar a quien no quiere movimiento: prefers-reduced-motion
  6. Más contraste: prefers-contrast y forced-colors
  7. Ocultar a la vista, pero no al lector de pantalla
  8. No transmitas información solo con color
  9. Orden visual y orden del DOM
  10. Zoom al 200 %
  11. Errores frecuentes
  12. Resumen

Una web accesible es la que puede usar cualquier persona: quien navega solo con el teclado, quien usa un lector de pantalla, quien ve mal los colores, quien amplía el texto al 200 % o quien se marea con las animaciones. En España, además, la accesibilidad es obligatoria para las webs públicas y, desde 2025, para muchos servicios privados. En la lección de accesibilidad en HTML viste la base: etiquetas semánticas, textos alternativos y etiquetas en los formularios. Aquí verás la parte que depende del CSS, que es mucha: con unas pocas líneas puedes hacer una web imposible de usar con el teclado, o al revés, una que funcione bien para todo el mundo.

El foco tiene que verse

Mucha gente navega sin ratón: con la tecla Tab salta de un enlace o botón al siguiente, y con Enter o la barra espaciadora lo activa. Para saber dónde está, el navegador dibuja un anillo de foco (focus ring) alrededor del elemento activo.

El error más grave del CSS accesible es este:

/* NO HAGAS ESTO */
*:focus {
  outline: none;
}

Esa regla, que se copia de plantillas antiguas porque “el borde azul queda feo”, deja a los usuarios de teclado a ciegas: no saben dónde están. Es como quitarle el puntero del ratón a alguien.

:focus-visible: foco solo cuando hace falta

La queja original tenía su parte de razón: al hacer clic con el ratón en un botón, a veces aparecía el anillo y molestaba. La pseudoclase :focus-visible lo resuelve: el navegador decide si el foco debe verse (con teclado, sí; con un clic de ratón en un botón, normalmente no).

:focus-visible {
  outline: 3px solid #e76f51;
  outline-offset: 3px;
}

Con esta regla tienes un foco claro, de un color de tu marca, que aparece cuando navegas con el teclado. En los campos de texto el navegador lo muestra siempre, también con el ratón, porque ahí indica dónde vas a escribir.

Si de verdad quieres cambiar el estilo del foco, sustitúyelo, no lo elimines:

.boton:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
  box-shadow: 0 0 0 5px #ffd166;
}

¿Por qué outline y no border? Porque outline no ocupa espacio en el modelo de caja: el elemento no cambia de tamaño ni empuja a sus vecinos al recibir el foco. Además, en el modo de alto contraste de Windows, outline se respeta y box-shadow desaparece (lo verás más abajo).

Tienes más sobre :focus, :focus-visible y :focus-within en pseudoclases a fondo.

Consejo: prueba tu web solo con el teclado al menos una vez: Tab para avanzar, Shift + Tab para retroceder. Si en algún momento no sabes dónde está el foco, tienes un problema.

Contraste de color

El texto debe destacar lo suficiente sobre su fondo. Las pautas internacionales de accesibilidad (WCAG) miden el contraste como una relación entre 1:1 (mismo color) y 21:1 (negro sobre blanco), y piden:

ElementoContraste mínimo (nivel AA)
Texto normal4,5:1
Texto grande (24 px, o 18,66 px en negrita)3:1
Iconos, bordes de campos, anillo de foco3:1

El gris claro sobre blanco que tanto se ve en diseños “minimalistas” (#aaa sobre #fff) tiene un contraste de 2,3:1: no pasa. #595959 sobre blanco ya llega a 7:1.

Para comprobarlo, en DevTools selecciona el texto, pulsa el cuadrado de color en el panel Styles y mira la línea Contrast ratio. Si no cumple, el propio selector te propone un tono que sí.

Fíjate también en el texto sobre imágenes: un titular blanco sobre una foto puede leerse bien en una parte y mal en otra. Pon debajo una capa oscura semitransparente o un degradado.

Tamaño de texto en rem

Muchas personas aumentan el tamaño de letra por defecto en la configuración del navegador (de 16 px a 20 o 24 px). Si escribes los tamaños en píxeles, ignoras esa preferencia:

/* Mal: el texto mide siempre 14 px */
body { font-size: 14px; }

/* Bien: 1rem es el tamaño que el usuario ha elegido */
body { font-size: 1rem; }
h1   { font-size: 2rem; }
.nota { font-size: 0.875rem; }

rem es relativo al tamaño de letra del elemento raíz, que por defecto sigue la configuración del usuario. Tienes las unidades explicadas en unidades.

Otras reglas para un texto legible:

  • Interlineado de al menos 1,5 en los párrafos (line-height: 1.5).
  • Líneas no muy largas: unos 60-75 caracteres (max-width: 65ch).
  • No justificar el texto (text-align: justify): crea huecos irregulares entre palabras que dificultan la lectura, sobre todo a personas con dislexia.
  • No escribir en mayúsculas frases largas; si quieres mayúsculas, usa text-transform: uppercase y deja el texto normal en el HTML (algunos lectores de pantalla deletrean las palabras escritas en mayúsculas).

Objetivos táctiles grandes

Un enlace diminuto es difícil de pulsar con el dedo, y más para personas con temblor o con poca destreza. Las pautas piden que los objetivos táctiles (touch targets) midan al menos 24 × 24 px (nivel AA), y lo recomendable son 44 × 44 px.

.menu a {
  display: inline-block;
  padding: 12px 16px;
  min-height: 44px;
}

.boton-icono {
  width: 48px;
  height: 48px;
}

Fíjate en que no hace falta que el icono sea grande; basta con que lo sea la zona pulsable, gracias al padding. Deja también espacio entre botones contiguos para no pulsar el que no querías.

Respetar a quien no quiere movimiento: prefers-reduced-motion

Las animaciones grandes (paralaje, elementos que vuelan, zoom al hacer scroll) pueden provocar mareos y náuseas a personas con trastornos vestibulares. Por eso los sistemas operativos tienen una opción de “reducir movimiento”, y CSS la detecta:

.tarjeta {
  transition: transform 0.3s;
}
.tarjeta:hover {
  transform: translateY(-6px);
}

@media (prefers-reduced-motion: reduce) {
  .tarjeta {
    transition: none;
  }
  .tarjeta:hover {
    transform: none;
  }
}

“Reducir” no significa “quitar todo”: un cambio de color o un fundido suave suelen estar bien. Lo que hay que eliminar o acortar son los desplazamientos, giros y zooms grandes. Una animación que transmite información (un indicador de “cargando”) puede seguir, más lenta.

La otra forma de plantearlo es al revés: escribir las animaciones solo para quien no ha pedido nada.

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
  .portada h1 { animation: aparecer 0.8s ease-out; }
}

Lo verás aplicado en transiciones, animaciones y scroll.

Más contraste: prefers-contrast y forced-colors

prefers-contrast

Algunas personas piden más contraste en la configuración de su sistema. Puedes reforzar bordes y textos suaves para ellas:

@media (prefers-contrast: more) {
  :root {
    --texto-suave: #1f2328;   /* el gris suave pasa a ser casi negro */
    --borde: #000;
  }
  .boton { border: 2px solid currentColor; }
}

Si organizas los colores con variables, basta con redefinir unas cuantas.

forced-colors: modo de alto contraste

Windows tiene un modo de contraste alto en el que el sistema sustituye todos tus colores por una paleta muy reducida (por ejemplo, texto amarillo sobre negro). El navegador elimina fondos, sombras (box-shadow) y degradados. Puedes detectarlo con forced-colors: active.

Lo importante es que tu web siga siendo comprensible sin tus colores:

  • Si un botón solo se distingue por su color de fondo, en este modo desaparece. Dale un borde, aunque sea transparente: border: 2px solid transparent. En modo forzado el borde se pinta con el color del sistema y el botón vuelve a verse.
  • Si tu foco es solo un box-shadow, desaparece. Usa outline, aunque sea outline: 2px solid transparent.
.boton {
  background: #1f5fc9;
  color: white;
  border: 2px solid transparent;   /* invisible normalmente, visible en alto contraste */
}

@media (forced-colors: active) {
  /* Un icono SVG pintado con un color fijo: que use el color del texto del sistema */
  .icono { fill: CanvasText; }
}

CanvasText es un color del sistema: el color que el usuario ha elegido para el texto. También existen Canvas (fondo), LinkText (enlaces) o ButtonText (texto de botones). Dentro de forced-colors, usa estos nombres en lugar de colores concretos.

Para probarlo sin Windows, en DevTools abre el panel Rendering y en Emulate CSS media feature forced-colors elige active.

Ocultar a la vista, pero no al lector de pantalla

Un botón que solo tiene un icono (un corazón, una lupa) no dice nada a quien usa un lector de pantalla. Necesita un texto que no se vea pero que sí se lea. Ni display: none ni visibility: hidden sirven: ocultan el texto también al lector.

La solución es una clase de utilidad que todos los proyectos tienen, con nombres como .visually-hidden o .sr-only (screen reader only):

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
<button class="boton-icono" type="button">
  <svg aria-hidden="true">...</svg>
  <span class="visually-hidden">Buscar</span>
</button>

El texto existe, mide un píxel y está recortado: no ocupa sitio ni se ve, pero el lector de pantalla anuncia “Buscar, botón”. Verás versiones antiguas con clip: rect(0 0 0 0); clip está obsoleto y hoy se usa clip-path.

Un uso relacionado es el enlace para saltar al contenido: el primer elemento de la página, oculto hasta que recibe el foco con el teclado, que permite saltarse el menú:

.saltar {
  position: absolute;
  left: 8px;
  top: -100px;
}
.saltar:focus {
  top: 8px;
}

No transmitas información solo con color

Alrededor del 8 % de los hombres tiene algún tipo de daltonismo: muchos confunden el rojo y el verde. Si un formulario marca los campos con error solo con un borde rojo, esa persona no sabe cuál falla.

Acompaña siempre el color de otra pista: un texto, un icono, un grosor, un subrayado.

/* Mal: el error solo se nota por el color */
.campo.error { border-color: #b42318; }

/* Bien: color + grosor + mensaje de texto */
.campo.error {
  border: 2px solid #b42318;
  background-image: url("icono-error.svg");
  background-repeat: no-repeat;
  background-position: right 8px center;
}
<p class="mensaje-error">El correo debe contener una @.</p>

Lo mismo con los enlaces dentro de un párrafo: si solo se distinguen por el color, quítales el subrayado únicamente cuando el contraste con el texto de alrededor sea suficiente. Lo más seguro es mantenerlo (text-decoration: underline). Y en los gráficos, además de colores distintos, usa etiquetas o patrones.

Orden visual y orden del DOM

Con Flexbox y Grid puedes cambiar el orden visual de los elementos (order, flex-direction: row-reverse, colocar ítems de Grid en cualquier celda). Pero el teclado y el lector de pantalla siguen el orden del HTML, no el visual.

/* El botón "Comprar" aparece primero en pantalla, pero en el HTML va último */
.acciones { display: flex; }
.acciones .comprar { order: -1; }

Quien navega con Tab verá que el foco salta de forma incoherente: primero los botones de la derecha y luego el de la izquierda. La regla es sencilla: el orden del HTML debe ser el orden lógico. Usa order y similares solo para ajustes visuales que no cambien el sentido de lectura, y si quieres otro orden, cámbialo en el HTML.

Zoom al 200 %

Las pautas piden que la web funcione al ampliarla al 200 % sin perder contenido ni funciones, y que a 320 px de ancho no haya scroll horizontal. Un diseño responsive bien hecho ya cumple casi todo, porque al ampliar el zoom el navegador se comporta como si la pantalla fuera más estrecha.

Para comprobarlo, pulsa Ctrl y + varias veces hasta llegar al 200 % y revisa:

  • Que no aparezca scroll horizontal (ojo con los anchos fijos en píxeles).
  • Que ningún texto quede cortado dentro de una caja con height fija y overflow: hidden. Usa min-height en lugar de height.
  • Que las cabeceras y barras fijas no ocupen media pantalla. Al 200 % una cabecera sticky de 100 px puede tapar casi todo; plantéate que deje de ser fija en pantallas bajas.
  • Que los menús sigan pudiendo abrirse y usarse.

Errores frecuentes

  • outline: none sin alternativa. Deja a los usuarios de teclado sin saber dónde están. Usa :focus-visible con un estilo propio.
  • Texto gris claro sobre blanco. Comprueba siempre el contraste en DevTools.
  • Tamaños de letra en px. Ignoran la configuración del usuario. Usa rem.
  • Botones e iconos diminutos. Como mínimo 24 × 24 px de zona pulsable; mejor 44 × 44.
  • Ocultar con display: none el texto de un botón de icono. El lector de pantalla tampoco lo lee. Usa .visually-hidden.
  • Errores marcados solo en rojo. Añade un texto o un icono.
  • Cambiar el orden con order y romper la navegación con teclado.
  • Animaciones grandes sin prefers-reduced-motion.
  • Alturas fijas que cortan el texto al hacer zoom.

Resumen

TécnicaPara quéEjemplo
:focus-visibleFoco visible al usar el tecladooutline: 3px solid; outline-offset: 3px;
Contraste 4,5:1Texto legibleComprobar en DevTools
remRespetar el tamaño de letra elegidofont-size: 1rem;
Objetivos de 44 pxPulsar fácil con el dedomin-height: 44px; padding: 12px;
prefers-reduced-motionMenos movimiento a quien lo pide@media (prefers-reduced-motion: reduce)
prefers-contrastMás contraste a quien lo pide@media (prefers-contrast: more)
forced-colorsModo de alto contraste de Windowsborder: 2px solid transparent;
.visually-hiddenTexto solo para lectores de pantallaclip-path: inset(50%);
Color + otra pistaInformación para daltónicosIcono o texto junto al rojo
Orden del HTMLNavegación lógica con tecladoEvitar order que cambie el sentido
Zoom 200 %Ampliar sin perder contenidomin-height en vez de height

Con estas bases tu CSS ya es accesible. Ahora toca conocer las novedades del lenguaje que más se usan hoy: CSS moderno.

Pruébalo tú

Cambia el código y pulsa Ejecutar (o Ctrl + Enter).

accesibilidad-css.html
Resultado

Pon a prueba lo que has aprendido

[CSS] Quitas el contorno del foco con outline: none en los botones. ¿Cuál es la alternativa correcta?

[CSS] ¿Qué contraste mínimo (nivel AA) se pide para texto normal?

[CSS] Tienes un botón de icono con texto "Buscar" para lectores de pantalla. ¿Cómo lo ocultas visualmente sin quitárselo al lector?

[CSS] ¿Para qué sirve @media (prefers-reduced-motion: reduce)?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.