Data Table

Native tables with responsive overflow, density and border variants, sortable headers, row states, totals, and sticky headers.

An order queue built from real table concerns

Selection, row headers, status, aligned money, timestamps, and actions compose without changing native table semantics.

Orders requiring attention
OrderCustomerStatusTotalPlacedActions
#1848Northstar LabsReview$1,284.0012 min ago
#1847Juniper StudioPacking$248.5028 min ago
#1846Atlas CoffeePaid$96.0041 min ago

More examples

Each example introduces a different part of the shipped API.

Sortable metrics expose state on the header

The live buttons reorder rows and move aria-sort to the active column; CSS only supplies the full-cell control and state icon.

Regional revenue · Activate a heading to sort
Region
North America$184,200+12.4%
Europe$152,800+8.7%
Asia Pacific$131,600+18.2%
Latin America$98,400+6.1%

Footers and numeric alignment make totals scannable

tfoot separates the summary from the body while data-numeric aligns currency with tabular figures.

August subscription forecast
PlanAccountsMRRChange
Starter1,842$27,630+4.2%
Team684$68,400+7.8%
Business126$31,500+2.1%
Total2,652$127,530+5.6%

Grouped headers describe a real comparison

colgroup and rowgroup scopes preserve relationships when one header spans several related metrics.

Support performance · last 30 days
TeamVolumeQuality
ReceivedResolvedCSATSLA
Americas1,2841,19694%88%
Europe96894196%92%
Asia Pacific74270493%86%

Sticky headers belong to a constrained scroll region

The wrapper owns both axes of overflow while native header cells stay visible during a long inventory review.

Low-stock inventory
SKUProductOn handReorder atSupplier
ST-204Studio headphones712Signal Co.
KB-118Compact keyboard410North Input
LM-042Monitor light915Luma Works
DS-730Desk shelf28Oakline
CM-315USB microphone610Signal Co.
AR-510Monitor arm39Frame Labs

Empty results remain part of the table

A spanning message cell preserves the columns and gives filters a clear recovery action instead of replacing the table with unrelated markup.

Invoices · Status: overdue · Customer: Northstar
InvoiceCustomerDueTotal
No matching invoices
Clear one or more filters to see results.

CSS Tags enhances native table elements instead of replacing their accessibility model. Keep <table>, <caption>, <th>, and <td> in the markup; use the component API for presentation and overflow.

Host model

Use data-table or .data-table directly on a small native table. When a data set needs horizontal overflow, keyboard scrolling, or a sticky header, put the native table inside any of the equivalent wrapper hosts:

<data-table tabindex="0" aria-label="Scrollable orders">
<table>...</table>
</data-table>
<div data-table-scroll tabindex="0" aria-label="Scrollable orders">
<table>...</table>
</div>
<div class="table-scroll" tabindex="0" aria-label="Scrollable orders">
<table>...</table>
</div>

Each wrapper now styles its direct table automatically. Add tabindex="0" only when overflow is possible, and provide a label when nearby text does not already name the region.

Small datasets fit without forced overflow

The default minimum width is now 100%, so a simple table uses its container instead of becoming 36rem wide.

Feature rollout
FeatureAudienceEnabled
New billingTeam plans82%
Search v2All accounts100%

Responsive overflow

Set --table-min-width only when wrapping would make a wide data set difficult to scan. The wrapper owns scrolling while the table keeps its native display:

<data-table
tabindex="0"
aria-label="Scrollable order history"
style="--table-min-width: 48rem"
>
<table>...</table>
</data-table>

The wrapper has a visible focus ring, contained overscroll, and a themeable thin scrollbar. A direct table[data-table] still receives a small-screen overflow fallback when a wrapper cannot be added.

Variants

Variants work on either the table or wrapper and have custom-attribute, data-*, and class forms.

