Responsieve tabellen

Grote gegevenstabellen worden vaak onleesbaar op kleine schermen omdat kolommen overlopen of te klein worden om te lezen. In deze handleiding wordt uitgelegd hoe je plakkerige positionering gebruikt om koppen zichtbaar te houden tijdens het scrollen en hoe je de tabel omzet in een gestapelde indeling die geschikt is voor mobiele apparaten als de ruimte beperkt is.

De kern van een responsieve tabel is dat de relatie tussen gegevenscellen en de bijbehorende koppen behouden blijft.

  1. Semantische basis: Gebruik standaard <table>-elementen met <thead>-, <tbody>- en <th>-elementen.
  2. Vaste context: Pas position: sticky toe op zowel kolomkoppen als rijkopteksten. Zo raken gebruikers nooit de context van de gegevens kwijt, hoe ver ze ook in een bepaalde richting scrollen.
  3. Aanpasbare transformaties: Gebruik @container-query's in plaats van @media-query's. Hierdoor kan de tabel zich aanpassen op basis van de eigen breedte (bijvoorbeeld als deze in een zijbalk of een smalle dashboardwidget wordt geplaatst) in plaats van de hele viewport.
  4. Labelinjectie: In de gestapelde indeling is de <thead> verborgen en worden toegankelijke koppen in elke cel geïnjecteerd met ::before pseudoelementen en CSS-variabelen.

Stapsgewijze implementatie

1. Opmaak en vormgeving

Structureer je tabel met standaard semantische koppen. Gebruik een .table-wrapper om overloop te verwerken en een container voor query's te bieden. Voeg een tabindex="0"-kenmerk toe aan de container, zodat mensen die alleen een toetsenbord gebruiken, de tabel consistent kunnen scrollen.

Geef de wrapper een role="region"-kenmerk om het doel ervan aan te geven. Geef het een toegankelijke naam met aria-labelledby (verwijzend naar het kenmerk id van een bestaand element dat de tabel een bijschrift/naam geeft) of een kenmerk 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. Vastgezette koppen instellen

Zet scrollen aan en maak kopteksten vast. Gebruik logische eigenschappen en expliciete z-indexwaarden om de stapelvolgorde te beheren.

.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. Responsieve gestapelde indeling

Als er weinig ruimte is, verberg je de oorspronkelijke koprij en zet je de tabel om in kaarten. Injecteer labels met pseudoelementen. Bied alleen alt-attributen voor content die geen tekst is, anders kunnen zowel de tekstcontent als de toegankelijke naam worden voorgelezen door schermlezers.

@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) ": ";
  }
}