data-table
Progressive enhancement wrapper for HTML tables. Adds sorting, filtering, pagination, row expansion, and selection.
Overview
The <data-table> component wraps a standard HTML table to provide interactive features like column sorting, text filtering, pagination, expandable rows, row selection with bulk actions, responsive card layouts, and sticky headers/columns. All features are opt-in via data attributes and the table remains fully functional without JavaScript.
<data-table> <table data-filterable data-paginate="5"> <thead> <tr> <th data-sort="string">Name</th> <th data-sort="number">Age</th> </tr> </thead> <tbody> <tr> <td>Alice Johnson</td> <td>32</td> </tr> <tr> <td>Bob Smith</td> <td>28</td> </tr> </tbody> </table></data-table>
Basic Usage
Wrap any HTML table with <data-table> and add data attributes to enable features. The table structure remains semantic and accessible.
<data-table> <table> <thead> <tr> <th data-sort="string">Name</th> <th data-sort="string">Email</th> <th data-sort="string">Department</th> </tr> </thead> <tbody> <tr> <td>Alice Johnson</td> <td>alice.johnson@example.com</td> <td>Engineering</td> </tr> <!-- More rows... --> </tbody> </table></data-table>
Sorting
Add data-sort to any <th> element to make that column sortable. Click the column header to cycle through ascending, descending, and unsorted states.
Sort Types
| Value | Description |
|---|---|
string |
Alphabetical sorting (case-insensitive) |
number |
Numeric sorting (handles decimals and negatives) |
date |
Date sorting (parses common date formats) |
Custom Sort Values
Use data-sort-value on cells when the display value differs from the sort value (e.g., formatted currency or dates).
<!-- Display shows formatted value, sorts by raw value --><td data-sort-value="95000">$95,000</td><td data-sort-value="2019-03-15">Mar 15, 2019</td>
Filtering
Add data-filterable to the table to enable text filtering. A search input is automatically generated above the table. Rows that don't match the filter text are hidden.
<data-table> <table data-filterable> <thead> <tr> <th>Name</th> <th>Email</th> <th>Department</th> </tr> </thead> <tbody> <!-- Rows will be filtered as user types --> </tbody> </table></data-table>
Pagination
Add data-paginate="n" to the table to show only n rows per page. Pagination controls are automatically generated below the table.
<data-table> <table data-paginate="5"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Department</th> <th>Hire Date</th> </tr> </thead> <tbody> <!-- 12 rows, showing 5 per page --> </tbody> </table></data-table>
Row Expansion
Create expandable rows by adding data-expandable to a row and following it with a data-expand-content row. A toggle button is automatically added to control the expansion.
<data-table> <table> <thead> <tr> <th></th> <th>Name</th> <th>Department</th> <th>Status</th> </tr> </thead> <tbody> <tr data-expandable> <td><button data-action="toggle-expand">...</button></td> <td>Alice Johnson</td> <td>Engineering</td> <td>Active</td> </tr> <tr data-expand-content hidden> <td colspan="4"> <div>Additional details here...</div> </td> </tr> </tbody> </table></data-table>
Row Selection
Add checkboxes to rows for selection, with a "select all" checkbox in the header and a bulk actions bar that appears when rows are selected.
<data-table> <div data-bulk-actions hidden> <span><strong data-selected-count>0</strong> selected</span> <button>Export</button> <button>Archive</button> <button class="danger">Delete</button> </div> <table> <thead> <tr> <th><input type="checkbox" data-action="select-all"/></th> <th>Name</th> <th>Email</th> <th>Department</th> </tr> </thead> <tbody> <tr data-selectable> <td><input type="checkbox" data-action="select-row"/></td> <td>Alice Johnson</td> <td>alice.johnson@example.com</td> <td>Engineering</td> </tr> <!-- More selectable rows... --> </tbody> </table></data-table>
Responsive Card Mode
Add data-responsive="card" to transform the table into a card layout on narrow screens. Use data-label on cells to show column headers in card mode.
<data-table> <table data-responsive="card"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Department</th> <th>Status</th> </tr> </thead> <tbody> <tr> <td data-label="Name">Alice Johnson</td> <td data-label="Email">alice.johnson@example.com</td> <td data-label="Department">Engineering</td> <td data-label="Status">Active</td> </tr> </tbody> </table></data-table>
Sticky Headers and Columns
Use data-sticky to keep headers or columns visible while scrolling.
Sticky Options
| Value | Description |
|---|---|
header |
Keeps the table header row fixed at the top |
column |
Keeps the first column fixed on horizontal scroll |
both |
Both header and first column are sticky |
Multiple Sticky Columns
Use data-sticky-column="n" to make the first n columns sticky.
<!-- Sticky header only --><table data-sticky="header">...</table> <!-- Sticky first column --><table data-sticky="column">...</table> <!-- Both sticky --><table data-sticky="both">...</table> <!-- First 2 columns sticky --><table data-sticky="column" data-sticky-column="2">...</table>
Events
The component dispatches custom events for each interactive feature.
| Event | Detail | Description |
|---|---|---|
data-table:sort |
{ column: number, direction: "asc"|"desc"|null } |
Fired when a column is sorted. |
data-table:filter |
{ query: string, count: number } |
Fired when the filter input changes. |
data-table:page |
{ page: number } |
Fired when the page changes. |
data-table:expand |
{ row: HTMLTableRowElement, expanded: boolean } |
Fired when a row is expanded or collapsed. |
data-table:selection |
{ count: number, rows: HTMLTableRowElement[] } |
Fired when row selection changes. |
const table = document.querySelector('data-table'); // Sort eventstable.addEventListener('data-table:sort', (e) => { console.log(`Column ${e.detail.column} sorted ${e.detail.direction}`);}); // Filter eventstable.addEventListener('data-table:filter', (e) => { console.log(`Filter "${e.detail.query}" matches ${e.detail.count} rows`);}); // Page change eventstable.addEventListener('data-table:page', (e) => { console.log(`Page ${e.detail.page} of ${e.detail.page}`);}); // Row expansion eventstable.addEventListener('data-table:expand', (e) => { console.log(`Row ${e.detail.expanded ? 'expanded' : 'collapsed'}`);}); // Selection eventstable.addEventListener('data-table:selection', (e) => { console.log(`${e.detail.count} rows selected`);});
Public API
The component exposes methods for programmatic control.
| Method | Parameters | Returns | Description |
|---|---|---|---|
goToPage(n) |
n: number |
void |
Navigate to a specific page (1-indexed). |
setFilter(query) |
query: string |
void |
Programmatically set the filter text. |
refresh() |
- | void |
Re-apply sorting, filtering, and pagination. |
getSelectedRows() |
- | HTMLTableRowElement[] |
Get array of currently selected rows. |
const table = document.querySelector('data-table'); // Navigate to page 3table.goToPage(3); // Filter by departmenttable.setFilter('engineering'); // Refresh after external data changetable.refresh(); // Get selected rows for bulk operationconst selected = table.getSelectedRows();selected.forEach(row => { console.log(row.cells[1].textContent); // Log names});
Attributes Reference
Host Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
data-filterable | boolean | — | Enable text search filtering |
data-paginate | number | — | Enable pagination with N rows per page |
Required Structure
| Element | Required | Description |
|---|---|---|
<table> | yes | Standard HTML table — the component enhances it in place |
Child Attributes
| Attribute | On | Values | Description |
|---|---|---|---|
data-sort | th | "string", "number", "date" | Enable sorting on this column with the specified comparator |
data-sort-value | td | string | Custom sort value overriding cell text |
data-filter-value | td | string | Custom filter value overriding cell text |
data-expandable | tr | boolean | Mark row as expandable (followed by a data-expand-content row) |
data-expand-content | tr | boolean | Hidden row revealed when its preceding expandable row is toggled |
data-selectable | tr | boolean | Mark row as selectable via checkbox |
CSS-Only Attributes
These attributes are handled by CSS styling, not JavaScript logic.
| Attribute | On | Value | Description |
|---|---|---|---|
data-responsive | <table> | card | Transform to card layout on narrow screens. |
data-sticky | <table> | header | column | both | Make header and/or first column sticky. |
data-sticky-column | <table> | number | Number of columns to make sticky (default: 1). |
data-label | <td> | string | Label shown in responsive card mode. |
data-filter-value | <td> | string | Custom value for filtering instead of cell text. |
Accessibility
Keyboard Navigation
| Key | Action |
|---|---|
| Tab | Move between interactive elements (sort headers, checkboxes, buttons) |
| Enter / Space | Activate sort, toggle checkbox, or expand/collapse row |
| Arrow Up/Down | Navigate between rows when focused on a row |
| Home / End | Jump to first/last page in pagination |
ARIA Attributes
aria-sorton sortable column headers indicates current sort directionaria-expandedon expand buttons indicates row expansion statearia-selectedon selectable rows indicates selection statearia-live="polite"on filter results announces match countaria-labelon pagination controls describes navigation
Reduced Motion
When prefers-reduced-motion: reduce is set, all transitions and animations are disabled. Sort indicators, row expansion, and pagination all function without motion effects.
Screen Reader Support
- Sort state changes are announced when columns are sorted
- Filter result counts are announced as users type
- Row expansion state is announced when toggled
- Selection count updates are announced on change