Cómo hacer accesibles las tablas complejas

Las tablas HTML tienen mala fama. A menudo se las considera malas para la accesibilidad, mala práctica y anticuadas.

Esta fama solo es justa cuando usamos las tablas para maquetación. La mayoría de los sitios web modernos no lo hacen, pero la mayoría de los correos electrónicos sí. Las tablas tienen valor semántico (significado) y comportamiento funcional. Las tablas funcionan bien para datos en filas y columnas. No funcionan bien para maquetación.

Puedes usar role="presentation" para eliminar el valor semántico y el comportamiento funcional de una tabla. Aun así, el orden del contenido en el DOM debe seguir teniendo sentido.

Veamos un ejemplo de una tabla con información de inventario de una pequeña panadería:

Inventario semanal por categoría
CategoríaArtículoPreciosStockEstado
Coste unitarioPrecio de ventaEn stockNivel de reposición
PanesBaguette$0.65$3.254220OK
Pan de masa madre$0.90$4.751518Bajo
Pan de centeno$0.85$4.502715OK
BolleríaCroissant de mantequilla$0.55$2.956025OK
Danesa de cereza$0.70$3.50812Bajo
TartasPorción de tarta de zanahoria$1.10$4.951810OK
Porción de tarta de queso$1.25$5.252210OK
Totales192 unidades110 unidades2 artículos bajos

No se ve mal, ¿verdad? El CSS hace que las partes sean fáciles de distinguir, y la maquetación tiene sentido.

Ese es el problema principal. Información importante, como las relaciones y la estructura, depende únicamente de los estilos visuales y la posición. Las personas que usan tecnología de asistencia no pueden acceder a esa información.

Al final de este artículo, encontrarás la misma tabla con todo lo que le falta a la primera versión.

Vamos paso a paso.

Celdas de encabezado

Este es el cambio más fácil de hacer. Todas las celdas de la primera tabla usan la etiqueta de celda de datos, <td>. Pero HTML también tiene una etiqueta de celda de encabezado, <th>, para las celdas que actúan como encabezados.

Usa el atributo scope para marcar una celda de encabezado como encabezado de columna (scope="col") o encabezado de fila (scope="row"). Esto te da encabezados en la parte superior de cada columna y un encabezado al inicio de cada fila.

Grupos de filas

Una tabla puede tener tanto grupos de columnas como grupos de filas. El atributo scope también acepta los valores colgroup y rowgroup para estos casos.

Hay 3 grupos de filas principales: la cabecera de la tabla (<thead>), el cuerpo de la tabla (<tbody>) y el pie de la tabla (<tfoot>). Son especialmente útiles para tablas grandes.

En nuestro ejemplo:

  • Los encabezados superiores van en la cabecera de la tabla.
  • Los datos de los productos van en el cuerpo de la tabla.
  • Los totales van en el pie de la tabla.

Esto facilita la navegación entre las partes principales.

Título

Esta parte importa. Un título (caption) describe el contenido de la tabla para que las personas puedan decidir si necesitan o quieren leer los datos.

Aquí tienes varias opciones. Puedes usar:

  • Un <caption> (o <figcaption> si la tabla está dentro de un <figure>)
  • Texto, como un párrafo, colocado antes de la tabla
  • Texto en otro elemento, vinculado con el atributo aria-describedby

Estos métodos funcionan igual de bien. Usa solo uno.

Una ventaja de <caption>: puedes anidar un elemento <details> dentro de él, con información adicional en un desplegable <summary>, o añadir texto que esté oculto visualmente pero que los lectores de pantalla puedan seguir leyendo. Usé este segundo enfoque en el ejemplo al final del artículo. Le da a todo el mundo una etiqueta corta y visible, y a los usuarios de lectores de pantalla un texto más largo con más contexto.

Nota: el atributo summary en el elemento <table> está obsoleto en el HTML Living Standard. No lo uses.

Encabezados multinivel

Esta parte marca la mayor diferencia para las personas que usan tecnología de asistencia, especialmente en tablas complejas.

En nuestro ejemplo, tanto las columnas como las filas tienen varios niveles de encabezado. Esto puede parecer complicado, pero la solución es sencilla.

Primero, añade un atributo id único a cada celda de encabezado. Luego añade un atributo headers a cada celda de datos. Enumera todas las celdas de encabezado con las que se relaciona, tanto de columna como de fila, en todos los niveles.

Por ejemplo, la celda con el precio de venta de la baguette incluye: headers="row-breads row-baguette col-pricing col-retail". Esto permite que los lectores de pantalla anuncien todos los encabezados relacionados con esa celda.

Conclusión

Las tablas accesibles no son difíciles de construir. El marcado puede volverse detallado, ya que estás vinculando muchas celdas con muchos encabezados. Pero es sencillo de implementar. Y convierte una experiencia lenta y frustrante —desplazarse por datos interminables sin contexto— en una fuente de información clara y utilizable.

Inventario semanal por categoría Enumera el coste unitario, el precio de venta, las existencias actuales y el nivel de reposición de cada artículo de la panadería, agrupados por categoría de producto, con los totales en la última fila.
CategoríaArtículoPreciosStockEstado
Coste unitarioPrecio de ventaEn stockNivel de reposición
PanesBaguette$0.65$3.254220OK
Pan de masa madre$0.90$4.751518Bajo
Pan de centeno$0.85$4.502715OK
BolleríaCroissant de mantequilla$0.55$2.956025OK
Danesa de cereza$0.70$3.50812Bajo
TartasPorción de tarta de zanahoria$1.10$4.951810OK
Porción de tarta de queso$1.25$5.252210OK
Totales192 unidades110 unidades2 artículos bajos