Table
A data table inside a scrollable, focusable region with a caption for its name.
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><x-wsu::table caption="Available courses, Winter 2027">
<x-slot:head>
<tr>
<th scope="col">Course</th>
<th scope="col">Title</th>
<th scope="col">Credits</th>
<th scope="col">Status</th>
</tr>
</x-slot:head>
@foreach ($courses as $course)
<tr>
<th scope="row">{{ $course->number }}</th>
<td>{{ $course->title }}</td>
<td>{{ $course->credits }}</td>
<td><x-wsu::badge :variant="$course->badge">{{ $course->status }}</x-wsu::badge></td>
</tr>
@endforeach
</x-wsu::table><DataTable
caption="Available courses, Winter 2027"
:columns="[
{ key: 'number', label: 'Course', sortable: true },
{ key: 'title', label: 'Title' },
{ key: 'credits', label: 'Credits', sortable: true },
{ key: 'status', label: 'Status' },
]"
:rows="courses"
>
<template #cell-status="{ value }">
<Badge :variant="value === 'Open' ? 'success' : 'danger'">{{ value }}</Badge>
</template>
</DataTable>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
| Attribute | Default | What it does |
|---|---|---|
caption | required | The table's accessible name. |
zebra | true | Alternating row shading. |
id | slug of the caption | Used to build the caption id. |
Vue
| Prop | Type and default | What it does |
|---|---|---|
columns | Array, [] | [{ key, label, sortable, sort }]. sort is an optional comparator. |
rows | Array, [] | Row objects. |
caption | String, '' | Required in practice. |
zebra | Boolean, true | Alternating row shading. |
rowHeader | Boolean, true | Render the first column as a row header. |
manual | Boolean, false | Sort on the server. Emits sort and renders rows as given. |
sort | Object, null | Initial 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.
| Template | Elements | Accessibility attributes | Uses |
|---|---|---|---|
| internal apps/examples/static-html/index.html | divtable | aria-labelledbyroletabindex | 2 |
| public apps/examples/static-html/public-app.html | divtable | aria-labelledbyroletabindex | 2 |