ComponentsData & statusDatatable
On this page
Data & status

Datatable released

A table drawn from your rows and a typed list of columns: sorting by one key or several, a quick filter over what the cells show, and the empty and loading states, in the look of the platform table.

  • Import@pacit/components/table
  • Selectorspct-data-table, ng-template[pctCell], ng-template[pctHeaderCell], ng-template[pctEmpty]

Semantics come from the native <table> — no role and no keyboard handling of ours

Devices
Recuperator Modbus TCP 0.2 kW
Inverter Modbus RTU 9.9 kW
Heat pump Modbus TCP 2.4 kW

datatable.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctDataTable } from '@pacit/components/table';
ExportedWhat it is
PctTableSortinterfaceunreleasedThe column the rows stand sorted by, named by the key its header carries, and which way.
PctTablecomponentunreleasedtable[pctTable]A table the consumer writes, dressed by the library: the platform's <table>, its caption, header cells and cells, each element wearing a component of its own.
PctTableCaptioncomponentunreleasedcaption[pctCaption]The table's caption: its name, which a reader announces with the table, and the name of the frame around it when the frame scrolls.
PctTableHeadercomponentunreleasedth[pctHeader]A header cell. With a sortKey its content is a button that proposes sorting the rows by that column, and the header of the sorted column carries aria-sort.
PctTableCellcomponentunreleasedtd[pctCell]A cell.
PctTableFramecomponentunreleasedpct-table-frameThe frame a table scrolls in when it is wider than the space it is given, or taller than a height it is given. While the table overflows, the frame is a focusable region named by the table's caption, so a keyboard can reach it and scroll it and a reader can say what it holds. While the table fits, the frame is nothing: a stop with nothing to scroll is a stop for nothing.
PctTableVisibleinterfaceunreleasedThe lines in the view, [start, end) among the lines drawn, and the ids of the rows among them, in order.
PctScrollAligntypeunreleased'start' | 'center' | 'end' | 'nearest'Where a row the table scrolls to stands in the view: at its top, its centre, its bottom, or wherever is nearest from where the view is — the default.
PctScrollToOptionsinterfaceunreleasedHow scrollTo scrolls.
PctDataTablecomponentunreleasedpct-data-tableA table built from data: typed columns, sorting by one key or several, a quick filter, and the empty and loading states, rendered as the first layer's <table pctTable> so the two look the same (0097).
PctCellContextinterfaceunreleasedWhat a cell template is handed.
PctCellTemplatedirectiveunreleasedng-template[pctCell]A cell the consumer draws for one column, or for several that share a look.
PctHeaderCellContextinterfaceunreleasedWhat a header template is handed.
PctHeaderCellTemplatedirectiveunreleasedng-template[pctHeaderCell]What a header shows in place of the column's header text.
PctEmptyContextinterfaceunreleasedWhat the empty-state template is handed.
PctEmptyTemplatedirectiveunreleasedng-template[pctEmpty]What the table shows beside itself when it has no row to show, in place of its sentence.
const col = pctColumnsOf<Rule>();
readonly columns = col.of([
  col.key('name', { header: 'Name' }),
  col.key('energy', { header: 'Energy', kind: { number: { unit: 'kWh' } } }),
  col.key('enabled', { header: 'Enabled', kind: 'boolean', sort: false }),
]);
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

The quick filter: every word in some column of the row, searched in the text it shows.

Devices
Recuperator Attic 0.2 kW
Inverter Garage 9.9 kW
Heat pump Garage 2.4 kW
Boiler Kitchen 1.8 kW

4 of 4

API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

unreleased marks 36 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.

A table built from data: typed columns, sorting by one key or several, a quick filter, and the empty and loading states, rendered as the first layer's <table pctTable> so the two look the same (0097).

Inputs

