Skip to content

Table

A data table inside a scrollable, focusable region with a caption for its name.

Preview

Code

The three render the same class names, the same ARIA and the same data attributes. That is checked when this site builds: a class in any of these samples that neither the markup contract nor the shipped stylesheet defines fails the build.

<div class="wsu-table-wrap" tabindex="0" role="region" aria-labelledby="courses-caption">
  <table class="wsu-table wsu-table--zebra">
    <caption id="courses-caption">Available courses, Winter 2027</caption>
    <thead>
      <tr>
        <th scope="col"><button class="wsu-table__sort" type="button">Course
          <svg class="wsu-icon" aria-hidden="true"><use href="/assets/wayne-ui/icons.svg#wsu-arrow-up-down"/></svg></button></th>
        <th scope="col">Title</th>
        <th scope="col">Credits</th>
        <th scope="col">Status</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">CSC 2110</th>
        <td>Computer Science I</td>
        <td>4</td>
        <td><span class="wsu-badge wsu-badge--success">Open</span></td>
      </tr>
    </tbody>
  </table>
</div>

Why it works this way

  • The wrapper carries tabindex="0" and role="region". A table wider than its column scrolls sideways, and a scrollable area that cannot be reached from the keyboard is content a keyboard user cannot read.
  • The caption is the table's accessible name and aria-labelledby points at it rather than duplicating the text. A table with no caption is a wall.
  • The page body itself must never scroll sideways at 320px. A table scrolling inside its own box is exactly what 1.4.10 permits.
  • Sortable headers are buttons inside the th, and the th carries aria-sort. wayne-ui-theme.js binds every wsu-table__sort button on every path, static, raw PHP, Blade and Vue alike, so a hand-authored table sorts with no script of its own. It clears aria-sort from every other column before it sets the one just pressed, so exactly one column is ever marked sorted, and it rewrites the button's own accessible name to say what pressing it will do next, not only that it is a button.
  • The DataTable component sorts itself instead, client-side by default and by emitting sort when manual is set, and marks its table data-wsu-sort="component" so the shared runtime leaves it alone. Binding both would sort the same click twice: the component would update aria-sort, the shared runtime would read that fresh value and invert it again, and every press after the first would run backwards.

Accessibility

  • role="region" with a name, and tabindex="0" so the scroller is reachable (2.1.1, 1.4.10).
  • scope on every header cell, and a th for the row key (1.3.1).
  • aria-sort on the sorted column, and only ever one column at a time (4.1.2).

Success criteria in brackets refer to WCAG 2.2. Every one of them is checked by axe-core in both themes at three viewport widths before this page can be published. See Accessibility for what the gates are.

Props

Blade

AttributeDefaultWhat it does
captionrequiredThe table's accessible name.
zebratrueAlternating row shading.
idslug of the captionUsed to build the caption id.

Vue

PropType and defaultWhat it does
columnsArray, [][{ key, label, sortable, sort }]. sort is an optional comparator.
rowsArray, []Row objects.
captionString, ''Required in practice.
zebraBoolean, trueAlternating row shading.
rowHeaderBoolean, trueRender the first column as a row header.
manualBoolean, falseSort on the server. Emits sort and renders rows as given.
sortObject, nullInitial sort, e.g. { key: 'course', direction: 'ascending' }.

In the markup contract

contract/markup-contract.json is extracted from the two static templates and every package tests against it. This is what it records for .wsu-table.

TemplateElementsAccessibility attributesUses
internal apps/examples/static-html/index.htmldivtablearia-labelledbyroletabindex2
public apps/examples/static-html/public-app.htmldivtablearia-labelledbyroletabindex2