On this page
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 word
import { Component } from '@angular/core';
import { PctHero } from '@pacit/components/hero';
/**
* The three faces
*
* `pctHero` names which surface takes the brand gradient — a rim, a word, or the surface
* itself. One face per element: two components cannot share a host, so gradient text on a
* gradient fill is unrepresentable rather than discouraged.
*/
@Component({
selector: 'demo-hero',
imports: [PctHero],
// The class is `face` and not `card`, measured: the gallery's own overflow check walks
// `document.querySelectorAll('.card')` across the whole page, so a demo that spells its
// box `card` hands that check a tile with no stage and no name inside it.
styles: `
:host {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}
.face {
box-sizing: border-box;
padding: 12px 16px;
border: 1px solid var(--pct-border);
border-radius: var(--pct-radius-lg);
background: var(--pct-surface);
}
.panel {
box-sizing: border-box;
padding: 12px 16px;
border-radius: var(--pct-radius-lg);
font-weight: 600;
}
.word {
margin: 0;
font-size: 28px;
font-weight: 800;
letter-spacing: -0.02em;
}
`,
template: `
<article class="face" pctHero="edge">A rim</article>
<div class="panel" pctHero="fill">A surface</div>
<p class="word" pctHero="text">A word</p>
`,
})
export class HeroDemo {}hero.demo.ts — the same file runs here and is highlighted under Code
Usage
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctHero } from '@pacit/components/hero';| Exported | What 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>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.
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
| Name | Type | What it does |
|---|---|---|
pctHerodefault null | PctHeroFace | null | The 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' | PctHeroShow | always, or only under hover and :focus-visible — never hover alone, because a treatment only a mouse can summon is one a keyboard cannot. |
pauseddefault false | boolean | Stops the sweep where it stands, for a page that wants it stopped. |
Outputs
(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.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-hero | |
data-pct-hero | pctHero() | |
data-pct-show | pctHero() ? show() : null | |
data-pct-paused | paused() ? "" : null |
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 word
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
| key | effect | test |
|---|
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.
libs/components/hero/src/hero.ts:focus-visible on the consumer's own element, measured in apps/sandbox-e2e/src/hero.spec.tsapps/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 itapps/sandbox-e2e/src/visual.spec.ts — hero-facesforced-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 CanvasTextprefers-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 elseapps/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--pct-control-height-*not applicable — a treatment has no height of its owndir="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-freeNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /hero in SBX_ROUTES::after, and a pseudo-element is not addressable from a test; tools/check-parts.mjs counts zerocontrast.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.mdPCT_TEXTSnot applicable — the component draws no text of its ownlibs/components/size.snapshot.md — the ./hero rowdocs/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 promisesapps/sandbox/src/app/views/hero/ (the sandbox view). The published site: /components/hero — prerendered, the demo's own source is the code tablibs/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 oneWhat the repository holds for this component
Read at build time from the tracked snapshots, not typed here.
Known limitations
- No
pctHeroon another component's host. Two components cannot share an element, so a hero-faced button isvariant="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
textface is invisible to axe. Text painted throughbackground-clipcarriescolor: 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-compositegets 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.