NameTypeWhat it does
rowsunreleaseddefault []readonly T[]The rows. They are read and never changed: the table filters and orders a copy.
columnsrequiredunreleasedPctColumns<TThe columns, built with pctColumnsOf<T>() so every cell template is checked against their ids and their values' types.
rowIdrequiredunreleased(row: T) => PctRowIdWhat names a row across renders: @for tracks it, so a row keeps its element, its focus and its scroll position when the application hands the table a new list with the same ids. Unique across the rows.
captionunreleaseddefault ''stringThe table's name, drawn as its <caption>. A table needs a name: this, or ariaLabelledby.
ariaLabelledbyunreleaseddefault ''stringThe id of an element that names the table, for a table whose name stands elsewhere on the page. It names the frame too when the frame scrolls.
sorttwo-wayunreleaseddefault []readonly PctTableSort<keyof M & string>[]The order of the rows: a list of keys, the first deciding first. A press on a sortable header makes that column the only key, ascending, and turns it the other way on the next; with Shift held it adds the column as the last key, or turns it where it stands.
unsortableunreleaseddefault falsebooleanA third press on a descending column takes it out of the order, so the rows can go back to the order they came in. false by default, as the APG's sortable table toggles.
clientSortunreleaseddefault truebooleanWhether the table orders the rows by sort itself. false for rows a server has already ordered: the headers still write sort, and the application asks the server.
filtertwo-wayunreleaseddefault ''stringThe quick filter's question: every word in it must be found in some column of a row. A question, not a value: the table never writes it.
clientFilterunreleaseddefault truebooleanWhether the table filters the rows by filter itself. false for rows a server has already filtered.
filterWithunreleaseddefault null((row: T, question: string) => boolean) | nullA predicate in place of the quick filter's own, asked for every row with the question as it was typed, empty included, so it can answer other filters of the application's too.
loadingunreleaseddefault falsebooleanRows are coming. The body is aria-busy, and with no row yet the table draws placeholder rows and says it is loading (0037).
localeunreleaseddefault ''stringThe locale the values are written and the text ordered in; the application's LOCALE_ID when empty.
nowunreleaseddefault nullPctInstant | nullThe instant a relative column counts from. Unset, it is the clock's when the cells are written; an application that wants in 5 minutes to tick binds a clock of its own.
stickyHeaderunreleaseddefault truebooleanThe header row stays at the top while the rows scroll under it. It needs a frame that scrolls, so it holds when the table is given a height, and true by default.
stripedunreleaseddefault falsebooleanEvery other row on a tint, so a wide row is easier to follow across.
rowNumbersunreleaseddefault falsebooleanA leading column numbering the rows as they stand, from 1: the row a user names when they say "row 12". A cell of data, not the row's header: the row's name is its data.
rowToneunreleaseddefault null((row: T) => PctTone | null) | nullThe tone a row wears: a tint behind the whole row. Like a column's tone, it repeats what a cell of the row says and never says it alone.
highlightMatchesunreleaseddefault truebooleanThe words the quick filter found are marked in the cells it read, so the eye sees why a row stayed. Only where the table's own filter decided: not with filterWith, nor with clientFilter off, where what matched is not the table's to say.
highlightChangesunreleaseddefault falsebooleanA cell whose value changed flashes for a moment, so a value that moved in a table of live readings is seen moving. A value is compared as itself, a date by its time: the clock moving a relative instant on is no change, and a value that is a new object at every poll flashes at every poll. The flash is a motion token's, so a user who asked for less motion sees none.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-data-table
data-pct-stickystickyHeader() ? "" : null
data-pct-stripedstriped() ? "" : null
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctDataTableHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
mark[data-pct-part="mark"]a word the quick filter found, in a cell it read by its text
message[data-pct-part="message"]the sentence beside an empty table, outside the table
place[data-pct-part="place"]the place and the way of a sort key after the first, drawn for the eye
placeholder[data-pct-part="placeholder"]a row of skeletons standing for rows that are still coming, hidden from readers
icon[data-pct-part="icon"]its chevron
toggle[data-pct-part="toggle"]the first layer's button in a sortable header — see the table's card

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-table-bg-changed{pct.warning-200}#fde68a · #b45309Background a data table's cell flashes in when its value changes.
--pct-table-bg-danger{pct.danger-100}#fee2e2 · #991b1bBackground of a data table's row or cell in the danger tone.
--pct-table-bg-hover{pct.primary-200}#bfdbfe · #1d4ed8Background of a data table's row under the pointer.
--pct-table-bg-info{pct.info-100}#dbeafe · #1e40afBackground of a data table's row or cell in the info tone.
--pct-table-bg-striped{pct.surface-100}#f1f5f9 · #1e293bBackground of every other row of a striped data table.
--pct-table-bg-success{pct.success-100}#dcfce7 · #166534Background of a data table's row or cell in the success tone.
--pct-table-bg-warning{pct.warning-100}#fef3c7 · #92400eBackground of a data table's row or cell in the warning tone.
--pct-table-border{pct.border}#e2e8f0 · #334155Line under a row.
--pct-table-caption-fg{pct.text}#0f172a · #ffffffText colour of the caption.
--pct-table-caption-font-weight{pct.font.weight.medium}500Font weight of the caption.
--pct-table-caption-gap{pct.space.3}8pxSpace between the caption and the table.
--pct-table-column-max-width20remMaximum width of a data table's column whose cells are cut to one line.
--pct-table-fg{pct.text}#0f172a · #ffffffText colour of the cells.
--pct-table-fg-danger{pct.on-danger-100}#991b1b · #fee2e2Text colour of a data table's row or cell in the danger tone.
--pct-table-fg-info{pct.on-info-100}#1d4ed8 · #bfdbfeText colour of a data table's row or cell in the info tone.
--pct-table-fg-muted{pct.text-muted}#334155 · #e2e8f0Text colour of the numbers of a data table's rows.
--pct-table-fg-success{pct.on-success-100}#166534 · #dcfce7Text colour of a data table's row or cell in the success tone.
--pct-table-fg-warning{pct.on-warning-100}#92400e · #fef3c7Text colour of a data table's row or cell in the warning tone.
--pct-table-font-size{pct.font.size.md}14pxFont size of the table.
--pct-table-heading-bg{pct.surface-100}#f1f5f9 · #1e293bBackground of a header cell.
--pct-table-heading-border{pct.border-strong}#64748bLine under the header row.
--pct-table-heading-fg{pct.text}#0f172a · #ffffffText colour of a header cell.
--pct-table-heading-font-weight{pct.font.weight.medium}500Font weight of a header cell.
--pct-table-icon-fg-muted{pct.text-muted}#334155 · #e2e8f0Colour of the chevron a sortable header shows as a hint.
--pct-table-mark-bg{pct.warning-200}#fde68a · #b45309Background of a word the quick filter found.
--pct-table-mark-fg{pct.on-warning-100}#92400e · #fef3c7Text colour of a word the quick filter found.
--pct-table-padding-x{pct.space.4}12pxHorizontal padding of a cell and a header cell.
--pct-table-padding-y{pct.space.3}8pxVertical padding of a cell and a header cell.
--pct-table-toggle-gap{pct.space.2}4pxGap between a sortable header's label and its chevron.
--pct-table-toggle-target-min{pct.target.min}24pxTouch-target floor of a sortable header.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-table-heading-bg: #ccfbf1;
  --pct-table-border: #99f6e4;
}
Devices
Recuperator Modbus TCP 0.2 kW
Inverter Modbus RTU 9.9 kW
Heat pump Modbus TCP 2.4 kW
Accessibility

