npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@excom/data-table

v0.1.3

Published

<data-table> custom element

Downloads

658

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-value hides non-matching rows
  • Export The --export command 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 .provision is { 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-table
npm install @excom/data-table
yarn add @excom/data-table

Import

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