An island that switches
The directive writes one attribute; the tokens under it do the rest. Bind it to a signal and a whole region of the page changes theme without touching the document.
This island is dark.
The theme as an attribute: light, dark, or the system's own answer.
@pacit/components/theme[pctTheme]No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓
A dark island: one attribute, every token below it flips.
import { Component } from '@angular/core';
import { PctButton } from '@pacit/components/button';
import { PctTheme } from '@pacit/components/theme';
/** The directive only spells the attribute — the cascade does the theming. */
@Component({
selector: 'demo-theme',
imports: [PctButton, PctTheme],
styles:
'section { background: var(--pct-surface); color: var(--pct-text); border: 1px solid var(--pct-border); border-radius: 8px; padding: 16px; display: grid; gap: 8px; justify-items: start; }',
template: `
<section pctTheme="dark">
<p>A dark island: one attribute, every token below it flips.</p>
<button pctButton variant="soft" size="sm">Reads the island</button>
</section>
`,
})
export class ThemeDemo {}theme.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 { PctTheme } from '@pacit/components/theme';| Exported | What it is |
|---|---|
PctThemeNametype'light' | 'dark' | The two themes the shipped skin keys its blocks on. |
PctThemedirective[pctTheme] | Theme as a template binding: [pctTheme]="'dark'" writes data-theme="dark" on the host — the attribute the generated skin keys its theme blocks on — and null removes it, so the system preference speaks again (req-token-system). |
<section pctTheme="dark">
<p>A dark island: every token below it flips.</p>
</section>Each example is the component itself, not a screenshot. Open the code under any of them.
The directive writes one attribute; the tokens under it do the rest. Bind it to a signal and a whole region of the page changes theme without touching the document.
This island is dark.
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
Theme as a template binding: [pctTheme]="'dark'" writes data-theme="dark" on the host — the attribute the generated skin keys its theme blocks on — and null removes it, so the system preference speaks again (req-token-system).
| Name | Type | What it does |
|---|---|---|
pctThemerequired | PctThemeName | null | The theme to pin on this subtree, or null to follow the page. |
(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 |
|---|---|---|
data-theme | pctTheme() |
Two ways in, both stable. No class names to depend on.
In a test, PctThemeHarness 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.
No tokens of its own — it paints from the semantic tier.
Pattern: none — the directive writes one data-* attribute and touches nothing the accessibility tree carries. Named in the class JSDoc.
| key | effect | test |
|---|
Empty by design: not interactive, adds nothing focusable, intercepts nothing.
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/theme/src/theme.tstheme.spec.ts › "writes nothing else" — one attribute is the whole surfaceapps/sandbox-e2e/src/a11y.spec.ts — the directive drives the demo stage on every view of the sweep, and the kitchen-sink's directive panel sits in /allvisual.spec.ts is already a picture of its effectforced-colors: active — no state carried by colour alonenot applicable — it paints nothingprefers-reduced-motion — duration from a tokennot applicable — nothing moves--pct-control-height-*not applicabledir="rtl" screenshotnot applicable — no stylesheet at allNG05xxapps/sandbox-e2e/src/hydration.spec.ts — the stage carries the directive on every route of the sweeplibs/components/parts.snapshot.md — a component with no parts is itself the entrycontrast.policy.jsonnot applicable — no tokens of its own; the pairs it switches between are the skin's, measured per theme by every tokens:buildPCT_TEXTSnot applicablelibs/components/size.snapshot.md — the ./theme row, on @angular/core alone: one host binding is one host bindingdocs/acr/at/ — three readers, whole sandbox, and it is measured on every view rather than on one: the scaffold carries a theme switch to all thirty-six, and two of the three read the CONTROL while the third names only the box around it: Orca Theme · panel · light · selected radio button. and VoiceOver light selected radio button, 1 of 2 Theme radio group both give the option and its state, while NVDA gives Theme, grouping at every one of its stops and says neither light nor selected — a user arriving there is told a group exists and not which theme is on. The theme itself is never announced: no reader said a word when the attribute changed the colours, which is the promise hereapps/sandbox/src/app/views/kitchen-sink/ (the /all view: the raw panel and the directive panel side by side). The published site: /components/theme — prerendered, the demo's own source is the code tablibs/components/theme/src/theme.spec.ts — 3 cases; in mutation.policy.json from day one — the day's shared full run generated zero mutants for one host binding, so the snapshot owes it no row until the directive grows logicRead at build time from the tracked snapshots, not typed here.
Sugar over the cascade (0059): [pctTheme]="'dark'" writes data-theme="dark" — the attribute the generated skin keys its theme blocks on — and null removes it, so the system preference speaks again. The mechanism stays the cascade itself; the sandbox measures a hand-written panel and a directive panel reading the same computed surface.
data-theme by hand — a bootstrap component has no template around it to bind an input on. One writer the directive cannot replace (0059).'light' | 'dark' mirrors the blocks the skin actually ships; a third theme is a token-build feature first, and the union widens the day the build emits it.'system' value. Following the system is the absence of the attribute, so it is spelled null — a 'system' string would be a third state pretending the attribute can express it.