ComponentsLayout & themingStack
On this page
Layout & theming

Stack released

A column of blocks standing one chosen step apart, the step taken from the spacing scale.

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

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

First
Second
Third

stack.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 { PctStack } from '@pacit/components/stack';
ExportedWhat it is
PctStackcomponentpct-stackVertical rhythm: a flex column whose blocks stand one chosen step apart, instead of each block bringing its own margin and the space between two of them being their sum. The second layout primitive (0057): no ARIA, no parts, one input.
<pct-stack gap="lg">
  <div>First</div>
  <div>Second</div>
</pct-stack>
Examples

Every state, running

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

Three rhythms

gap rides the library's shared size axis — sm, md, lg — so the space between a form's rows is the same decision as the space between a page's sections, taken once.

sm
sm
sm
md
md
md
lg
lg
lg
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.

Vertical rhythm: a flex column whose blocks stand one chosen step apart, instead of each block bringing its own margin and the space between two of them being their sum. The second layout primitive (0057): no ARIA, no parts, one input.

Inputs

NameTypeWhat it does
gapdefault 'md'PctSizeThe step of the spacing scale between blocks.

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-stack
data-pct-gapgap()
Styling

Parts to select, tokens to override

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

Parts

In a test, PctStackHarness 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-stack-gap{pct.space.5}16pxGap between the stack's children.
--pct-stack-gap-lg{pct.space.6}24pxGap between the stack's children at size lg.
--pct-stack-gap-sm{pct.space.3}8pxGap between the stack's children at size sm.

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-stack-gap: 32px;
}
First
Second
Third
Accessibility

Measured, not assumed

Pattern: none — layout is presentational and this host carries no ARIA at all (0057). The unit suite pins the absence itself. Named in the class JSDoc.

Keyboard

keyeffecttest

Empty by design: not interactive, nothing focusable added, no handler anywhere.

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/stack/src/stack.ts
MeasuredKeyboard map tested key by keythe empty map is the claim: stack.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 screenshotdeliberately none of its own: the three gaps are ruler readings — layout.spec.ts measures 8, 16 and 24 px between real boxes AND the flush first edge, which pixels would only restate
n/aforced-colors: active — no state carried by colour alonenot applicable — stack.scss paints nothing
n/aprefers-reduced-motion — duration from a tokennot applicable — nothing moves
n/aTouch target ≥ 24×24 px outrightnot applicable — nothing here is a target
MeasuredSize axis aligned to --pct-control-height-*deliberately not — the gap axis reuses the scale's three words but measures distance, not height; the reasoning is the card's own paragraph above and 0057
GapDensity axisnone — gap: the same one every component here has (req-token-density)
MeasuredRTL — no physical properties + a dir="rtl" screenshotstack.scss holds one flex-direction: column and three gaps — nothing directional exists to flip, and check-styles enforces the logical rule over the file 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 — three --pct-stack-* names; no contrast entry, deliberately: no colour painted
n/aStrings through PCT_TEXTSnot applicable
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./stack row, on @angular/core alone — the PctSize import is a type, so even the shared axis costs no ./core at runtime
MeasuredScreen-reader logRead 2026-09-16 from docs/acr/at/ — three readers, whole sandbox. As for the container and the grid: /layout produced no stop of its own in any of the three logs. A stack is a gap between children and a reader walks the children, which it did — every stop on that view is one of the scaffold controls, announced by its own group
MeasuredA docs page with live examplesapps/sandbox/src/app/views/layout/ (the sandbox view). The published site: /components/stack — prerendered, the demo's own source is the code tab
MeasuredUnit + mutationlibs/components/stack/src/stack.spec.ts — 4 cases; targeted Stryker run 2026-09-02: 1 mutant, 1 killed — 100.00 (the 'md' default swapped away dies on the default-gap case), and the day's shared full run wrote the same row into the snapshot: stack.ts 100.00 1(0) 0 0 0
Evidence

What the repository holds for this component

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

100%mutation score1 of 1 mutants killed
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
4elements in the preview0 listeners · 1 render to settle · 1010 µs in jsdom, held exactly but for the clock

Why it is built this way

The second layout primitive (0057): a flex column whose blocks stand one chosen step apart. The step comes from the spacing scale through the shared sm | md | lg axis, so rhythm is picked, not typed — and because it is a flex gap, it cannot collapse, spaces between blocks only, and leaves the first and last block flush with the host: three properties margins never manage together.

Known limitations

  • No horizontal mode. A wrapping row is a different primitive (pct-cluster), refused in 0057 until something in the repository needs it twice — the extraction law of lesson-21.
  • The scale is the only input. An arbitrary length is not a gap value; a consumer who truly needs 13px overrides --pct-stack-gap in a scope and owns the deviation.
  • Foreign margins still apply. The stack does not reset the margins its blocks bring; gap adds to them. Resetting other people's styles is a thing this library does not do anywhere, and starting here would be the surprise.