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ía | Artículo | Precios | Stock | Estado | ||
| Coste unitario | Precio de venta | En stock | Nivel de reposición | |||
| Panes | Baguette | $0.65 | $3.25 | 42 | 20 | OK |
| Pan de masa madre | $0.90 | $4.75 | 15 | 18 | Bajo | |
| Pan de centeno | $0.85 | $4.50 | 27 | 15 | OK | |
| Bollería | Croissant de mantequilla | $0.55 | $2.95 | 60 | 25 | OK |
| Danesa de cereza | $0.70 | $3.50 | 8 | 12 | Bajo | |
| Tartas | Porción de tarta de zanahoria | $1.10 | $4.95 | 18 | 10 | OK |
| Porción de tarta de queso | $1.25 | $5.25 | 22 | 10 | OK | |
| Totales | — | — | 192 unidades | 110 unidades | 2 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.
| Categoría | Artículo | Precios | Stock | Estado | ||
|---|---|---|---|---|---|---|
| Coste unitario | Precio de venta | En stock | Nivel de reposición | |||
| Panes | Baguette | $0.65 | $3.25 | 42 | 20 | OK |
| Pan de masa madre | $0.90 | $4.75 | 15 | 18 | Bajo | |
| Pan de centeno | $0.85 | $4.50 | 27 | 15 | OK | |
| Bollería | Croissant de mantequilla | $0.55 | $2.95 | 60 | 25 | OK |
| Danesa de cereza | $0.70 | $3.50 | 8 | 12 | Bajo | |
| Tartas | Porción de tarta de zanahoria | $1.10 | $4.95 | 18 | 10 | OK |
| Porción de tarta de queso | $1.25 | $5.25 | 22 | 10 | OK | |
| Totales | — | — | 192 unidades | 110 unidades | 2 artículos bajos | |