ComponentsLayout & themingHero
On this page
Layout & theming

Hero released

The library's one loud face, on the element that is already there: a rim, a word or a surface.

  • Import@pacit/components/hero
  • Selector[pctHero]

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

A rim
A surface

A word

hero.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 { PctHero } from '@pacit/components/hero';
ExportedWhat it is
PctHerocomponent[pctHero]The brand gradient as equipment — an attribute component on the element the consumer already has, exactly as pctButton dresses a <button> (0065).
PctHeroFacetype'edge' | 'text' | 'fill'Which surface of the element takes the brand gradient. One face per element and not three switches: gradient text on a gradient fill is the one combination nobody can read, and a single input refuses it by construction rather than by documentation (0065).
PctHeroShowtype'always' | 'interact'When the face paints: always, or only while the element is under attention.
<article pctHero="edge" show="interact">…</article>
<h3 pctHero="text">Tabs</h3>
<div [pctHero]="featured() ? 'fill' : null">…</div>
Examples

Every state, running

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

When it paints

show="interact" is hover OR :focus-visible, never hover alone — a treatment only a mouse can summon is one a keyboard cannot, and this library does not ship those. On a device with no hover the pair falls to focus and a press. Tab to the card to see it.

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.

The brand gradient as equipment — an attribute component on the element the consumer already has, exactly as pctButton dresses a <button> (0065).

Inputs

NameTypeWhat it does
pctHerodefault nullPctHeroFace | nullThe face, or null for none — so a condition needs no API of its own: the input takes a binding, and a signal already says what a second input would have said again.
showdefault 'always'PctHeroShowalways, or only under hover and :focus-visible — never hover alone, because a treatment only a mouse can summon is one a keyboard cannot.
pauseddefault falsebooleanStops the sweep where it stands, for a page that wants it stopped.

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-hero
data-pct-heropctHero()
data-pct-showpctHero() ? show() : null
data-pct-pausedpaused() ? "" : null
Styling

Parts to select, tokens to override

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

Parts

In a test, PctHeroHarness 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.

No tokens of its own — it paints from the semantic tier.

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-hero: #0f766e;
  --pct-hero-via: #0e7490;
  --pct-hero-to: #1d4ed8;
  --pct-hero-text: #115e59;
  --pct-hero-text-via: #155e75;
  --pct-hero-text-to: #1e40af;
}
A rim
A surface

A word

Accessibility

Measured, not assumed

Pattern: none — the face is paint (0065): it adds no role, no state and nothing audible, and the element under it keeps whatever it already was. Named in the class JSDoc.

Keyboard

keyeffecttest

Empty by design: the face takes no focus and answers no key. show="interact" reacts to :focus-visible on the element the consumer made focusable — the treatment follows attention, it does not create it.

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/hero/src/hero.ts
MeasuredKeyboard map tested key by keythe empty map is the claim — the component adds no focusable node and no handler; what it reads is :focus-visible on the consumer's own element, measured in apps/sandbox-e2e/src/hero.spec.ts
Measuredaxe audit on the component's own sandbox viewapps/sandbox-e2e/src/a11y.spec.ts — /hero in SBX_ROUTES. Worth saying plainly: axe cannot judge the text face, because text clipped from a background carries color: transparent and the tool reports that it cannot tell — the contrast policy is what holds it
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — hero-faces
Measuredforced-colors: active — no state carried by colour aloneapps/sandbox-e2e/src/forced-colors.spec.ts — all three faces drop their gradient (an image survives the forcing) and the word comes back as CanvasText
Measuredprefers-reduced-motion — duration from a tokenapps/sandbox-e2e/src/preferences.spec.ts — the drift is a DIVISION of the motion axis (calc(var(--pct-motion-drift-duration) / 2)), so 0s / 2 is 0s and the sweep freezes with everything else
MeasuredSC 2.2.2 — motion that ends, and a stop for the pageapps/sandbox-e2e/src/hero.spec.ts — the sweep runs ONE pass of four seconds and stands still, read as movement (the position moves early, and is the same value at 5.2 s and at 6 s); paused freezes it where it is and lets it go again. The three mechanisms are ordered: prefers-reduced-motion is the reader's and wins, paused is the page's, and the settle is what happens when neither has spoken
n/aTouch target ≥ 24×24 px outrightnot applicable — the face is paint on the consumer's element and adds no target
n/aSize axis aligned to --pct-control-height-*not applicable — a treatment 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" screenshotlibs/components/hero/src/hero.scss — one pct-exception for background-position, the drift's closed loop, which has no writing direction; the rest is logical or geometry-free
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /hero in SBX_ROUTES
n/aFormsnot applicable — paint holds no value
By designParts registered in the inventorynone — deliberately: the faces are the host and its ::after, and a pseudo-element is not addressable from a test; tools/check-parts.mjs counts zero
MeasuredTokens registered + an entry in contrast.policy.jsonlibs/tokens/src/contrast.policy.json — six entries: the three stops as a WORD against surface at AA, and the three as an EDGE at UI; libs/tokens/tokens.snapshot.md
n/aStrings through PCT_TEXTSnot applicable — the component draws no text of its own
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./hero row
MeasuredScreen-reader logRead 2026-10-08 from docs/acr/at/ — three readers, whole sandbox. The whole card arrives in one announcement, heading included: VoiceOver reads link heading level 3 Under attention The rim is drawn and hidden, so the reveal is an opacity. and Orca the same text as a visited link. NVDA adds where it goes — Read the case, link, visited, same page. So the hero is one link to a reader, with its heading and its body inside the name, which is what the pattern promises
MeasuredA docs page with live examplesapps/sandbox/src/app/views/hero/ (the sandbox view). The published site: /components/hero — prerendered, the demo's own source is the code tab
MeasuredUnit + mutationlibs/components/hero/src/hero.spec.ts — 4 cases. hero.ts is in libs/components/mutation.policy.json's inventory from this step, and its row in libs/components/mutation.snapshot.md is owed: the run that writes it is the next full one
Evidence

What the repository holds for this component

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

100%mutation score2 of 2 mutants killed
8 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
6colour pairs measuredlight and dark, at every token build
1visual baselinesChromium, the sandbox's own pictures
3elements in the preview0 listeners · 1 render to settle · 2436 µs in jsdom, held exactly but for the clock

Known limitations

  • No pctHero on another component's host. Two components cannot share an element, so a hero-faced button is variant="hero" — which the button has — and a component that wants the face grows its own variant. That is the honest path rather than a workaround: a variant brings its own contrast entries and its own card, a face bolted on from outside brings neither.
  • The text face is invisible to axe. Text painted through background-clip carries color: transparent, so the tool reports that it cannot tell rather than that it is wrong. The six policy entries are what make the claim measurable at all.
  • An engine without mask-composite gets no rim. The rule stands behind @supports, and deliberately: the failure mode of a missing feature has to be nothing, not a gradient painted over the whole element.
  • The face cannot verify what it paints. pctHero="text" on a 12 px word is legal CSS and a contrast failure the component cannot see; the policy measures the stops, not the type size the consumer chose.