PurposeCustom hostData hostClass host
Alternating rowsstripeddata-striped.table-striped
Pointer and focus-within row highlighthoverdata-hover.table-hover
Cell grid and strong outer borderbordereddata-bordered.table-bordered
No outer border or dividersborderlessdata-borderless.table-borderless
Compact densitydensity="compact"data-density="compact".table-compact
Spacious densitydensity="spacious"data-density="spacious".table-spacious
Sticky header wrappersticky-headerdata-sticky-header.table-sticky-header

Hover is opt-in and only activates on devices that actually support hover. Rows containing keyboard focus receive the same treatment.

Cell and row hooks

  • data-numeric / .table-numeric aligns values to the inline end and uses tabular figures.
  • data-actions / .table-actions keeps a narrow action column aligned to the inline end.
  • data-nowrap / .table-nowrap protects dates, identifiers, and short status values from wrapping.
  • data-table-message styles a spanning empty, loading, or error cell.
  • A body row containing a checked checkbox or radio receives the selected-row surface. data-selected is available when application state is mirrored in the DOM.
  • <tfoot> receives a stronger separator and summary surface automatically.

Sorting

Put the current sort direction on the header cell, not the button. The button owns interaction; an aria-hidden icon reflects the header state:

<th scope="col" aria-sort="descending">
<button type="button" data-table-sort>
Revenue
<span data-sort-icon aria-hidden="true"></span>
</button>
</th>

CSS Tags makes that button fill the header cell and supplies distinct unsorted, ascending, and descending icons. Your application must reorder the rows and move aria-sort to the active header. The interactive revenue example above does both. Only one header should carry aria-sort at a time, following the WAI sortable table pattern.

document.querySelectorAll("table[data-sortable]").forEach((table) => {
table.addEventListener("click", (event) => {
const button = event.target.closest("button[data-table-sort]");
if (!button) return;
const header = button.closest("th");
const column = header.cellIndex;
const direction = header.getAttribute("aria-sort") === "ascending" ? "descending" : "ascending";
const factor = direction === "ascending" ? 1 : -1;
const rows = [...table.tBodies[0].rows];
const value = (row) => row.cells[column].dataset.sortValue
?? row.cells[column].textContent.trim();
table.querySelectorAll("th[aria-sort]").forEach((cell) => cell.removeAttribute("aria-sort"));
header.setAttribute("aria-sort", direction);
rows.sort((a, b) => value(a).localeCompare(value(b), undefined, { numeric: true }) * factor);
table.tBodies[0].append(...rows);
});
});

Sticky headers

Sticky headers require a vertically constrained scroll wrapper. Set its maximum height with --table-max-block-size:

<data-table
sticky-header
tabindex="0"
aria-label="Scrollable inventory"
style="--table-max-block-size: 20rem; --table-min-width: 42rem"
>
<table>...</table>
</data-table>

Theme tokens

.billing-table {
--table-min-width: 44rem;
--table-layout: auto;
--table-cell-padding-block: 0.75rem;
--table-cell-padding-inline: 1rem;
--table-heading-bg: var(--table-heading-background);
--table-row-hover-bg: var(--table-hover-background);
--table-row-selected-bg: var(--table-selected-background);
--table-footer-bg: var(--table-footer-background);
--table-max-block-size: 24rem;
}

Additional component tokens include --table-bg, --table-text, --table-border-width, --table-border-color, --table-divider-width, --table-divider-color, --table-radius, --table-font-size, --table-line-height, --table-caption-*, --table-heading-*, --table-stripe-bg, --table-scrollbar-*, and --table-message-*.

Accessibility checklist

  • Use <caption> when a visible name helps identify the dataset.
  • Add scope="col" and scope="row" for simple tables; use colgroup, rowgroup, id, and headers for more complex relationships.
  • Keep sorting controls as real buttons and update aria-sort with the rendered order.
  • Give selection checkboxes an accessible name that identifies their row.
  • Make scroll wrappers keyboard reachable and visibly focused.
  • Do not use a table for page layout or replace native table structure with responsive card markup when the content remains tabular.