@excom/data-table
v0.1.3
Published
<data-table> custom element
Downloads
658
Maintainers
Readme
data-table
Sortable, filterable tables from plain custom tags — one behavior owner
(<data-table> + <data-th>), everything else is CSS.
<data-table>
<data-thead>
<data-tr>
<data-th column-type="string" sort-direction="asc">Name</data-th>
<data-th column-type="number">Age</data-th>
</data-tr>
</data-thead>
<data-tbody>
<data-tr><data-td>Beatrice</data-td><data-td>44</data-td></data-tr>
<data-tr><data-td>Cynthia</data-td><data-td>41</data-td></data-tr>
<data-tr><data-td>Beau</data-td><data-td>29</data-td></data-tr>
<data-tr><data-td>Adam</data-td><data-td>36</data-td></data-tr>
</data-tbody>
</data-table>Features
- Sort Click a
<data-th>to visually sort by string, number, or date - Filter
filter-valuehides non-matching rows - Export The
--exportcommand downloads visible / all rows as CSV / JSON - DOM-stable Sort / filter via CSS only. Does not conflict with DOM owners, such as Quark.
- Bindable counts
.provisionis{ totalRows, visibleRows, sortColumnIndex, sortDirection, filterValue }— a "12 of 40 rows" readout is one Quark rule - Plain structural tags
<data-thead>/<data-tbody>/<data-tr>/<data-td>are CSS-only — no registration cost
Installation
@excom/data-table v0.1.3
pnpm add @excom/data-tablenpm install @excom/data-tableyarn add @excom/data-tableImport
import "@excom/data-table";Usage
Only <data-table> and <data-th> are registered custom elements.
<data-thead>, <data-tbody>, <data-tr>, <data-td>, <data-tfoot>, and <data-tf> are plain tags — this package's CSS styles them as a table (or apply the equivalent .tag-data-* classes).
Sort and filter are visual only (CSS order / display).
Row nodes never move or leave the DOM, so Quark bindings and iterate() tables keep working.
<data-table>
<data-thead>
<data-tr>
<data-th column-type="string" sort-direction="asc">Name</data-th>
<data-th column-type="number">Age</data-th>
</data-tr>
</data-thead>
<data-tbody>
<data-tr><data-td>Adam</data-td><data-td>36</data-td></data-tr>
<data-tr><data-td>Beau</data-td><data-td>29</data-td></data-tr>
</data-tbody>
</data-table><data-tbody> is required — sorting and filtering both operate on its
<data-tr> children.
.provision reports the row counts and the active sort / filter, recomputed after connect, after a sort, and after every filter change.
Read it from a rule matching the table:
data-table {
$visible: prop("provision").visibleRows;
$total: prop("provision").totalRows;
[bind-count] { content: "#{$visible} of #{$total} rows"; }
}API Reference
Attributes
| Name | Surface | Type | Default | Values | Description |
| --- | --- | --- | --- | --- | --- |
| filter-value | option | string | | | Hides data-tr rows (via --data-tr-display: none) whose text content doesn't include this value. Case-insensitive unless filter-casing is set. Unset / empty clears the filter. Rows stay in the DOM so Quark bindings survive. |
| filter-casing | option | boolean | | | Match filter-value case-sensitively instead of the default case-insensitive comparison. |
Provision
| Name | Type | Description |
| --- | --- | --- |
| provision | DataTableProvision ({ totalRows: number; visibleRows: number; sortColumnIndex: number \| null; sortDirection: "asc" \| "desc" \| null; filterValue: string \| null; }) | { totalRows, visibleRows, sortColumnIndex, sortDirection, filterValue } — recomputed after connect, after the data-table-sort default action, and after every filter change. Not reflected as an attribute. |
Recognized Elements
| Relationship | Selector | Required | Description |
| --- | --- | --- | --- |
| data-th | descendant | yes | Sortable column header. Clicking toggles its sort-direction and fires data-th-sort, which becomes the active column. |
| data-tbody | descendant | yes | Required row container. Sorting and filtering both operate on its data-tr children. |
| data-tr | descendant | yes | Row, direct child of data-tbody. Gets --data-tr-order on sort and --data-tr-display on filter. DOM order is unchanged. |
| data-td | descendant | yes | Cell within a data-tr, read as sortable / export cell content. |
Fires
| Name | Type | Description |
| --- | --- | --- |
| data-table-sort | DataTableSortEvent (CustomEvent & { type: "data-table-sort"; detail: { sortDirection: "asc" \| "desc"; columnType: "string" \| "number" \| "date"; columnIndex: number; sortFn: (a: string, b: string) => number; rows: HTMLElement[]; }; bubbles: true; cancelable: true; composed: true }) | Cancelable. Dispatched when the active sort column / direction changes: on connect (if a data-th already has sort-direction) and after every data-th-sort. Call preventDefault() to take over sorting yourself. |
Listens for
| Name | Type | Description |
| --- | --- | --- |
| data-th-sort | DataThSortEvent (CustomEvent & { type: "data-th-sort"; detail: void; bubbles: true; cancelable: true; composed: true }) | Bubbled up from a descendant data-th when its sort-direction changes. Sets that header as the active sort column (clearing sort-direction from the previously active one) and emits data-table-sort. |
Commands
| Command | Action |
| --- | --- |
| --export | Builds a file from the table's data-tr / data-td / data-th text content and downloads it. Options are data-* on the invoker: data-file-type (csv, the default, or json), data-file-name (default export_table_<locale-date>), and data-full to download every row in DOM order instead of only the visible rows in visual sort order. Filtering needs no command: write filter-value / filter-casing. |
Default actions
| Event | Default behavior (unless preventDefault() is called) |
| --- | --- |
| data-table-sort | Sorts event.detail.rows by columnIndex with sortFn and sets --data-tr-order on each row (visual CSS order — DOM order is unchanged). |
CSS Custom Properties
| Name | Syntax | Default | Description |
| --- | --- | --- | --- |
| --data-table-cols | <integer> | 1 | Column count for shared subgrid tracks. Auto-detected from the widest row; set on the host to override. |
CSS Aliases
| Alias | Kind | Matches | Description |
| --- | --- | --- | --- |
| :--data-table | element | data-table, .tag-data-table | |
| :--data-thead | element | data-thead, .tag-data-thead | |
| :--data-tbody | element | data-tbody, .tag-data-tbody | |
| :--data-tfoot | element | data-tfoot, .tag-data-tfoot | |
| :--data-tr | element | data-tr, .tag-data-tr | |
| :--data-td | element | data-td, .tag-data-td | |
| :--data-tf | element | data-tf, .tag-data-tf | |
Examples
Filter rows + export as CSV
Filtering is State: write filter-value / filter-casing on the table — here a Quark @on input block copies the search field into them. Matching is case-insensitive unless filter-casing is set.
Invoke --export on the table (<button command="--export" commandfor="…">) to download its visible rows (visual sort order). The button's data-file-type is csv (default) or json; data-file-name sets the download name; data-full downloads every row in DOM order, regardless of filtering / sorting.
The first export button is the happy path (the table as you see it, including active filter / sort). The form below it writes data-file-type / data-file-name / data-full onto its button.
<div>
<quark-sheet>
:scope {
@on input (target: "form[data-filter]") {
data-table { filter-value: target.elements.filterValue.value; }
}
@on change (target: "form[data-filter]") {
data-table { filter-casing: target.elements.filterCasing.checked; }
}
@on change (target: "form[data-export]") {
form[data-export] [command="--export"] {
data-file-type: target.elements.fileType.value;
data-file-name: target.elements.fileName.value;
data-full: target.elements.full.checked;
}
}
}
</quark-sheet>
<form data-filter>
<fieldset role="group">
<input name="filterValue" placeholder="Filter table…" />
<label>
Case-sensitive
<input name="filterCasing" role="switch" type="checkbox" />
</label>
</fieldset>
</form>
<data-table id="demo-data-table-filter">
<data-thead>
<data-tr>
<data-th column-type="string">Name</data-th>
<data-th column-type="number">Age</data-th>
</data-tr>
</data-thead>
<data-tbody>
<data-tr><data-td>Beatrice</data-td><data-td>44</data-td></data-tr>
<data-tr><data-td>Cynthia</data-td><data-td>41</data-td></data-tr>
<data-tr><data-td>Beau</data-td><data-td>29</data-td></data-tr>
<data-tr><data-td>Adam</data-td><data-td>36</data-td></data-tr>
</data-tbody>
</data-table>
<hr />
<button type="button" command="--export" commandfor="demo-data-table-filter">
Export as-is
</button>
<hr />
<form data-export class="tag-article">
<header>
<h4>Export with options</h4>
</header>
<label>
<select name="fileType">
<option value="csv" selected>CSV</option>
<option value="json">JSON</option>
</select>
</label>
<label>
File name
<input name="fileName" placeholder="File name" />
</label>
<label>
<input name="full" type="checkbox" role="switch" />
Full table (ignores active filter/sort)
</label>
<button type="button" command="--export" commandfor="demo-data-table-filter">Export</button>
</form>
</div>Release notes
0.1.1 (2026-09-23)
- Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
<data-th>
Sortable column header — click toggles sort direction.
Tag: <data-th>
API
Attributes
| Name | Surface | Type | Default | Values | Description |
| --- | --- | --- | --- | --- | --- |
| column-type | option | string | | "string" | "number" | "date" | Sort comparator to use for this column's cell values. data-table treats null as "string". |
| sort-direction | hybrid | string | | "asc" | "desc" | Current sort direction. A click toggles between asc / desc; setting it (by any means) fires data-th-sort. Only one data-th per table should carry this at a time — the parent <data-table> clears the previously active header when a new one is set. |
Fires
| Name | Type | Description |
| --- | --- | --- |
| data-th-sort | DataThSortEvent (CustomEvent & { type: "data-th-sort"; detail: void; bubbles: true; cancelable: true; composed: true }) | Dispatched whenever sort-direction is set, whether by a click or programmatically. Bubbles to the parent <data-table>. |
CSS Aliases
| Alias | Kind | Matches | Description |
| --- | --- | --- | --- |
| :--data-th | element | data-th, .tag-data-th | |
| :--data-th--sort | state | :is(:--data-th) | |
| :--data-th--sort-asc | state | [sort-direction="asc"], [aria-sort="ascending"] | |
| :--data-th--sort-desc | state | [sort-direction="desc"], [aria-sort="descending"] | |
Full documentation: https://excom.dev/nucleus/packages/data-table
