Saltar al contenido

CSS, de a poco

Curso de CSS

CSS accesible

Lección 29 de 37 · 4 min

La accesibilidad empieza en el HTML, pero el CSS puede romperla o sostenerla: un foco que no se ve, una animación que marea o un botón demasiado chico son problemas de CSS. Acá están las herramientas para hacerlo bien.

Un foco que se vea

Quien navega con el teclado necesita ver en todo momento dónde está. :focus coincide con el elemento que tiene el foco, venga del teclado o de un clic. :focus-visible coincide solo cuando el navegador cree que conviene mostrarlo: al llegar con Tab sí, al hacer clic en un botón no. En un campo de texto coincide siempre, porque ahí el foco indica dónde vas a escribir.

El anillo se dibuja con outline, que no ocupa lugar ni mueve nada, y outline-offset lo separa del borde (más detalles en Bordes y sombras). Con currentColor toma el color del texto y contrasta en modo claro y oscuro. Ojo con los botones invertidos (fondo oscuro y texto claro, o al revés): ahí el anillo toma el color del texto y puede perderse contra la página.

Elegí :focus, hacé clic en una página y después repetí con :focus-visible. Probá también con Tab: el texto de arriba dice qué coincide en el botón enfocado.

el anillo se dibuja con
sin foco en la paginación

Archivo de notas

13–24 de 140

paginas.css
.paginas button:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
paginas.html
<button class="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current">
Siguiente
</button>

:focus-visible es Baseline ampliamente disponible desde hace tiempo. Nunca saques el contorno con outline: none sin poner otro igual de visible: WCAG pide un indicador de foco visible.

Preferencias del sistema

Las media queries leen ajustes que cada persona eligió en su sistema. Respetarlos no es un detalle: hay gente que se marea con el movimiento y gente que no distingue un gris claro de un blanco.

  1. Menos movimientoprefers-reduced-motion: reduceNo hace falta sacar toda animación: dejá los fundidos y quitá desplazamientos, zooms y parallax.
  2. Más contrasteprefers-contrast: moreBordes sólidos en lugar de translúcidos, texto secundario al mismo color que el principal.
  3. Tema oscuroprefers-color-scheme: darkSumale color-scheme: light dark en :root para que los controles nativos y las barras de scroll también cambien.
  4. Colores forzadosforced-colors: activeEl sistema reemplaza tus colores por su paleta, como el modo de alto contraste de Windows.
Un aviso de préstamo renovado con tres preferencias. Sin preferencia entra deslizando; con más contraste el borde y el texto son sólidos; con colores forzados todo toma los colores del sistema y el ícono queda con un borde en lugar del fondo.

En modo de colores forzados el navegador pisa colores, fondos y sombras, y desaparece todo lo que dependía de ellos: un ícono hecho con un fondo de color, un borde dibujado con box-shadow. Para esos casos están los colores del sistema (Canvas, CanvasText, LinkText, ButtonText, Highlight…), que siempre valen lo que el sistema eligió.

prestamo.css
@media (forced-colors: active) {
.estado { border: 1px solid CanvasText; }
/* este sí conserva su color */
.muestra-de-color { forced-color-adjust: none; }
}

Las cuatro media queries son Baseline ampliamente disponible. forced-color-adjust no: está en Chrome 89 y Firefox 113, pero no en Safari. Usalo solo para lo que de verdad necesita su color, como una muestra en un selector de colores.

Estilos según el estado ARIA

Un botón que abre un panel tiene que anunciar si está abierto, y para eso existe aria-expanded. Si el CSS lee ese mismo atributo, el dibujo y lo que oye el lector de pantalla salen de un solo lugar y no se pueden contradecir. Con una clase aparte (.abierta) hay dos estados que mantener, y alcanza con olvidarse uno.

Abrí y cerrá preguntas con las dos opciones. En la versión con clase, el script cambia solo la clase: se ve abierta, pero el lector sigue anunciando contraída.

aria-expandedcoinciden

Hasta cinco a la vez, por 15 días. Podés renovarlos dos veces.

No hay multa: el préstamo se suspende un día por cada día de atraso.

Sí, en cualquiera de las ocho sedes.

.abiertadesfasadas: 1

Hasta cinco a la vez, por 15 días. Podés renovarlos dos veces.

No hay multa: el préstamo se suspende un día por cada día de atraso.

Sí, en cualquiera de las ocho sedes.

Dos copias de las mismas preguntas frecuentes. La de la izquierda dibuja el estado leyendo aria-expanded; la de la derecha lee una clase, y su script nunca actualiza aria-expanded.

Lo mismo sirve para otros estados que ya tienen atributo:

estados.css
nav [aria-current="page"] { font-weight: 500; text-decoration: underline; }
[aria-invalid="true"] { border-color: oklch(0.58 0.2 27); }
[aria-pressed="true"] { background: oklch(0.92 0 0); }
[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

Texto oculto y tamaño de los objetivos

Un botón que es solo un ícono necesita un nombre. Ponelo como texto dentro del botón y ocultalo con el patrón visually hidden: queda en el árbol de accesibilidad pero no ocupa lugar en pantalla. display: none o visibility: hidden no sirven, porque también lo esconden del lector.

El tamaño también importa. WCAG 2.2 pide que cada objetivo mida al menos 24 × 24 px (criterio 2.5.8, nivel AA), salvo que esté lo bastante separado de sus vecinos; el nivel AAA pide 44 × 44. Si el diseño quiere un ícono más chico, agrandá solo el área que recibe el clic con un ::before invisible.

width: 28px; height: 28pxárea: 28 × 28 px · WCAG 2.5.8: cumple

Guardado

Compras del sábado

Yerba, pan de campo, dos kilos de naranjas y el repuesto del filtro de agua.

barra.css
.barra button {
position: relative;
width: 28px;
height: 28px;
}
.barra button::before {
content: "";
position: absolute;
inset: min(0px, (100% - 24px) / 2);
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
barra.html
<button class="relative size-7 before:absolute before:inset-[min(0px,(100%-24px)/2)] before:content-['']">
<svg aria-hidden="true">…</svg>
<span class="sr-only">Negrita</span>
</button>

El inset con min() hace la cuenta solo: si el botón mide 20px, sale −2px por lado y el área llega a 24; si ya mide 24 o más, queda en 0 y no la achica.

En Tailwind

Tailwind trae variantes para todo esto. Los colores del sistema van entre corchetes, y el patrón visually hidden ya viene como sr-only (con not-sr-only para mostrarlo de nuevo, por ejemplo un enlace para saltar al contenido).

CSSTailwind
:focus-visiblefocus-visible:
outline: 2px solid currentColoroutline-2 outline-current
prefers-reduced-motionmotion-reduce: · motion-safe:
prefers-contrast: morecontrast-more:
forced-colors: activeforced-colors:
border-color: CanvasTextborder-[CanvasText]
[aria-expanded="true"]aria-expanded: · group-aria-expanded:
[aria-current="page"]aria-[current=page]:
visually hiddensr-only
barra.html
<button aria-pressed="true"
class="relative size-5 aria-pressed:bg-neutral-200
before:absolute before:content-['']
before:inset-[min(0px,(100%-24px)/2)]
focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-current">
<svg aria-hidden="true">…</svg>
<span class="sr-only">Negrita</span>
</button>
<div class="motion-safe:animate-[entrar_250ms_ease-out]
contrast-more:border-current">…</div>