wandres.dev
SVG Y ACCESIBILIDAD · Gráficos que se pueden leer

El patrón correcto para iconos

Los cinco casos de icono en una interfaz, dónde va el nombre accesible en cada uno, el tamaño mínimo del objetivo táctil, y el contraste que exige un icono que porta información.

⏱ 16 min

Un icono mal marcado hace que un lector de pantalla lea basura, y los iconos son los SVG más numerosos de cualquier interfaz. La buena noticia es que solo hay cinco casos y cada uno tiene una respuesta única. La mala es que la respuesta depende de dónde está el icono, no de cómo es, así que no se puede resolver dentro del componente de icono: hay que resolverlo en cada uno de los componentes que lo usan.

🎯 Al terminar esta lección sabrás
  • Marcar correctamente los cinco casos de icono de una interfaz.
  • Situar el nombre accesible en el elemento adecuado en cada caso.
  • Aplicar el tamaño mínimo de objetivo y el contraste que exigen las pautas.
  • Diseñar un componente de icono cuya API haga difícil equivocarse.

Caso 1: icono junto a texto

El más frecuente y el más simple.

<button type="button" class="boton">
  <svg aria-hidden="true" width="16" height="16"><use href="#i-guardar" /></svg>
  Guardar
</button>

El icono se oculta. El nombre del botón sale de su contenido de texto. Nada más que hacer.

Lo mismo para un enlace, un elemento de lista, una fila de tabla o un encabezado con icono. Si hay texto al lado que dice lo mismo, el icono es decorativo.

Caso 2: icono solo, en un control

Un botón sin texto. El nombre va en el control, no en el icono.

<button type="button" class="boton-icono" aria-label="Cerrar el diálogo">
  <svg aria-hidden="true" width="20" height="20"><use href="#i-cruz" /></svg>
</button>

Por qué en el control y no en el SVG: porque el nombre describe la acción, no el dibujo. «Cerrar el diálogo» es lo que hace el botón; «cruz» es lo que se ve. Y porque el cálculo del nombre accesible de un button con aria-label tiene el soporte más sólido que existe.

Una alternativa igualmente válida y a veces mejor:

<button type="button" class="boton-icono">
  <svg aria-hidden="true" width="20" height="20"><use href="#i-cruz" /></svg>
  <span class="solo-lector">Cerrar el diálogo</span>
</button>

Es más verboso y tiene dos ventajas: el texto se traduce como contenido, y si el CSS falla, el usuario ve la palabra en lugar de un botón vacío.

Caso 3: icono solo, portando información

Un icono de estado en una tabla, sin control alrededor. No hay dónde poner el nombre salvo en el propio SVG o en un texto oculto.

<td>
  <svg aria-hidden="true" width="16" height="16" class="ico-ok"><use href="#i-check" /></svg>
  <span class="solo-lector">Pagado</span>
</td>

Aquí hay una segunda obligación que la mayoría pasa por alto: el color no puede ser el único portador del significado. Si el estado se distingue solo por un tic verde frente a una cruz roja, quien no percibe ese contraste de color pierde la información. La forma tiene que ser distinta, no solo el color, y en este caso lo es. Si tu diseño usa el mismo símbolo en dos colores, tienes un problema que ningún atributo ARIA arregla.

Caso 4: icono decorativo puro

Un adorno, una flecha de acompañamiento, una ilustración de fondo.

<svg aria-hidden="true" width="120" height="80"><use href="#ilustracion-vacio" /></svg>

Nada más. Y si es un fondo, mejor todavía en CSS con background-image o mask, donde ni siquiera está en el DOM.

Caso 5: icono que es un enlace

Un logotipo que enlaza a la portada, o un icono de red social.

<a href="/" class="logo">
  <svg aria-hidden="true" width="120" height="32"><use href="#logotipo" /></svg>
  <span class="solo-lector">Inicio</span>
</a>