Measured, not assumed

Pattern: a native <table> — sorting as the APG's sortable table draws it, with aria-sort on the header of the first sort key alone. Named in the class JSDoc.

Keyboard

keyeffecttest
Tab / Shift+Tabmoves between the sort buttons and the controls in the cellsapps/sandbox-e2e/src/datatable.spec.ts — "Tab moves from the sort buttons into the controls in the cells", and Shift+Tab in the next
Enter / Spacesorts by the column as its only key, or turns the first keyapps/sandbox-e2e/src/datatable.spec.ts — "the keyboard sorts by one key, Space turns it, and Shift+Enter adds a second marked by its place"
Shift+Enteradds the column as the next key, or turns it where it standsthe same case

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA pattern named in the class JSDoclibs/components/table/src/data-table.ts
MeasuredKeyboard map tested key by keythe table above — every row measured in apps/sandbox-e2e/src/datatable.spec.ts
Measuredaxe audit on the component's own sandbox viewapps/sandbox-e2e/src/a11y.spec.ts — /datatable in SBX_ROUTES
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — datatable-devices and datatable-devices-rtl: stripes, numbers, tones and a cut name; the first layer's table-pattern holds the rest of the paint
Measuredforced-colors: active — no state carried by colour alonethe first layer's: the lines CanvasText, the focus Highlight; the data table adds a check mark, a drawing, and the place of a sort key, a digit. A tone repeats a word of its row, so the tint forced colours drop takes nothing with it, and a marked word takes the system's Mark and MarkText
Measuredprefers-reduced-motion — duration from a tokenlibs/components/table/src/data-table.scss — a changed value's flash lasts eight of --pct-motion-transition-duration, which the reduced scope makes 0.01ms, so a user who asked for less motion sees none; the skeletons of a loading table follow their own
MeasuredTouch target ≥ 24×24 px outrightthe first layer's sort button, libs/components/table/src/header.scss; the controls in cells are the consumer's own components
MeasuredSize axis aligned to --pct-control-height-*deliberately not: a cell is not a control, as the first layer's card says
MeasuredDensity axisthe first layer's padding on space.3 and space.4, re-pointed by the compact scope
MeasuredRTL — no physical properties + a dir="rtl" screenshotlogical properties only, held by check-styles; the screenshot is the first layer's table-pattern-rtl
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /datatable in SBX_ROUTES; the scroll listener and the observer start after render
n/aFormsnot applicable — the selection, the one value a form will own, arrives as a control of its own
MeasuredParts registered in the inventorylibs/components/parts.snapshot.md — message, placeholder, place, mark
MeasuredTokens registered + an entry in contrast.policy.jsonlibs/tokens/src/component.table.json — the rows' surfaces, the tones, the mark; libs/tokens/src/contrast.policy.json — a cell's text on each surface, each tone's pair, the rows' numbers and the mark
MeasuredStrings through PCT_TEXTSlibs/components/core/src/texts.ts — tableEmpty, tableNoMatches, tableLoading, tableYes, tableNo, tableRowNumber, tableSortLater, tableSortAscending, tableSortDescending
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./table row
MeasuredScreen-reader logRead 2026-10-30 from docs/acr/at/ — three readers, whole sandbox. The table is a table under all three: Orca enters it by its shape, table with 21 rows 6 columns · column header. · row 1. · column 1 · Name · button.; VoiceOver says the sort button, Room button; NVDA reads it as its header, Room, column header, row 1, column 2, with no role word, as on the first layer. A control in a cell is read with its row and its column: NVDA Enabled: Supply fan, row 2, Enabled, column 6, Orca row 3. · Enabled: Heater · switch not pressed., VoiceOver Enabled: Supply fan on switch. A table given a height is a named stop, because its frame scrolls: NVDA Polling rules, region, VoiceOver Polling rules region, Orca Polling rules. · clickable. · Browse mode. The press, the filter and the empty sentence are not walked: the e2e suite reads them
MeasuredA docs page with live examplesapps/sandbox/src/app/views/datatable/ (the sandbox view). The published site: /components/datatable, its demos' source in the code tab
MeasuredUnit + mutationlibs/components/table/src/data-table.spec.ts, the engine's specs in libs/components/table/src/model/; libs/components/mutation.snapshot.md — measured by a narrow run
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

