Data table
The <velin-data-table> Web Component enhances a plain <table> with sorting, filtering and pagination. The markup stays semantic, so the table is fully readable before the component upgrades and with JavaScript disabled.
Basic
Wrap an ordinary table. Add sortable to make every column sortable, or opt in per column with data-sort.
| Name | Commits | Joined | Role |
|---|---|---|---|
| Ada Lovelace | 412 | 2023-01-15 | Engineering |
| Grace Hopper | 1280 | 2022-06-02 | Engineering |
| Alan Turing | 96 | 2024-03-01 | Research |
| Katherine Johnson | 734 | 2021-11-20 | Research |
| Radia Perlman | 58 | 2025-07-09 | Networking |
<velin-data-table filter-input="#dtFilter" page-size="3">
<table class="velin-table">
<caption>Team members</caption>
<thead>
<tr>
<th data-sort="text">Name</th>
<th data-sort="number">Commits</th>
<th data-sort="date">Joined</th>
<th data-sort="none">Role</th>
</tr>
</thead>
<tbody>…</tbody>
</table>
</velin-data-table>
The table needs an accessible name. Provide a <caption>, an aria-label, or a label attribute on the host — the component warns in the console if all three are missing.
Sorting
Each sortable header is replaced with a real <button>, so sorting works with the keyboard and is announced as a button by screen readers. The current state lives on the <th> as aria-sort, and only one column is sorted at a time. Activating the same column again reverses the direction.
data-sort | Comparison |
|---|---|
text | Case-insensitive string comparison (default with sortable) |
number | Parsed as a number, so 9 sorts before 30 |
date | Parsed with Date.parse |
none | Column is not sortable and gets no button |
To sort by something other than the visible text — a raw timestamp behind a formatted date, for example — put the comparable value in data-sort-value on the cell.
<td data-sort-value="1700000000">15 Nov 2023</td>
Filtering
Point filter-input at any existing input; typing filters rows on a debounced substring match. Non-matching rows get the hidden attribute, which removes them from the layout and the accessibility tree rather than just hiding them visually.
By default the whole row is searched. Mark one or more cells with data-filter to restrict matching to those columns.
<td data-filter>Ada Lovelace</td>
<td>412</td> <!-- ignored while filtering -->
When nothing matches, a single row with empty-text is rendered across all columns.
Pagination
Set page-size to show a slice at a time. A labelled <nav> with Previous and Next buttons and a “Page X of Y” status is appended; the edge buttons are disabled rather than removed so focus is never lost. Pagination is omitted entirely while everything fits on one page, and filtering resets to page 1.
Attributes
| Attribute | Default | Notes |
|---|---|---|
sortable | — | Makes every column sortable as text unless data-sort says otherwise |
filter-input | — | CSS selector of the input driving the filter |
page-size | — | Rows per page; omit for no pagination |
empty-text | No matching rows | Shown when the filter matches nothing |
label | — | Applied as aria-label when the table has no caption |
pagination-label | Table pagination | Accessible name of the pagination nav |
previous-text / next-text | Previous / Next | Pagination button labels |
JavaScript API
const table = document.querySelector('velin-data-table');
table.sort(1, 'descending'); // column index, optional direction
table.filter('ada'); // same matching as the bound input
table.goToPage(2); // clamped to the available pages
table.rows; // every data row
table.matchingRows; // rows passing the current filter
table.visibleRows; // rows on the current page
table.page; // current page number
table.pageCount; // total pages for the current filter
Events
velin-data-table-sort—detail.index,detail.direction,detail.columnvelin-data-table-filter—detail.query,detail.countvelin-data-table-page—detail.page,detail.pageCount
Accessibility
- Progressive enhancement over a semantic
<table>: header cells, scope and caption are the author's markup, untouched. - Sort triggers are native buttons (WCAG 2.1.1 Keyboard) and the sorted column exposes
aria-sort(4.1.2 Name, Role, Value). - Sort direction is conveyed by
aria-sortand an announcement, not by the arrow glyph alone (1.4.1 Use of Color). - Sort, filter and page changes are announced politely through
<velin-announcer>(4.1.3 Status Messages). - Hidden rows use the
hiddenattribute, so they leave the accessibility tree instead of lingering as invisible content. - No animation, so there is nothing to suppress for
prefers-reduced-motion. - Sort buttons fill the header cell and keep a 2.75rem minimum block size (2.5.8 Target Size).
Contract status in core/a11y/component-contracts.json: pass. See also Tables for the static styling options.