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

title, desc y aria-labelledby

Los dos elementos que SVG trae para el nombre y la descripción, por qué su soporte solo no basta, y el patrón que funciona de verdad en los lectores de pantalla actuales.

⏱ 16 min

SVG trae dos elementos pensados para el texto alternativo desde 1999, y la mitad de los desarrolladores que los usan no consiguen que un lector de pantalla los anuncie. El motivo no es que estén rotos, sino que su exposición depende de un mapeo de accesibilidad que los motores implementaron tarde y de forma desigual. El patrón que funciona hoy combina esos elementos con atributos ARIA, y hay una razón concreta para cada pieza.

🎯 Al terminar esta lección sabrás
  • Colocar title y desc en la posición donde se exponen.
  • Explicar por qué title produce un tooltip además del nombre accesible.
  • Escribir el patrón completo con aria-labelledby y saber qué añade.
  • Elegir entre aria-label y title según el contexto.

title y desc

title da el nombre accesible de su elemento padre. desc da la descripción. Ambos son elementos, no atributos, y ambos pueden aparecer en cualquier elemento contenedor: el svg raíz, un g, o una forma individual.

<svg viewBox="0 0 24 24" width="24" height="24">
  <title>Descargar el informe</title>
  <path d="M12 3v12m0 0 4-4m-4 4-4-4M4 19h16" fill="none"
        stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>

Dos reglas de colocación que hay que respetar:

title tiene que ser el primer hijo de su padre. La especificación lo exige, y aunque los motores actuales toleran que esté en otro sitio, hay lectores de pantalla que no. Ponerlo el primero no cuesta nada.

Solo el primer title de un elemento cuenta. Un segundo title hermano se ignora.

Hay un efecto secundario que sorprende: los navegadores muestran el contenido de title como tooltip nativo al pasar el ratón, igual que el atributo title de HTML. Eso puede ser deseable o no. Si no lo quieres (por ejemplo, porque ya tienes un tooltip propio y aparecen dos), la alternativa es usar aria-label en lugar de title.

Por qué el patrón necesita ARIA

En teoría, un svg con un title debería exponer ese texto como nombre accesible sin más. En la práctica, el mapeo de accesibilidad de SVG llegó tarde a los navegadores y el soporte de los lectores de pantalla ha sido irregular durante años. Todavía hoy hay combinaciones donde un title suelto no se anuncia.

El patrón robusto añade tres cosas:

<svg viewBox="0 0 100 100" width="100" height="100"
     role="img" aria-labelledby="t1 d1">
  <title id="t1">Distribución del gasto por categoría</title>
  <desc id="d1">Un gráfico circular con cuatro sectores. Vivienda 42 por ciento,
  alimentación 24, transporte 19 y ocio 15.</desc>
  <!-- las formas -->
</svg>

Qué aporta cada pieza:

role="img" declara que el SVG es una imagen. Sin él, el rol implícito es graphics-document, cuyo soporte en los lectores de pantalla es limitado, y algunos motores exponen el SVG con un rol genérico que no se anuncia como imagen. Con role="img", además, el subárbol se trata como opaco: la tecnología de asistencia no recorre las formas de dentro anunciando basura.

aria-labelledby conecta explícitamente el nombre con el title. Es la vía que mejor soporte tiene, por delante del mapeo implícito.

Los id son necesarios para esa conexión, y arrastran el problema de ámbito global del nivel 20: dos gráficos en la misma página con id="t1" colisionan, y el segundo apuntará al título del primero. Los identificadores de accesibilidad tienen que ser únicos por instancia, generados desde el código.

aria-label frente a title

Los dos dan el nombre accesible. Las diferencias prácticas:

title aria-label
Produce tooltip nativo No
Visible en el DOM como texto No
Se traduce automáticamente Sí, es contenido No siempre, es un atributo
Funciona sin ARIA Depende del motor Necesita un rol adecuado
Admite formato No, es texto plano No