El nombre describe el destino, no el dibujo. Y aquí es especialmente importante que el texto sea informativo: una lista de enlaces de redes sociales donde todos se llaman «icono» es inservible en la navegación por enlaces de un lector de pantalla, que presenta la lista de nombres fuera de contexto.

La tabla

Caso aria-hidden en el SVG Dónde va el nombre
Icono junto a texto Ya está en el texto
Icono solo en un control En el control
Icono solo con información Texto oculto al lado
Icono decorativo En ningún sitio
Icono como enlace En el enlace

La columna del medio tiene el mismo valor en las cinco filas. Esa es la conclusión operativa del artículo: el componente de icono debe llevar aria-hidden="true" por defecto, siempre, y el nombre se resuelve fuera.

Tamaño y contraste

Dos requisitos que no son de marcado y que fallan igual de a menudo.

El objetivo táctil. Un icono de 16 píxeles dentro de un botón sin relleno es un objetivo de 16 por 16. Las pautas de accesibilidad establecen un mínimo de 24 por 24 píxeles CSS para los objetivos de puntero, con excepciones cuando hay separación suficiente entre objetivos. El criterio más exigente de nivel AAA pide 44 por 44. En la práctica: el botón, no el icono, debe medir al menos 24 píxeles, y 44 en una interfaz táctil.

.boton-icono {
  display: inline-grid;
  place-items: center;
  inline-size: 2.75rem;   /* 44px */
  block-size: 2.75rem;
}

El contraste. Un icono que porta información es un objeto gráfico esencial para la comprensión, y las pautas exigen una relación de contraste de al menos 3 a 1 con lo que tenga alrededor. Un icono gris claro sobre fondo blanco falla, aunque se vea «bien» en la pantalla del diseñador.

Y un matiz que se olvida: el contraste se mide sobre la parte más fina del icono. Un icono de trazo de 1,5 unidades con antialiasing tiene, en la práctica, un contraste efectivo menor que el que calcula la herramienta sobre el color plano. En iconos de línea conviene subir el grosor antes que el contraste.

El componente de icono no puede resolver la accesibilidad solo, y el que lo intenta la empeora

La tentación al construir un sistema de diseño es hacer que el componente de icono acepte una propiedad de etiqueta y decida solo:

<Icono nombre="cruz" etiqueta="Cerrar" />

Parece razonable y produce dos fallos garantizados. El primero: quien usa el componente dentro de un botón con texto pondrá una etiqueta por costumbre, y tendrás el anuncio duplicado del artículo anterior. El segundo: quien lo usa en un botón sin texto pondrá la etiqueta en el icono en lugar de en el botón, y el nombre acabará dentro de un elemento que quizá ni se exponga.

La API que funciona hace lo contrario: el icono es siempre decorativo y no admite etiqueta.

function Icono({ nombre, tam = 20 }) {
  return (
    <svg aria-hidden="true" width={tam} height={tam}>
      <use href={`#i-${nombre}`} />
    </svg>
  );
}

Y el nombre lo pone el componente de nivel superior, que es el que sabe qué hace el control:

<BotonIcono etiqueta="Cerrar el diálogo" icono="cruz" />

Ese BotonIcono es el que pone el aria-label en el button, el que fuerza el tamaño mínimo de 44 píxeles, y el que hace obligatoria la propiedad de etiqueta con una comprobación de tipos. Si alguien intenta usarlo sin etiqueta, el proyecto no compila.

La lección general, y vale para más cosas que los iconos: la accesibilidad no se resuelve en la pieza más pequeña, se resuelve en la pieza que conoce el propósito. Un icono no sabe si es un botón, un estado o un adorno. El componente que lo envuelve, sí.

⚔️ Reto práctico

Audita el componente de icono de tu proyecto contra los cinco casos. Comprueba si permite poner etiqueta al icono, si el botón de icono obliga a una etiqueta, y si el tamaño mínimo del objetivo es de 44 píxeles. Después busca en el código todos los sitios donde un icono lleva etiqueta estando junto a texto: cada uno es un anuncio duplicado en producción.