99.64%mutation score552 of 554 mutants killed
14 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
0colour pairs measuredlight and dark, at every token build
2visual baselinesChromium, the sandbox's own pictures
34elements in the preview10 listeners · 1 render to settle · 14369 µs in jsdom, held exactly but for the clock

Known limitations

  • No window yet. Every row is in the DOM, which 0098 measured as fluent up to 8,000 cells — a thousand rows of eight columns. A longer list waits for the window.
  • A later sort key is a description. aria-sort stands on one header at a time, as ARIA asks; a key after the first describes its header's button — Sort key 2, descending — and a reader says a description after the name, or only when asked.
  • visible counts lines. start and end are positions among the lines drawn, the end excluded, so a details row or a group heading, when they come, counts as a line too.
  • The sticky header needs a height. The header row stays at the top of a table given a block size, where its frame scrolls; a table the page scrolls has no box to stick to.
  • A cell template does not change what is searched. The quick filter reads the text the column's kind writes, whatever the template draws in its place.
  • A cut cell's whole text is the pointer's. The tooltip is the platform's title, which a keyboard does not open; a reader hears the whole text, because the cut is only paint.
  • A tone is a tint and a colour. It is gone in forced colours and for a reader who cannot tell the colours apart, which is why a column of states wears the tone of the word it writes rather than a tone of its own.
  • A flash compares values, not texts. A value that is a new object at every poll flashes at every poll; the clock moving a relative instant on does not.
  • now is read when the rows are written. A relative column counts from the clock each time the rows, the order or the filter change; an application that wants in 5 minutes to tick between those binds a clock of its own.
  • NVDA does not say the sort is a control, as the first layer's card records for the same button: Room, column header, row 1, column 2 in the walk of 2026-10-30.