ComponentsLayout & themingGrid
On this page
Layout & theming

Grid released

A grid that finds its own column count from the narrowest a card may get, with no media query to write.

  • Import@pacit/components/grid
  • Selectorpct-grid

No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓

One
Two
Three
Four
Five
Six

grid.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 { PctGrid } from '@pacit/components/grid';
ExportedWhat it is
PctGridcomponentpct-gridA 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>
Examples

Every state, running

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

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.

Container
Stack
Grid
Theme
Button
Tabs
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.

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).

Inputs

NameTypeWhat it does
No inputs — the element is configured by its content.

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-grid
Styling

Parts to select, tokens to override

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

Parts

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.

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-grid-gap{pct.space.5}16pxGap between the grid's cells.
--pct-grid-min-width16remMinimum width of a cell before the grid wraps.

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-grid-min-width: 12rem;
  --pct-grid-gap: 24px;
}
One
Two
Three
Four
Five
Six
Accessibility

Measured, not assumed

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.

Keyboard

keyeffecttest

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.

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/grid/src/grid.ts — including why it is not role="grid"
MeasuredKeyboard map tested key by keythe empty map is the claim: grid.spec.ts › "carries no ARIA at all" plus layout.spec.ts › "none of the three carries a single ARIA attribute", three engines
Measuredaxe audit on the component's own sandbox viewapps/sandbox-e2e/src/a11y.spec.ts — /layout in SBX_ROUTES
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — layout-grid and layout-grid-rtl: six cards, three columns at the stage width
n/aforced-colors: active — no state carried by colour alonenot applicable — grid.scss paints nothing
n/aprefers-reduced-motion — duration from a tokennot applicable — the column count changes at resize, but through layout, not animation; nothing here declares motion
n/aTouch target ≥ 24×24 px outrightnot applicable — nothing here is a target
n/aSize axis aligned to --pct-control-height-*not applicable — no height of its own
GapDensity axisnone — gap: the same one every component here has (req-token-density); the scoped --pct-grid-min-width demo is what density will one day generalise
MeasuredRTL — no physical properties + a dir="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 anyway
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /layout in SBX_ROUTES
n/aFormsnot applicable
MeasuredParts registered in the inventorylibs/components/parts.snapshot.md — a component with no parts is itself the entry
MeasuredTokens registered + an entry in contrast.policy.jsonlibs/tokens/tokens.snapshot.md — two --pct-grid-* names; no contrast entry, deliberately: no colour painted
n/aStrings through PCT_TEXTSnot applicable
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./grid row, on @angular/core alone, because the whole component is one declaration
MeasuredScreen-reader logRead 2026-09-16 from docs/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 nothing
MeasuredA docs page with live examplesapps/sandbox/src/app/views/layout/ (the sandbox view). The published site: /components/grid — prerendered, the demo's own source is the code tab
MeasuredUnit + mutationlibs/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 yet
Evidence

What the repository holds for this component

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

—mutation scoreno mutants to kill — the policy says why
7 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
0colour pairs measuredlight and dark, at every token build
0visual baselinesChromium, the sandbox's own pictures
7elements in the preview0 listeners · 1 render to settle · 1401 µs in jsdom, held exactly but for the clock

Why it is built this way

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.

Known limitations

  • Equal columns only. No spans, no featured-card-twice-as-wide, no masonry — those are layouts with opinions about content, and this grid has exactly one opinion: a minimum. A page needing spans writes its own grid-template-areas where it can see them.
  • A single child stretches full width. 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.
  • DOM order is visual order, always. The grid never reorders, so the tab sequence and the reading sequence stay the document's — which is a limitation only for designs that wanted them to differ, and those pay for it in any layout system.