The quick filter: every word in some column of the row, searched in the text it shows.
| Recuperator | Attic | 0.2 kW |
| Inverter | Garage | 9.9 kW |
| Heat pump | Garage | 2.4 kW |
| Boiler | Kitchen | 1.8 kW |
4 of 4
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.
@pacit/components/tablepct-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
| Recuperator | Modbus TCP | 0.2 kW |
| Inverter | Modbus RTU | 9.9 kW |
| Heat pump | Modbus TCP | 2.4 kW |
import { Component } from '@angular/core';
import { PctDataTable, pctColumnsOf } from '@pacit/components/table';
interface Device {
readonly id: number;
readonly name: string;
readonly protocol: string;
readonly power: number;
}
const col = pctColumnsOf<Device>();
/** A table built from data: three typed columns, sortable by a press on their headers. */
@Component({
selector: 'demo-datatable',
imports: [PctDataTable],
template: `
<pct-data-table
caption="Devices"
[rows]="devices"
[columns]="columns"
[rowId]="byId"
/>
`,
})
export class DatatableDemo {
readonly columns = col.of([
col.key('name', { header: 'Device' }),
col.key('protocol', { header: 'Protocol' }),
col.key('power', {
header: 'Power',
kind: { number: { unit: 'kW', fractionDigits: 1 } },
}),
]);
readonly byId = (device: Device) => device.id;
readonly devices: readonly Device[] = [
{ id: 1, name: 'Recuperator', protocol: 'Modbus TCP', power: 0.2 },
{ id: 2, name: 'Inverter', protocol: 'Modbus RTU', power: 9.9 },
{ id: 3, name: 'Heat pump', protocol: 'Modbus TCP', power: 2.4 },
];
}datatable.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctDataTable } from '@pacit/components/table';| Exported | What it is |
|---|---|
PctTableSortinterfaceunreleased | The 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-frame | The 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. |
PctTableVisibleinterfaceunreleased | The 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. |
PctScrollToOptionsinterfaceunreleased | How scrollTo scrolls. |
PctDataTablecomponentunreleasedpct-data-table | 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). |
PctCellContextinterfaceunreleased | What a cell template is handed. |
PctCellTemplatedirectiveunreleasedng-template[pctCell] | A cell the consumer draws for one column, or for several that share a look. |
PctHeaderCellContextinterfaceunreleased | What a header template is handed. |
PctHeaderCellTemplatedirectiveunreleasedng-template[pctHeaderCell] | What a header shows in place of the column's header text. |
PctEmptyContextinterfaceunreleased | What 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 }),
]);Each example is the component itself, not a screenshot. Open the code under any of them.
| Recuperator | Attic | 0.2 kW |
| Inverter | Garage | 9.9 kW |
| Heat pump | Garage | 2.4 kW |
| Boiler | Kitchen | 1.8 kW |
4 of 4
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).
| Name | Type | What it does |
|---|---|---|
rowsunreleaseddefault [] | readonly T[] | The rows. They are read and never changed: the table filters and orders a copy. |
columnsrequiredunreleased | PctColumns<T | The columns, built with pctColumnsOf<T>() so every cell template is checked against their ids and their values' types. |
rowIdrequiredunreleased | (row: T) => PctRowId | What 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 '' | string | The table's name, drawn as its <caption>. A table needs a name: this, or ariaLabelledby. |
ariaLabelledbyunreleaseddefault '' | string | The 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 false | boolean | A 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 true | boolean | Whether 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 '' | string | The 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 true | boolean | Whether the table filters the rows by filter itself. false for rows a server has already filtered. |
filterWithunreleaseddefault null | ((row: T, question: string) => boolean) | null | A 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 false | boolean | Rows are coming. The body is aria-busy, and with no row yet the table draws placeholder rows and says it is loading (0037). |
localeunreleaseddefault '' | string | The locale the values are written and the text ordered in; the application's LOCALE_ID when empty. |
nowunreleaseddefault null | PctInstant | null | The 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 true | boolean | The 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 false | boolean | Every other row on a tint, so a wide row is easier to follow across. |
rowNumbersunreleaseddefault false | boolean | A 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) | null | The 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 true | boolean | The 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 false | boolean | A 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. |
(click), (focus), the form's submit — are the platform's, and two-way models above carry the rest. The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-data-table | |
data-pct-sticky | stickyHeader() ? "" : null | |
data-pct-striped | striped() ? "" : null |
Two ways in, both stable. No class names to depend on.
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.
| Part | Selector | What 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 |
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.
| Token | Styles |
|---|---|
--pct-table-bg-changed{pct.warning-200}#fde68a · #b45309 | Background a data table's cell flashes in when its value changes. |
--pct-table-bg-danger{pct.danger-100}#fee2e2 · #991b1b | Background of a data table's row or cell in the danger tone. |
--pct-table-bg-hover{pct.primary-200}#bfdbfe · #1d4ed8 | Background of a data table's row under the pointer. |
--pct-table-bg-info{pct.info-100}#dbeafe · #1e40af | Background of a data table's row or cell in the info tone. |
--pct-table-bg-striped{pct.surface-100}#f1f5f9 · #1e293b | Background of every other row of a striped data table. |
--pct-table-bg-success{pct.success-100}#dcfce7 · #166534 | Background of a data table's row or cell in the success tone. |
--pct-table-bg-warning{pct.warning-100}#fef3c7 · #92400e | Background of a data table's row or cell in the warning tone. |
--pct-table-border{pct.border}#e2e8f0 · #334155 | Line under a row. |
--pct-table-caption-fg{pct.text}#0f172a · #ffffff | Text colour of the caption. |
--pct-table-caption-font-weight{pct.font.weight.medium}500 | Font weight of the caption. |
--pct-table-caption-gap{pct.space.3}8px | Space between the caption and the table. |
--pct-table-column-max-width20rem | Maximum width of a data table's column whose cells are cut to one line. |
--pct-table-fg{pct.text}#0f172a · #ffffff | Text colour of the cells. |
--pct-table-fg-danger{pct.on-danger-100}#991b1b · #fee2e2 | Text colour of a data table's row or cell in the danger tone. |
--pct-table-fg-info{pct.on-info-100}#1d4ed8 · #bfdbfe | Text colour of a data table's row or cell in the info tone. |
--pct-table-fg-muted{pct.text-muted}#334155 · #e2e8f0 | Text colour of the numbers of a data table's rows. |
--pct-table-fg-success{pct.on-success-100}#166534 · #dcfce7 | Text colour of a data table's row or cell in the success tone. |
--pct-table-fg-warning{pct.on-warning-100}#92400e · #fef3c7 | Text colour of a data table's row or cell in the warning tone. |
--pct-table-font-size{pct.font.size.md}14px | Font size of the table. |
--pct-table-heading-bg{pct.surface-100}#f1f5f9 · #1e293b | Background of a header cell. |
--pct-table-heading-border{pct.border-strong}#64748b | Line under the header row. |
--pct-table-heading-fg{pct.text}#0f172a · #ffffff | Text colour of a header cell. |
--pct-table-heading-font-weight{pct.font.weight.medium}500 | Font weight of a header cell. |
--pct-table-icon-fg-muted{pct.text-muted}#334155 · #e2e8f0 | Colour of the chevron a sortable header shows as a hint. |
--pct-table-mark-bg{pct.warning-200}#fde68a · #b45309 | Background of a word the quick filter found. |
--pct-table-mark-fg{pct.on-warning-100}#92400e · #fef3c7 | Text colour of a word the quick filter found. |
--pct-table-padding-x{pct.space.4}12px | Horizontal padding of a cell and a header cell. |
--pct-table-padding-y{pct.space.3}8px | Vertical padding of a cell and a header cell. |
--pct-table-toggle-gap{pct.space.2}4px | Gap between a sortable header's label and its chevron. |
--pct-table-toggle-target-min{pct.target.min}24px | Touch-target floor of a sortable header. |
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;
}| Recuperator | Modbus TCP | 0.2 kW |
| Inverter | Modbus RTU | 9.9 kW |
| Heat pump | Modbus TCP | 2.4 kW |
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.
| key | effect | test |
|---|---|---|
Tab / Shift+Tab | moves between the sort buttons and the controls in the cells | apps/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 / Space | sorts by the column as its only key, or turns the first key | apps/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+Enter | adds the column as the next key, or turns it where it stands | the same case |
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.
libs/components/table/src/data-table.tsapps/sandbox-e2e/src/datatable.spec.tsapps/sandbox-e2e/src/a11y.spec.ts — /datatable in SBX_ROUTESapps/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 paintforced-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 MarkTextprefers-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 ownlibs/components/table/src/header.scss; the controls in cells are the consumer's own components--pct-control-height-*deliberately not: a cell is not a control, as the first layer's card saysspace.3 and space.4, re-pointed by the compact scopedir="rtl" screenshotlogical properties only, held by check-styles; the screenshot is the first layer's table-pattern-rtlNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /datatable in SBX_ROUTES; the scroll listener and the observer start after renderlibs/components/parts.snapshot.md — message, placeholder, place, markcontrast.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 markPCT_TEXTSlibs/components/core/src/texts.ts — tableEmpty, tableNoMatches, tableLoading, tableYes, tableNo, tableRowNumber, tableSortLater, tableSortAscending, tableSortDescendinglibs/components/size.snapshot.md — the ./table rowdocs/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 themapps/sandbox/src/app/views/datatable/ (the sandbox view). The published site: /components/datatable, its demos' source in the code tablibs/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 runRead at build time from the tracked snapshots, not typed here.
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.title, which a keyboard does not open; a reader hears the whole text, because the cut is only paint.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.Room, column header, row 1, column 2 in the walk of 2026-10-30.