Data table (sort and filter)
Sorting and filtering for a table that already exists in the HTML. The server sends a normal <table class="iv-table">; on init the sortable headers become buttons with aria-sort, the filter field appears and a live status announces how many rows are visible. Nothing is paginated or virtualised: the rows you serve are the rows that get sorted, so keep the table at a size a page can hold.
<div class="iv-datatable" data-iv-component="datatable">
<div class="iv-datatable__filter">
<label class="iv-label" for="dt-orders-q">Filter rows</label>
<input class="iv-input" id="dt-orders-q" type="search" autocomplete="off" placeholder="Customer, city or status" data-iv-datatable-filter>
</div>
<div class="iv-table-wrap" role="region" aria-labelledby="dt-orders-caption" tabindex="0">
<table class="iv-table">
<caption id="dt-orders-caption">Sample orders. The names, cities and totals below are made up for this demo; nothing here comes from a real account.</caption>
<thead>
<tr>
<th scope="col" data-iv-sort>Customer</th>
<th scope="col" data-iv-sort="text">City</th>
<th scope="col" data-iv-sort="date">Placed</th>
<th scope="col" class="iv-u-text-end" data-iv-sort="number">Total (EUR)</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Ada Márquez</th>
<td>Córdoba</td>
<td><time datetime="2026-03-04">4 Mar 2026</time></td>
<td class="iv-u-text-end">1,240.00</td>
<td><span class="iv-badge iv-badge--success">Shipped</span></td>
</tr>
<tr>
<th scope="row">Bruno Oliveira</th>
<td>Porto</td>
<td><time datetime="2026-01-22">22 Jan 2026</time></td>
<td class="iv-u-text-end">318.50</td>
<td><span class="iv-badge">Draft</span></td>
</tr>
<tr>
<th scope="row">Chiara Rossi</th>
<td>Milano</td>
<td><time datetime="2026-05-11">11 May 2026</time></td>
<td class="iv-u-text-end">2,905.75</td>
<td><span class="iv-badge iv-badge--success">Shipped</span></td>
</tr>
<tr>
<th scope="row">Diego Fernández</th>
<td>Alicante</td>
<td><time datetime="2026-02-08">8 Feb 2026</time></td>
<td class="iv-u-text-end">76.20</td>
<td><span class="iv-badge iv-badge--warning">On hold</span></td>
</tr>
<tr>
<th scope="row">Elin Karlsson</th>
<td>Göteborg</td>
<td><time datetime="2026-04-27">27 Apr 2026</time></td>
<td class="iv-u-text-end">540.00</td>
<td><span class="iv-badge iv-badge--success">Shipped</span></td>
</tr>
<tr>
<th scope="row">Farid Haddad</th>
<td>Marseille</td>
<td><time datetime="2026-06-02">2 Jun 2026</time></td>
<td class="iv-u-text-end">1,088.90</td>
<td><span class="iv-badge iv-badge--danger">Cancelled</span></td>
</tr>
<tr>
<th scope="row">Greta Hoffmann</th>
<td>Leipzig</td>
<td><time datetime="2026-01-09">9 Jan 2026</time></td>
<td class="iv-u-text-end">9,410.00</td>
<td><span class="iv-badge iv-badge--success">Shipped</span></td>
</tr>
<tr>
<th scope="row">Hugo Álvarez</th>
<td>Ourense</td>
<td><time datetime="2026-03-30">30 Mar 2026</time></td>
<td class="iv-u-text-end">212.40</td>
<td><span class="iv-badge iv-badge--warning">On hold</span></td>
</tr>
<tr>
<th scope="row">Ingrid Dahl</th>
<td>Bergen</td>
<td><time datetime="2026-05-19">19 May 2026</time></td>
<td class="iv-u-text-end">47.00</td>
<td><span class="iv-badge">Draft</span></td>
</tr>
</tbody>
</table>
</div>
</div>// data-iv-component="datatable"
{
datatableFilter: true,
sort: true,
}Initial sort and typed values
data-iv-sorted="asc" or "desc" on a header sorts the table as soon as it is initialised. A cell can carry data-iv-value when the displayed text is not the value to sort by, and date cells are read from <time datetime>. Cells without a valid value go last in both directions.
<div class="iv-datatable" data-iv-component="datatable" data-iv-status-text="Showing {visible} of {total} builds">
<div class="iv-table-wrap" role="region" aria-labelledby="dt-builds-caption" tabindex="0">
<table class="iv-table iv-table--striped">
<caption id="dt-builds-caption">Made-up build report, sorted by duration on load. The last row has no duration on purpose: rows without a value sort to the end in both directions.</caption>
<thead>
<tr>
<th scope="col" data-iv-sort>Job</th>
<th scope="col" class="iv-u-text-end" data-iv-sort="number" data-iv-sorted="desc">Duration</th>
<th scope="col" data-iv-sort="text">Result</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">bundle-css</th>
<td class="iv-u-text-end" data-iv-value="42.5">42.5 s</td>
<td>Passed</td>
</tr>
<tr>
<th scope="row">bundle-js</th>
<td class="iv-u-text-end" data-iv-value="18">18 s</td>
<td>Passed</td>
</tr>
<tr>
<th scope="row">unit-tests</th>
<td class="iv-u-text-end" data-iv-value="126.75">2 min 6.75 s</td>
<td>Passed</td>
</tr>
<tr>
<th scope="row">browser-tests</th>
<td class="iv-u-text-end" data-iv-value="394">6 min 34 s</td>
<td>Failed</td>
</tr>
<tr>
<th scope="row">size-report</th>
<td class="iv-u-text-end" data-iv-value="7.25">7.25 s</td>
<td>Passed</td>
</tr>
<tr>
<th scope="row">pack-smoke</th>
<td class="iv-u-text-end"></td>
<td>Skipped</td>
</tr>
</tbody>
</table>
</div>
</div>// data-iv-component="datatable"
{
statusText: "Showing {visible} of {total} builds",
sort: true,
sorted: "desc",
value: 42.5,
}Column types
| Attribute | Comparison | Cell value |
|---|---|---|
data-iv-sort or data-iv-sort="text" | localeCompare, numeric-aware, accent and case insensitive | data-iv-value, else the cell text |
data-iv-sort="number" | numeric, after stripping units and separators | data-iv-value, else the cell text |
data-iv-sort="date" | chronological (Date.parse) | data-iv-value, else <time datetime>, else the cell text |
Options
| Option | Attribute | Default | Effect |
|---|---|---|---|
filterDelay | data-iv-filter-delay | 150 | Milliseconds waited after typing before the rows are filtered. |
emptyText | data-iv-empty-text | No rows match | Text of the row shown when the filter hides everything. |
statusText | data-iv-status-text | {visible} of {total} rows | Template of the live status; both placeholders are replaced. |
locale | data-iv-locale | document language | Locale used by text comparisons. |
Methods and events
sort(columnIndexOrTh, direction?), filter(query), clearFilter(), reset() (original order and no filter), destroy(); read sortColumn, sortDirection, rows (original order), visibleRows and query. Events on the root element: iv:sort (cancelable, detail.column, direction, previousColumn, previousDirection) then iv:sorted; iv:filter (cancelable, detail.query, previousQuery) then iv:filtered with visible and total.
Keyboard and assistive technology
Sort controls are real buttons, so Enter and Space activate them and their accessible name is the header text; the arrow is decorative CSS. Only the sorted column carries aria-sort. The filter is a native search field. Hidden rows use the hidden attribute, so screen readers do not read them, and the row count is announced through a role="status" element. Focus never moves when sorting or filtering.
Without JavaScript
The table is a plain table in the order the server sent it. The filter block is not displayed at all, so users never see a control that does nothing. Serve the rows in the most useful order.
Common mistakes
- Thousands of rows: this component keeps every row in the DOM. Paginate on the server or filter the data before rendering.
- Marking a header sortable when its cells are mixed content (badges, buttons): put the sort value in
data-iv-value. - Omitting the
<caption>and the labelled scroll region: the table needs a name, with or without JavaScript.