Tablas responsivas

Las tablas de datos grandes suelen volverse ilegibles en pantallas pequeñas, ya que las columnas se desbordan o se reducen más allá de lo legible. En esta guía, se muestra cómo usar el posicionamiento fijo para mantener visibles los encabezados durante el desplazamiento y cómo transformar la tabla en un diseño "apilado" optimizado para dispositivos móviles cuando el espacio es limitado.

El núcleo de una tabla responsiva es mantener la relación entre las celdas de datos y sus encabezados.

  1. Base semántica: Usa elementos <table> estándar con elementos <thead>, <tbody> y <th>.
  2. Contexto fijo: Aplica position: sticky tanto a los encabezados de columna como a los de fila. Esto garantiza que, sin importar cuánto se desplace un usuario en cualquier dirección, nunca pierda el contexto de lo que representan los datos.
  3. Transformaciones adaptables: Usa consultas @container en lugar de consultas @media. Esto permite que la tabla se adapte en función de su propio ancho (p.ej., cuando se coloca en una barra lateral o en un widget de panel estrecho) en lugar de todo el viewport.
  4. Inyección de etiquetas: En el diseño apilado, se oculta el elemento <thead> y se insertan encabezados accesibles en cada celda con pseudoelementos ::before y variables de CSS.

Pasos de implementación

1. Marcado y diseño

Estructura tu tabla con encabezados semánticos estándar. Usa un .table-wrapper para controlar el desbordamiento y proporcionar un contenedor para las consultas. Agrega un atributo tabindex="0" al contenedor para asegurarte de que las personas que solo usan un teclado puedan desplazarse por la tabla de forma coherente.

Asigna un atributo role="region" al wrapper para comunicar su propósito. Proporciónale un nombre de accesibilidad con aria-labelledby (que haga referencia al atributo id de un elemento existente que subtitule o nombre la tabla) o un atributo aria-label.

<div tabindex="0" role="region" aria-labelledby="employee-table-caption" class="table-wrapper">
  <table>
    <caption id="employee-table-caption">Current employee directory</caption>
    <thead>
      <tr>
        <th>Employee</th>
        <th>Role</th>
        <th>Status</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <!-- Row header remains sticky horizontally on desktop, 
             and becomes a sticky card header on mobile -->
        <th>Alex Rivera</th>
        <td>Engineer</td>
        <td>Active</td>
      </tr>
    </tbody>
  </table>
</div>

2. Configura encabezados fijos

Habilita el desplazamiento y haz que los encabezados sean fijos. Usa propiedades lógicas y valores explícitos de z-index para administrar el orden de apilamiento.

.table-wrapper {
  overflow: auto;
  max-inline-size: 100%;
  max-block-size: min(500px, 80vh);
  container-type: inline-size;
}

table {
  border-collapse: separate;
  border-spacing: 0;
}

/* Sticky column headers (desktop) */
thead {
  position: sticky;
  inset-block-start: 0;
  z-index: 1; /* Above sticky row headers */
}

/* Sticky row headers (desktop) */
tbody th {
  position: sticky;
  inset-inline-start: 0;
  background: white; /* Required to cover background during scroll */
}

3. Diseño apilado responsivo

Cuando el espacio sea limitado, oculta la fila de encabezado original y transforma la tabla en tarjetas. Inserta etiquetas con seudoelementos. Solo proporciona texto alternativo para el contenido que no sea de texto; de lo contrario, es posible que los lectores de pantalla anuncien tanto el contenido de texto como el nombre de accesibilidad.

@container (width < 600px) {
  table, thead, tbody, tr, th, td {
    display: block;
  }

  thead {
    /* MANDATORY: Hide column header from layout and screen readers. */
    display: none
  }

  tr {
    margin-block-end: 1.5rem;
    border: 1px solid #ccc;
  }

  /* Sticky row headers at the top of each block/card */
  tbody th {
    position: sticky;
    inset-block-start: 0;
    z-index: 2;
    background: #eee;
  }

  /* Define column labels as CSS variables (often set via JS) */
  table {
    --label-1: "Employee";
    --label-2: "Role";
    --label-3: "Status";
  }

  /* Inject labels */
  td:nth-child(2)::before {
    /* MANDATORY: If using non-text content, provide the accessible name (after the /) which ensures screen readers hear the text alternative. */
    content: var(--label-2) ": ";
  }
  /* MANDATORY: Map the label for each column */
  td:nth-child(3)::before {
    content: var(--label-3) ": ";
  }
}