Cards that decide their own count
--pct-grid-min-width is the smallest a cell may be; the grid fits as many as the container allows and wraps the rest. No breakpoints — the container is the question.
A grid that finds its own column count from the narrowest a card may get, with no media query to write.
@pacit/components/gridpct-gridNo APG pattern applies, and none is invented — the reasoning is under Accessibility ↓
import { Component } from '@angular/core';
import { PctGrid } from '@pacit/components/grid';
/** Tiles find their own column count from one minimum width — no media query anywhere. */
@Component({
selector: 'demo-grid',
imports: [PctGrid],
// The host states its width — the stage is a flex row, and `auto-fit` under a width
// that is not definite repeats exactly once: one column, and a grid reads as a stack
// (lesson-146).
styles:
':host { display: block; inline-size: 100%; } div { border: 1px solid var(--pct-border); border-radius: 8px; padding: 12px; }',
template: `
<pct-grid style="--pct-grid-min-width: 8rem">
<div>One</div>
<div>Two</div>
<div>Three</div>
<div>Four</div>
<div>Five</div>
<div>Six</div>
</pct-grid>
`,
})
export class GridDemo {}grid.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 { PctGrid } from '@pacit/components/grid';| Exported | What it is |
|---|---|
PctGridcomponentpct-grid | A grid of equals that finds its own column count. The consumer states the one thing only they know — how narrow a card may get, --pct-grid-min-width — and the browser derives every "breakpoint" from it: columns drop one by one as the space runs out, down to a single column that never overflows. Responsive without a media query in consumer code, which is the third layout primitive's whole pitch (0057). |
<pct-grid>
<div>One</div>
<div>Two</div>
<div>Three</div>
</pct-grid>Each example is the component itself, not a screenshot. Open the code under any of them.
--pct-grid-min-width is the smallest a cell may be; the grid fits as many as the container allows and wraps the rest. No breakpoints — the container is the question.
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
A grid of equals that finds its own column count. The consumer states the one thing only they know — how narrow a card may get, --pct-grid-min-width — and the browser derives every "breakpoint" from it: columns drop one by one as the space runs out, down to a single column that never overflows. Responsive without a media query in consumer code, which is the third layout primitive's whole pitch (0057).
| Name | Type | What it does |
|---|---|---|
| No inputs — the element is configured by its content. | ||
(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-grid |
Two ways in, both stable. No class names to depend on.
In a test, PctGridHarness 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.
No parts of its own — the element you write is the whole of it.
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-grid-gap{pct.space.5}16px | Gap between the grid's cells. |
--pct-grid-min-width16rem | Minimum width of a cell before the grid wraps. |
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-grid-min-width: 12rem;
--pct-grid-gap: 24px;
} Pattern: none — layout is presentational and this host carries no ARIA at all (0057). In particular it is not role="grid": that role names a keyboard-walked widget of cells, and calling a card layout a grid widget is a claim screen-reader users pay for. The unit suite pins the absence. Named in the class JSDoc.
| key | effect | test |
|---|
Empty by design: not interactive, nothing focusable added. The cards keep their own keyboard, in DOM order — which is also the visual order, because the grid never reorders.
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/grid/src/grid.ts — including why it is not role="grid"grid.spec.ts › "carries no ARIA at all" plus layout.spec.ts › "none of the three carries a single ARIA attribute", three enginesapps/sandbox-e2e/src/a11y.spec.ts — /layout in SBX_ROUTESapps/sandbox-e2e/src/visual.spec.ts — layout-grid and layout-grid-rtl: six cards, three columns at the stage widthforced-colors: active — no state carried by colour alonenot applicable — grid.scss paints nothingprefers-reduced-motion — duration from a tokennot applicable — the column count changes at resize, but through layout, not animation; nothing here declares motion--pct-control-height-*not applicable — no height of its ownreq-token-density); the scoped --pct-grid-min-width demo is what density will one day generalisedir="rtl" screenshotgrid flow is writing-mode aware on its own — the component writes no direction anywhere, and the layout-grid-rtl baseline proves the flow flips anywayNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /layout in SBX_ROUTESlibs/components/parts.snapshot.md — a component with no parts is itself the entrycontrast.policy.jsonlibs/tokens/tokens.snapshot.md — two --pct-grid-* names; no contrast entry, deliberately: no colour paintedPCT_TEXTSnot applicablelibs/components/size.snapshot.md — the ./grid row, on @angular/core alone, because the whole component is one declarationdocs/acr/at/ — three readers, whole sandbox. The same measurement as the container beside it: /layout gave no stop of its own to any reader, only the scaffold radio groups, so nothing a grid draws is a stop of the Tab key. A grid here is a column count in CSS and the logs agree by holding nothingapps/sandbox/src/app/views/layout/ (the sandbox view). The published site: /components/grid — prerendered, the demo's own source is the code tablibs/components/grid/src/grid.spec.ts — 2 cases; the targeted Stryker run of 2026-09-02 generates zero mutants for grid.ts — the whole component is one CSS declaration, and CSS is what the e2e ruler measures instead. In mutation.policy.json regardless — the day's shared full run confirmed the zero, so the snapshot owes it no row yetRead at build time from the tracked snapshots, not typed here.
The third layout primitive (0057), and the whole of it is one declaration: repeat(auto-fit, minmax(min(100%, var(--pct-grid-min-width)), 1fr)). The consumer states the one thing only they know — how narrow a card may get — and the engine derives every "breakpoint" from it. Responsive without a media query in consumer code, or in this component's own: the e2e suite watches the column count change with the viewport while the stylesheet names no width anywhere.
grid-template-areas where it can see them.auto-fit collapses the empty tracks, so one card in a wide grid is one wide card. That is the declaration's honest reading, recorded so nobody files it as a bug.