You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/decisions/design-text-effects.md

8.9 KiB

title type audience authority status related
Text effects — family design reference human + agent E3 design record — why the text-effects family exists, where each member sits, and the doctrine every member obeys current
packs format
docs/architecture/packs.md (the sibling stream — decorative backgrounds went to the pack tier) 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, 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 service Spring-counts a number to a target on viewport entry, formatting every frame through uix.format.numbers.
TextGradient decorative (CSS) Animated gradient painted through the text (background-clip); colors takes a free stop list or a canonical gradient name.
TextCircular decorative (JS) Characters on a spinning circle; hover retunes the spin.
TextBlur decorative (WAAPI) Staggered blur→sharp entrance per word/letter on viewport entry.
TextFocus decorative (CSS+JS) Every word blurred except the active one; a corner frame travels to it.
TextScramble 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 <FormatNumber> — 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 is the same rule read from the other side.

Backgrounds later showed the rule cuts through the middle of a subject rather than around it. The animated EFFECT stayed in the pack; the HOST of a surface's layers — Background — entered the canon, because it owns a contract: named parts, aria-hidden layers, the pause control WCAG 2.2.2 requires of anything that moves on its own, and a token surface a theme swaps. One subject, two tiers, and the seam between them is a layer the pack mounts its scene into.

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's colors prop is discriminated by shape: a string[] is a free stop list, a bare string names a canonical gradient (--gradient-{name}, decision D-T3) — so the S9 gradient vocabulary and raw stops are the SAME input, discriminated by shape, not two rival props.

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). 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.

Powered by TurnKey Linux.