Saltar al contenido principal

Core · Mostrar contenido

Tabla

Compara registros con varios atributos, en filas y columnas que se leen bien a 320 px.

Esta traducción la ha escrito una IA y todavía no la ha revisado ninguna persona. Leer esta página en inglés

Ejemplo

Una tabla con tres servicios, su estado, CPU y región.

Service status
ServiceStatusCPURegion
api-gatewayRunning12%eu-west-1
data-pipelineDegraded78%us-east-1
mailerStopped0%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.

HTML
<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:

HTML
<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 atributoEfecto
.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--sticky-col
Mantiene visible la primera columna mientras la tabla se desplaza horizontalmente
.luz-table--compact
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__numeric 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.

Shipments this week
RouteParcelsWeight (kg)
Sevilla to Madrid1,2048,430.5
Cádiz to Málaga3121,912.0
Huelva to Faro58406.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.

Parcels per route
RouteParcelsWeight (kg)Notes
Cádiz to Málaga3121,912.0Daily
Huelva to Faro58406.3Monday and Thursday
Sevilla to Madrid1,2048,430.5Daily
HTML
<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, con Room 2 antes que Room 10.
  • Un número con separador de miles, unidad o moneda necesita data-sort-value con el número sin formato (1204 para 1,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-sort y 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__numeric establece text-align: end y font-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--compact solo 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 lleva aria-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 su href con el id de 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-sort a las cabeceras. luz.js carga table-sort.js cuando 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, escribe aria-sort solo 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 display de los elementos de la tabla, lo que puede ocultarla a algunos lectores de pantalla. La hoja de estilos recomienda role="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.

JS
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 un aria-label para que ese punto de parada del foco tenga un nombre.
  • Usa scope="col" en las cabeceras de columna y scope="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-sort se aplica al único <th scope="col"> ordenado. Cada ordenación se anuncia mediante la línea de estado (<output>, role status), en el idioma de la tabla; las cadenas son sortedAsc y sortedDesc de las cadenas de core (consulta los módulos de JavaScript; data-luz-label-sorted-asc y data-luz-label-sorted-desc las 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

Sí La unidad en la cabecera, los mismos decimales y la columna alineada al final, incluida la cabecera.
No Una cabecera imprecisa, la unidad en cada celda, decimales mezclados y sin alineación: cuesta comparar la columna.

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.

ParteColor sobre el fondoClaroOscuroAlto contrasteNecesita
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-ring de 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-sort y 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.

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.