El punto de la traducción es el que más pesa en un producto internacional. Los sistemas de traducción automática del navegador y las herramientas de localización tratan el contenido de un elemento como texto traducible; los atributos, no siempre. Un title se traduce; un aria-label puede quedarse en el idioma original.

Recomendación: title como opción por omisión, y aria-label cuando el tooltip nativo estorbe o cuando el SVG sea generado y no quieras añadir nodos.

Si usas los dos a la vez, aria-label gana: el orden de precedencia del cálculo del nombre accesible pone aria-labelledby primero, aria-label segundo, y el contenido (incluido title) después.

desc y cuándo se anuncia

desc produce la descripción accesible, que es un campo distinto del nombre. Los lectores de pantalla la tratan de forma diferente: el nombre se anuncia siempre, la descripción solo en ciertos modos o tras una pausa, y algunos usuarios la tienen desactivada.

Eso tiene una consecuencia de diseño importante: no pongas en desc información imprescindible. Si un dato es necesario para entender el gráfico, tiene que estar en un sitio que se anuncie siempre. La descripción es para el detalle adicional.

Para un gráfico de datos, ni el nombre ni la descripción bastan, y ese es el tema de una gráfica de datos accesible.

Un title dentro de un símbolo se clona en todas sus instancias, y eso rompe los identificadores

Poner el title dentro del symbol de un sprite parece la decisión limpia: el nombre viaja con el icono y no hay que repetirlo. Es un error, por dos razones que se refuerzan.

La primera es semántica. El mismo icono significa cosas distintas en sitios distintos. Una cruz es «cerrar» en un diálogo, «eliminar» en una fila de tabla y «error» en un mensaje de estado. Un nombre fijado en el símbolo obliga a todos los usos a compartir un nombre, y ese nombre será incorrecto en la mayoría.

La segunda es técnica y más sutil. Si el title lleva un id para referenciarlo con aria-labelledby, ese id se clona en cada árbol de sombra. Dentro de cada árbol el identificador es local y no colisiona, lo cual está bien. Pero el aria-labelledby que escribes en el use está fuera del árbol de sombra, y desde fuera ese identificador no existe. La referencia no resuelve y el elemento se queda sin nombre. El fallo es silencioso y no lo detecta ninguna herramienta automática, porque el atributo está presente y bien escrito.

El patrón correcto: el símbolo no lleva title, y el nombre lo pone el svg que envuelve al use, o el botón que lo contiene.

<svg role="img" aria-label="Cerrar el diálogo" width="20" height="20">
  <use href="#i-cruz" />
</svg>

Y en el caso de un icono dentro de un botón, ni siquiera eso: el nombre va en el botón y el SVG se oculta. Ese es el tema de la lección sobre iconos.

Un generador de identificadores

Como los identificadores tienen que ser únicos, conviene tener el generador resuelto:

let n = 0;
const idUnico = (prefijo) => `${prefijo}-${(++n).toString(36)}`;

function graficoAccesible(titulo, descripcion, contenido) {
  const t = idUnico('svg-t');
  const d = idUnico('svg-d');
  return `<svg role="img" aria-labelledby="${t} ${d}" viewBox="0 0 100 100">
    <title id="${t}">${titulo}</title>
    <desc id="${d}">${descripcion}</desc>
    ${contenido}
  </svg>`;
}

En un entorno con renderizado de servidor e hidratación, el contador propio produce identificadores distintos en servidor y en cliente. Usa el generador estable que ofrezca tu framework.

⚔️ Reto práctico

Coge tres SVG de tu proyecto con title y comprueba con un lector de pantalla real qué anuncia cada uno. Después añade role="img" y aria-labelledby y vuelve a comprobar. Anota en qué casos ha cambiado el resultado: eso te dirá cuánto puedes fiarte del mapeo implícito en tu combinación concreta de navegador y lector.