ComponentsLayout & themingContainer
On this page
Layout & theming

Container released

A reading column: content capped at a maximum width, centred, with a gutter that follows the viewport.

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

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

This column is 28rem at most — one custom property, scoped to this instance. Children can respond to the container, not the viewport.

container.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 { PctContainer } from '@pacit/components/container';
ExportedWhat it is
PctContainercomponentpct-containerA reading column: content capped at --pct-container-max-width, centred, with a gutter that follows the viewport. The first of the three layout primitives (0057), and like the other two it carries no ARIA at all — layout is presentational, and the platform's div needs no help being one. It has no inputs either: both of its lengths are tokens, and a token scoped on the element is the per-instance API.
<pct-container>
  <p>A column no wider than the reading measure.</p>
</pct-container>
Examples

Every state, running

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

A narrower column, per instance

The cap is a token, and a token is an input: override --pct-container-max-width on one element and only that column narrows — no class, no media query.

Twenty rem, for a form.

Thirty-two rem, for an article.

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 reading column: content capped at --pct-container-max-width, centred, with a gutter that follows the viewport. The first of the three layout primitives (0057), and like the other two it carries no ARIA at all — layout is presentational, and the platform's div needs no help being one. It has no inputs either: both of its lengths are tokens, and a token scoped on the element is the per-instance API.

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

Parts to select, tokens to override

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

Parts

In a test, PctContainerHarness 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-container-max-width72remMaximum width of the column.
--pct-container-padding-xclamp(16px, 4vw, 40px)Horizontal padding of the column.

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-container-max-width: 60rem;
}

This column is 28rem at most — one custom property, scoped to this instance. Children can respond to the container, not the viewport.

Accessibility

Measured, not assumed

Pattern: none — layout is presentational and this host carries no ARIA at all: no role, no label, not one aria-* attribute. The unit suite pins the absence itself, so a future "helpful" attribute is a red test. Named in the class JSDoc.

Keyboard

keyeffecttest

Empty by design: the container is not interactive and adds nothing focusable. The projected content keeps its own keyboard, untouched — there is no handler in the component to intercept anything with.

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/container/src/container.ts — "no ARIA at all", and the reason
MeasuredKeyboard map tested key by keythe empty map is the claim: container.spec.ts › "carries no ARIA at all" plus layout.spec.ts › "none of the three carries a single ARIA attribute" — no role, no aria-*, nothing focusable added, in 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 cap, the centring and the clamp are ruler readings in apps/sandbox-e2e/src/layout.spec.ts, and pixels would re-prove the same numbers blurrier. The /layout view's baseline is the grid card's
n/aforced-colors: active — no state carried by colour alonenot applicable — the component paints nothing: no colour, no border, no background stands in container.scss, so forced colours has nothing to replace
n/aprefers-reduced-motion — duration from a tokennot applicable — nothing moves
n/aTouch target ≥ 24×24 px outrightnot applicable — nothing here is a target
n/aSize axis aligned to --pct-control-height-*not applicable — a column is not a control and has no height of its own
GapDensity axisnone — gap: the same one every component here has (req-token-density)
MeasuredRTL — no physical properties + a dir="rtl" screenshotcontainer.scss is logical throughout (max-inline-size, margin-inline, padding-inline — req-token-logical, enforced by check-styles); nothing directional is drawn, so no RTL baseline of its own
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /layout in SBX_ROUTES
n/aFormsnot applicable — implements no FormValueControl
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-container-* names; the contrast policy deliberately holds no entry, because the component paints no colour
n/aStrings through PCT_TEXTSnot applicable — no text at all
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./container row, on @angular/core alone: no ./core, no config, no texts — a column is one class and one stylesheet
MeasuredScreen-reader logRead 2026-09-16 from docs/acr/at/ — three readers, whole sandbox, and the argument is now measured rather than asserted: /layout produced no stop of its own in any of the three logs. Every stop there belongs to the sandbox scaffold — Theme · panel · light · selected radio button. and its two neighbours — so a container adds nothing a Tab walk can land on. It does not prove silence to a reader READING the page, which is a different pass, and no reader announced a box where one was drawn
MeasuredA docs page with live examplesapps/sandbox/src/app/views/layout/ (the sandbox view). The published site: /components/container — prerendered, the demo's own source is the code tab
MeasuredUnit + mutationlibs/components/container/src/container.spec.ts — 2 cases; the targeted Stryker run of 2026-09-02 generates zero mutants for container.ts — a class with no logic offers nothing to break. The file stays in mutation.policy.json, so the day it grows logic the report owes a row — the day's shared full run confirmed it: zero mutants, and the snapshot owes no row to a file that generates none
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
2elements in the preview0 listeners · 1 render to settle · 1016 µs in jsdom, held exactly but for the clock

Why it is built this way

The first of the three layout primitives (0057): content capped at --pct-container-max-width, centred, with a gutter that follows the viewport through a clamp(). The host also declares container-type: inline-size, so everything projected into the column can measure against this column with a container query instead of guessing at a viewport a sidebar may have eaten half of.

Known limitations

  • No width of its own as a flex or grid item — inline-size containment reports no content to a box sized by what it holds, and the column's own auto margins absorb the free space of a flex row or a grid track, so it collapses to its gutter there (measured: 80 px of an 810 px row). The column is a block-flow element by contract: the parent states the width, the column caps it — in a flex row or a grid, a plain block wrapper is that parent. The rig holds both the collapse and the remedy (lesson-146).
  • The gutter's clamp() lives in one token. A theme overriding --pct-container-padding-x owns the whole expression — floor, slope and ceiling — not a parameter of it. Recorded in 0057's costs.
  • container-type: inline-size makes the host a query container, not a positioning ancestor. Size containment does not change what position: absolute resolves against; the two are unrelated, and the name reads scarier than what it does.
  • The cap follows the root font size. 72rem widens with a reader's larger type on purpose; a fixed-pixel column is one token override away, not a second component.
  • No nesting rule. A container inside a container simply caps twice; nothing warns, because nothing breaks — the inner one just has no effect until the outer one is wider.