--- title: Text effects — family design type: reference audience: human + agent authority: E3 design record — why the text-effects family exists, where each member sits, and the doctrine every member obeys status: current related: packs: docs/architecture/packs.md (the sibling stream — decorative backgrounds went to the pack tier) format: src/uix/eidos/components/format-number/README.md (CountUp's service-component sibling) --- # Text effects — family design The text-effects family is one of the two outcomes of incorporating a large animation collection into the ecosystem. The collection split along a single axis — **what the animation is applied to**: - **Backgrounds** paint behind content and touch nothing semantic → they are decorative leaves → they went to the **pack tier** ([`architecture/packs.md`](../architecture/packs.md), the `Ambient` pack). - **Text effects** treat REAL text content the reader consumes → the accessibility surface *is* the contract surface → they are **canon** components (this document). Both streams share one provenance rule: the seed collection (`web/routes/demos/animations`) is kept intact as comparative reference and is NOT the shipped code; every component is a clean re-implementation against the ecosystem's contracts. ## The members Six components, in two roles (decision D-T1): | Component | Role | What it does | | --- | --- | --- | | [`CountUp`](../../src/uix/eidos/components/count-up/README.md) | **service** | Spring-counts a number to a target on viewport entry, formatting every frame through `uix.format.numbers`. | | [`TextGradient`](../../src/uix/eidos/components/text-gradient/README.md) | decorative (CSS) | Animated gradient painted through the text (`background-clip`), token- or preset-driven. | | [`TextCircular`](../../src/uix/eidos/components/text-circular/README.md) | decorative (JS) | Characters on a spinning circle; hover retunes the spin. | | [`TextBlur`](../../src/uix/eidos/components/text-blur/README.md) | decorative (WAAPI) | Staggered blur→sharp entrance per word/letter on viewport entry. | | [`TextFocus`](../../src/uix/eidos/components/text-focus/README.md) | decorative (CSS+JS) | Every word blurred except the active one; a corner frame travels to it. | | [`TextScramble`](../../src/uix/eidos/components/text-scramble/README.md) | decorative (JS) | Characters near the pointer flicker through a charset and settle back. | **`CountUp` is a service component**, not a text effect (D-T2): it is the animated sibling of `` — formatting is FormatNumber's job, counting is CountUp's, and both route through the same locale-aware runtime formatter. It carries no CSS recipe (the E-2.2 exception), like the rest of the service family (`Trans` / `FormatNumber` / `FormatDate` / `RelativeTime`). The five `Text*` decoratives (D-T4 naming: the `Text*` prefix groups them in the catalog beside the typographic primitives) are **eidos-native passive components** — a morfo (parts + data-attrs, no events), an eidos recipe where there is styling to own, and NO soma provider: none owns an accessible state machine, so the 2-of-3 rule leaves them at eidos-only. Their interaction (hover retune, pointer proximity) is decorative modulation of an ambient loop, not a semantic event — hence a **0-event morfo** with a passive justification, the same class as `banner` / `box`. ## Why canon and not the pack A background is invisible to the a11y tree (`aria-hidden`, pointer-transparent) — nothing in the system selects against it, so it earns the pack's exemption from the acceptance matrix. A text effect wraps content a screen reader must read and a keyboard user may reach. That is contract surface by definition: the moment an artifact owns an accessibility obligation, it enters the canon through the normal route and takes its row in `component:audit`. The [admission rule](../architecture/packs.md#the-admission-rule-canon-vs-pack) is the same rule read from the other side. ## The doctrine every member obeys The seeds were self-contained demos; bringing them into the ecosystem meant correcting the same citizenship gaps in each. These corrections ARE the design — they are what makes a "text animation" a UIX component rather than a pasted snippet: 1. **The content is the accessibility surface, always.** Effects that split text into per-character/word spans (`TextBlur`, `TextScramble`) keep the real string in a visually-hidden node and mark every animated fragment `aria-hidden` — a screen reader reads one continuous string, never letter-by-letter debris. `TextCircular` uses `role="img"` + `aria-label`. `TextGradient` leaves the text untouched (paint-only). This is an **upgrade over the seeds**, which shipped no SR surface. 2. **No fake interactivity.** `TextFocus`'s seed stamped `role="button"` + `tabindex="0"` on every word with no activatable action — a keyboard trap of phantom buttons. The component drops them: the words are presentational, the sentence reads as text. An effect only claims an interactive role when it owns a real interactive contract (which would move it up a class — see each component's Gaps). 3. **Measurement discipline.** Layout reads run coalesced post-layout via `dom.measure`, never sync-after-write. `TextScramble`'s seed measured every character's rect on every `pointermove` (a reflow storm); the component caches character centers once per layout (invalidated by `ResizeObserver`) and does a single container read per move. `TextFocus` reads the active word's rect the same way. 4. **Reduced motion is honored.** Where a seed ignored it (`TextCircular` spun forever), the component renders a static, legible state under `prefers-reduced-motion`; the others jump to the final frame or freeze. 5. **Platform access through the ecosystem.** `IntersectionObserver` and rAF arrive via `eidos.dom` (`observeIntersection` / `requestFrame`); delay and cadence timers via `eidos.timers`; the reduced-motion query via `eidos.dom.prefersReducedMotion`. No raw `setTimeout` / `setInterval` / `requestAnimationFrame` / `matchMedia`. 6. **Theme-aware color.** Color params accept eidos token names — CSS-native `var()` interpolation for the gradient stops (`TextGradient` re-tints on a mode switch with zero JS), or `var()`-wrapped custom properties for the focus frame. `TextGradient` additionally consumes the theming's named gradients (`--gradient-{preset}`, decision D-T3) so the S9 gradient vocabulary is available alongside free stops. ## Where the animation lives (per member, D-T5) Animation home follows the motion doctrine (perceptual anchoring; animation = a channel), not a single mechanism: - **CountUp** — an analytic damped-harmonic-oscillator spring evaluated in JS per frame. `$motion`'s `spring` driver animates element CSS properties, not numeric callbacks, so it does not apply; the spring is local. - **TextGradient** — pure CSS keyframes in the recipe (an ambient background-position pan), annotated `/* functional: */` per R-4.5. - **TextBlur** — WAAPI stagger from the wrapper. This is a content ENTRANCE; when the motion service's content domain lands (`src/uix/eidos/MOTION_SERVICE_RFC.md`) the stagger migrates behind it. - **TextCircular / TextScramble** — continuous JS state stepped by `eidos.dom.requestFrame`. - **TextFocus** — CSS transitions gated by `eidos.timers`. ## The Aura connection `TextCircular` is orbital text around a round shape — a candidate piece of the periphery of the future agent-presence component (reserved name `Aura`; the promotion path is in [`architecture/packs.md`](../architecture/packs.md#the-promotion-path-the-agentic-direction)). It stays presentation-only so `Aura` can compose it; the agent-lifecycle events would be declared by `Aura`'s morfo (`delegate` / `sustain`), never by `TextCircular`. ## Verification Each member self-documents (per-component README with design · usage · API · Baseline · Comparativa · Gaps · Passive justification) and ships an interactive demo at `web/routes/uix/components/{kebab}/`. The family passes the acceptance matrix (`npm run component:audit`) as six passive components. The per-effect user-facing decisions (D-T1…D-T5) were made in the session and are summarized above; their working record is the (ephemeral) execution plan `docs/process/PLAN-text-effects.md`.