Core · Mostrar contenido
Tabla
Compara registros con varios atributos, en filas y columnas que se leen bien a 320 px.
Ejemplo
Una tabla con tres servicios, su estado, CPU y región.
| Service | Status | CPU | Region |
|---|---|---|---|
| api-gateway | Running | 12% | eu-west-1 |
| data-pipeline | Degraded | 78% | us-east-1 |
| mailer | Stopped | 0% | eu-west-1 |
Make the window narrow: below 480 px of container width each row becomes a card.
Problemas conocidos
La ordenación en el navegador (data-luz-sort) solo se ha comprobado en el árbol de accesibilidad de Chromium; aún no se ha probado con NVDA, JAWS, VoiceOver, TalkBack ni Orca. La columna ordenada se indica con aria-sort en su cabecera y cada ordenación se anuncia en un mensaje de estado; al recorrer las cabeceras con Tab se anuncia el nombre de la columna, y el estado de ordenación se lee en la celda de cabecera y en el mensaje de estado.
- Solo se ordenan las filas cargadas en la página; la ordenación no se conserva al recargar y la barra de direcciones no cambia. No se admiten tablas con varias filas de cabecera ni cabeceras con
colspan; al convertir una cabecera ordenable en botón se descarta cualquier contenido que no sea su texto o un enlace. - Ordenar unos miles de filas las mueve todas en una sola tarea: ordénalas en el servidor. El orden del texto depende de la configuración regional del navegador para el idioma de la tabla.
Código
Copiar el marcado
Una table sin clase no necesita ninguna: llena su contenedor.
<table>
<caption>Invoices</caption>
<thead>
<tr><th scope="col">Number</th><th scope="col">Date</th><th scope="col">Amount (€)</th></tr>
</thead>
<tbody>
<tr><td>INV-0042</td><td>1 Oct 2026</td><td>120.00</td></tr>
</tbody>
</table>
Las clases añaden lo que una tabla sin clase no puede hacer: .luz-table-wrap hace que una tabla ancha se desplace, y .luz-table con data-label apila sus filas en tarjetas. Con las clases:
<div class="luz-table-wrap" role="region" aria-label="Invoices" tabindex="0">
<table class="luz-table">
<caption>Invoices</caption>
<thead>
<tr><th scope="col">Number</th><th scope="col">Date</th><th scope="col">Amount (€)</th></tr>
</thead>
<tbody>
<tr><td data-label="Number">INV-0042</td><td data-label="Date">1 Oct 2026</td><td data-label="Amount (€)">120.00</td></tr>
</tbody>
</table>
</div>
Variantes y opciones
| Clase o atributo | Efecto |
|---|---|
.luz-table | Ancho completo, fila de cabecera sobre la superficie intensa, una línea entre filas y resaltado de la fila bajo el puntero |
.luz-table-wrap | Desplaza la tabla horizontalmente cuando es más ancha que su contenedor |
.luz-table | Mantiene visible la primera columna mientras la tabla se desplaza horizontalmente |
.luz-table | Menos espacio encima y debajo de cada fila (unos 33 px para una línea de texto, no 49 px) y menos espacio entre columnas; el tamaño del texto no cambia |
.luz-table en un <th> y su <td> | Alinea una columna de números al final de la celda, con cifras del mismo ancho |
data-luz-sort en un <th scope="col"> | La columna se puede ordenar en el navegador: su texto se convierte en un botón |
data-sort-value en un <td> | El criterio de ordenación de la celda cuando no coincide con su texto: un número con formato ( 1,204), una fecha mostrada como «13 Oct» o un precio con su moneda |
aria-sort en el <th> ordenado | ascending o descending, solo en una cabecera: un triángulo indica la dirección |
data-label en cada <td> | El nombre de la columna que se muestra antes del valor cuando las filas pasan a ser tarjetas |
Los números y los datos densos tienen cada uno su propia clase. Aplica la clase numérica a la cabecera y a todas las celdas de la columna.
| Route | Parcels | Weight (kg) |
|---|---|---|
| Sevilla to Madrid | 1,204 | 8,430.5 |
| Cádiz to Málaga | 312 | 1,912.0 |
| Huelva to Faro | 58 | 406.3 |
Ordenación en el navegador
Marca cada cabecera ordenable con data-luz-sort. El script convierte su texto en un botón; sin él, la tabla sigue funcionando.
| Route | Parcels | Weight (kg) | Notes |
|---|---|---|---|
| Cádiz to Málaga | 312 | 1,912.0 | Daily |
| Huelva to Faro | 58 | 406.3 | Monday and Thursday |
| Sevilla to Madrid | 1,204 | 8,430.5 | Daily |
<table class="luz-table" id="routes">
<caption>Parcels per route</caption>
<thead>
<tr>
<!-- With a link: it sorts on the server without JavaScript -->
<th scope="col" data-luz-sort><a href="?sort=route#routes">Route</a></th>
<!-- Plain text: the script makes it a button -->
<th scope="col" class="luz-table__numeric" data-luz-sort>Parcels</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Route">Sevilla to Madrid</td>
<td class="luz-table__numeric" data-label="Parcels" data-sort-value="1204">1,204</td>
</tr>
</tbody>
</table>
- La ordenación se basa en el texto de la celda, salvo que tenga
data-sort-value. Dos números simples (12,0.5,-3) se ordenan como números; todo lo demás, como texto, según el idioma de la tabla, conRoom 2antes queRoom 10. - Un número con separador de miles, unidad o moneda necesita
data-sort-valuecon el número sin formato (1204para1,204); de lo contrario, se ordena como texto y «1,204» queda antes de «999». Las fechas deben ser ISO (2026-10-13), sea cual sea el formato mostrado. - Los valores iguales conservan su orden. La primera pulsación ordena de forma ascendente, la siguiente de forma descendente, y así sucesivamente; para recuperar el orden inicial, recarga la página.
- La cabecera solo indica la columna. No pongas la dirección en su texto ni en el pie: el triángulo,
aria-sorty la línea de estado la indican y se actualizan con la ordenación.
Cómo funciona
Una <table> nativa con <caption>, <thead> y <th scope>, con estilos de components/table.css. Sin JavaScript.
- El contenedor es un contenedor de consulta: por debajo de 480 px de ancho propio, cada fila se convierte en una tarjeta de parejas de etiqueta y valor, y se reduce el relleno de las celdas. Se adapta al ancho del contenedor, no al de la ventana, así que una tabla estrecha en una página ancha también se apila.
- Las etiquetas proceden de
data-label; las celdas que no lo tienen muestran solo el valor. - Las celdas que contienen más de un elemento necesitan un
<div>que los envuelva para mantener unido el valor. .luz-table__numericestablecetext-align: endyfont-variant-numeric: tabular-nums, para alinear las cifras de una columna sea cual sea el idioma. En el diseño de tarjetas apiladas, el valor ya queda al final de la fila..luz-table--compactsolo cambia el relleno de las celdas: el tamaño del texto se mantiene, así que la tabla es más densa, pero igual de legible.- Ordenación en el servidor, para resultados paginados o filtrados y para todas las tablas sin JavaScript: cada cabecera es un enlace a la misma página, ordenada por esa columna, y solo el
<th scope="col">de la columna ordenada llevaaria-sort="ascending"o"descending". Indica también la dirección en el texto visible del enlace, por ejemplo, «Importe (€), ascendente»; haz que el texto del enlace empiece con las palabras de la propia cabecera; termina suhrefcon elidde la tabla (?sort=amount&dir=desc#invoices) para que el siguiente Tab llegue a la tabla después de recargar la página; e indica la ordenación en el pie («Facturas, ordenadas por importe, ascendente»). - Ordenación en el navegador, para una tabla que contiene todas sus filas: añade
data-luz-sorta las cabeceras.luz.jscargatable-sort.jscuando hace falta, solo en páginas con una cabecera así, y el texto de cada cabecera (o el contenido de su enlace) pasa a un<button type="button">. Al pulsarlo, ordena todos los<tbody>en su sitio, escribearia-sortsolo en esa cabecera e indica «Ordenado por Parcelas, ascendente» en un<output>que se anuncia con cortesía y no se ve. El foco permanece en el botón. Una tabla ordenada en el navegador no lleva la dirección en el pie ni las cabeceras, porque dejaría de ser correcta tras la primera pulsación. - El triángulo que aparece tras la cabecera ordenada se dibuja con CSS a partir de
aria-sort(es una forma, no texto), así que también aparece en las cabeceras ordenadas en el servidor. Hacia arriba indica orden ascendente; hacia abajo, descendente. - Por debajo de 480 px, una fila de cabecera con un enlace o un botón permanece visible: sus cabeceras se muestran en una fila sobre las tarjetas y se ajustan para que se puedan ver y usar los controles. Una fila de cabecera con texto simple se oculta, como antes, porque cada tarjeta lleva sus etiquetas.
- Las tarjetas cambian la propiedad
displayde los elementos de la tabla, lo que puede ocultarla a algunos lectores de pantalla. La hoja de estilos recomiendarole="table","row"y"cell"en las tablas que se apilan.
Sin JavaScript
No cambia nada: el desplazamiento, la columna fija y las tarjetas son todo CSS. Una cabecera con enlace se ordena en el servidor; una cabecera de texto simple sigue siendo una cabecera.
Las filas nunca se ocultan y, si no se carga el script, la tabla permanece completa.
Cargar la ordenación por separado
El módulo no importa nada ni tiene efecto hasta que lo llamas. Pásale la función t de las cadenas de core para escribir la línea de estado en el idioma de la tabla; sin ella, la tabla se sigue ordenando, pero la línea de estado queda vacía.
import { initTableSort } from "@luz-ds/core/table-sort";
import { t } from "@luz-ds/core/i18n";
initTableSort(document, t);
Teclado y ARIA
El contenedor recibe el foco (tabindex="0") para que se pueda desplazar con las teclas de flecha.
- Asigna al contenedor
role="region"y unaria-labelpara que ese punto de parada del foco tenga un nombre. - Usa
scope="col"en las cabeceras de columna yscope="row"en la celda de cabecera que identifica su fila. - Una cabecera ordenable es un botón: Tab llega a ella, Enter o Espacio ordena y el foco permanece en ella mientras se mueven las filas. Los enlaces que sustituye el script conservan el foco que tenían.
aria-sortse aplica al único<th scope="col">ordenado. Cada ordenación se anuncia mediante la línea de estado (<output>, rolestatus), en el idioma de la tabla; las cadenas sonsortedAscysortedDescde las cadenas de core (consulta los módulos de JavaScript;data-luz-label-sorted-ascydata-luz-label-sorted-desclas sobrescriben).- El nombre de la columna es solo el texto de la cabecera: no añadas un carácter de flecha ni «ascendente» al marcado.
Contraste y foco están en la pestaña Diseño.
Diseño
Cuándo usarlo
Usa una tabla para comparar registros con los mismos atributos: servicios y su estado, facturas e importes.
Cuándo no usarlo
No uses una tabla para maquetar.
- Para un atributo por elemento, usa una lista.
- Para unos pocos elementos con la misma importancia, usa tarjetas.
- Para una cuadrícula de datos (filtros, selección de filas, edición de celdas o miles de filas), usa una biblioteca diseñada para eso. La tabla de LUZ muestra y ordena filas; no las gestiona. En resultados paginados, ordena en el servidor.
- Para mostrar una tendencia temporal, usa una biblioteca de gráficos y conserva la tabla como alternativa textual. Una barra por fila es un medidor en la celda de su cifra.
Qué hacer y qué no
Cómo escribir el contenido
El pie identifica la tabla; los encabezados de columna son sustantivos breves.
- Pon las unidades en el encabezado, no en cada celda: «Importe (€)».
- Coloca primero la columna que identifica la fila.
- Alinea los números al final con
.luz-table__numeric, tanto en la cabecera como en todas las celdas de la columna, y usa el mismo número de decimales. El texto queda al principio. - En las tablas que se ordenan en el navegador, la cabecera indica solo el nombre de la columna y su unidad, «Peso (kg)»: no añadas flechas ni la dirección en la cabecera ni en el pie.
Contraste y foco
Las cabeceras usan la superficie intensa; las celdas, la de la página y, bajo el puntero, la superficie de resalte.
| Parte | Color sobre el fondo | Claro | Oscuro | Alto contraste | Necesita |
|---|---|---|---|---|---|
Celda de cabecera | --luz-text-primary on --luz-surface-strong | 13.5:1 | 10.9:1 | 18.0:1 | 4.5:1 |
Celda | --luz-text-primary on --luz-surface-page | 15.8:1 | 15.1:1 | 21.0:1 | 4.5:1 |
Celda, fila bajo el puntero | --luz-text-primary on --luz-surface-hover | 14.4:1 | 12.6:1 | 20.1:1 | 4.5:1 |
Pie y etiquetas de las tarjetas | --luz-text-secondary on --luz-surface-page | 6.2:1 | 7.5:1 | 19.3:1 | 4.5:1 |
Anillo de foco de una cabecera ordenable | --luz-focus-ring on --luz-surface-strong | 4.8:1 | 5.6:1 | 18.0:1 | 3:1 |
Anillo de foco | --luz-focus-ring on --luz-surface-page | 5.6:1 | 7.7:1 | 21.0:1 | 3:1 |
Medido a partir de los valores de los tokens de cada modo, redondeado hacia abajo. «Necesita» indica el mínimo AA de WCAG 2.2: 4,5:1 para texto y 3:1 para anillos de foco, bordes e iconos.
- Foco: el contenedor desplazable lo recibe, con un contorno de
--luz-focus-ringde 2 px a 2 px por fuera. También lo recibe el botón de una cabecera ordenable. - No solo el color: las cabeceras ordenables están subrayadas y la ordenada tiene un triángulo del color del texto que apunta hacia arriba o abajo. La dirección también aparece en
aria-sorty en la línea de estado. - Área interactiva: los botones de cabecera miden al menos 24 px de alto, sea cual sea el relleno de la celda.
- Bordes: las líneas de las filas (
--luz-divider) son decorativas; la cabecera tiene su propia superficie y las celdas, sus encabezados.
Tokens que usa
Los 17 tokens que lee components/table.css, por familia. Cada familia enlaza a su página, con los valores de todos los modos. En Figma son variables con los mismos nombres: consulta Tokens en Figma.
- Color
--luz-divider,--luz-focus-ring,--luz-surface-hover,--luz-surface-page,--luz-surface-strong,--luz-text-primary,--luz-text-secondary- Spacing and sizing
--luz-space-2xs,--luz-space-md,--luz-space-sm,--luz-space-xs- Typography
--luz-font-sans,--luz-text-md,--luz-text-sm- Radius
--luz-radius-md- Motion
--luz-duration-base,--luz-duration-fast
Teclado y ARIA están en la pestaña Código.