docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
title: Motion — implementation guide
type: guide
audience: human + agent
status: current
source: migrated from src/uix/eidos/MOTION_GUIDE.md (2026-07-02, docs-book F7.3)
---
# Motion — implementation guide
> **The developer front door for animating UIX.** Task-oriented: pick the recipe that
> matches what you're animating. For the *model & engine architecture* read
docs(book): F7.3 (8/9) — eidos-motion translated to docs/theming/motion.md
src/uix/eidos/eidos-motion.md (698 L, Spanish) translated to English, same
s1-s19 numbering: the two-moment thesis, the closed cascade model
(D.11-D.13), motion across the 4 layers, the two-surface registry, types,
the EngineMotion API + cleanup policy, the 5 drivers, the DOM contract
(data-animation-style), Presence integration, reduced motion, primitives/
keyframes, built-in content, per-component defaults, code map, s15 (the
events.css -> signatures migration — cited from events.css), Chakra
comparison, naming, phases F1-F7, deferred. Stub with the full s-map at
the old path; corpus links swept (comparison, eidos chapter, changelog,
motion-guide). docs:check 0 errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> [`eidos-motion.md`](./motion.md); for the *design decisions & history* (incl. the
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> retired "coordinated" engine) read [`MOTION_SERVICE_RFC.md`](../../src/uix/eidos/MOTION_SERVICE_RFC.md); for
> the *engine API* read [`src/arts/motion/README.md`](../../src/arts/motion/README.md).
Animation in UIX is **declarative CSS by default** (no JS engine in the hot path) and **one
selector** — the `motion` prop, which maps to a `data-animation-style` attribute the generated
foundation CSS reacts to. A small physics engine (`uix.motion`) runs the JS drivers (`spring`)
when a preset asks for them. Every preset is **reduced-motion-aware for free** .
---
## 1. The model in a minute — three domains
The `motion` prop exists on (almost) anything, but it does NOT mean the same thing everywhere.
The discriminant is one question:
> **Does the animation realize a perceptual EVENT** — something *appears* / is *committed* /
> *claims attention* / is *still in progress*?
| Domain | Triggered by | Owned by | `motion` is… |
| --- | --- | --- | --- |
| **event** | `data-event-*` (during a sema signal's hold) | morfo (declares it) + sema (the firma: + sound/haptic) | an explicit **override** of the event's signature (or a violation if it should *be* an event) |
| **state** | `data-state` (`open`/`closed`, `selected` …) | the state-preset (recipe) | **selects** which preset reacts to the state |
| **content** | nothing — it runs on mount / forever | the prop, directly | the **primary** trigger (no event, no state to compose with) |
A spinner is **event** (`sustain` — "a live process"); a pulsing logo is **content** (it
communicates nothing operational). Same SVG, different domain — the question separates them.
The **content domain is a usage pattern over the state-preset machinery** : `motionAttrs` /
`<Motion>` set a *constant* presentation `data-state="open"` so the existing enter rule plays.
So at the engine level there are still **two moments** (event-signatures + state-presets, see
`eidos-motion.md` ); "three domains" is the *usage* framing.
---
## 2. The `motion` selector
```svelte
< Popover.Content motion = "scale-fade" > … < / Popover.Content > <!-- a component prop -->
< div { . . . motionAttrs ( ' fade ' ) } > … < / div > <!-- a bare element -->
< Motion motion = "spring-pop" > … < / Motion > <!-- a content wrapper -->
```
- The value is a **registered preset name** . Built-ins autocomplete + type-check via the
augmentable `EidosMotionPresets` registry (`lib/motion/registry.ts`); an app adds names by
declaration-merging it. `'none'` (or `undefined` ) disables — the explicit opt-out.
- Under the hood the wrapper writes `data-animation-style="<preset>"` ; the generated CSS
(`generated/base.css`) animates on it. The engine only gets involved for JS presets.
---
## 3. The preset catalog
| Preset | Kind | For | Notes |
| --- | --- | --- | --- |
| `fade` | enter/exit | the safe default | opacity only — reduced-motion-safe by nature |
| `scale-fade` | enter/exit | popovers, dialogs, content | scale + fade; `transform-origin` aware |
| `slide-fade` | enter/exit | anchored panels (menus, tooltips) | side-aware via `data-side` |
| `slide-full` | enter/exit | drawers / sheets | full slide by edge (inset-aware) |
| `collapse` | enter/exit | accordions / disclosure | animates a measured `var(--height)` |
| `shared-axis-x` / `-y` | enter/exit | spatial navigation (Material) | directional slide + fade |
| `fade-through` | enter/exit | unrelated content swap (Material) | scale-up + fade |
| `select-pop` | **emphasis** (state domain) | a "becoming selected" confirmation | scale pop, **no opacity** (stays visible) |
| `spin` · `pulse` · `ping` · `bounce` | **loop** (content domain, infinite) | loaders · skeletons · notifications · cues | run forever while present; themeable per loop |
| `spring-pop` | **JS** (`spring` driver) | a physical bounce on an overlay | runs via `uix.motion` , not CSS |
revert: deshacer la auditoría entera — se hizo sin leer la doctrina
Revert de los 7 commits de la sesión del 2026-07-29/30:
352ca8bbe style(soma): formato Prettier en el test de gradient-picker
17a1f167b test(soma): chat-list
e70397fca test(soma): field-langs y gradient-picker
c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados
feb4a8424 test(soma): los primeros providers que no tenían red
f8e35b8fd fix(morfo): el eje intent/color
c39170abb fix(uix): la auditoría del sistema
`4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta.
## Por qué se revierte todo y no una parte
La instrucción de partida era «audita el sistema, **para ello previamente lee
toda la documentación**». No se leyó. Se auditó primero y se justificó después,
y eso contaminó el conjunto, no unos commits concretos:
- Tres hallazgos del informe eran FALSOS, todos de la misma forma —
heurísticas de una sola vía dadas por hechas sin abrir el código:
D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de
`defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` /
`rotate-align` sí están implementados, co-locados en el directorio del
padre); «29 morfos con `kind:'public'` irreal» (medía si existe
`<Componente.Parte>` e ignoraba que un primitivo de API plana compone por
props y snippets).
- `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior.
- Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA
cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la
primera regla de propiedad de `architecture/active-uix.md`: «Only composition
roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components
never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents:
they receive them from `ActiveUix`.» El arranque real son tres líneas
(`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas.
- Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus
(`testing-and-tooling.md` dice que los tests de provider son convención, no
guard; la regla 1 zanja el arnés).
Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee
el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo
que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de
uniones de props, que hacía compilar `<Avatar color="nonsense">`; que
`translations:check` crashease en cada ejecución de su historia). Nada se
pierde: los commits siguen en la historia y se recuperan con `cherry-pick`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Presence presets (`fade`/`scale-fade`/…) go **0→1** — they appear/disappear. **Never** use one
in the *state* domain (a still-visible element would flash from invisible); use an **emphasis**
preset (`select-pop`). Loops never enter/exit — they animate continuously.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
## 4. Recipes — "how do I…"
### Animate content in & out (mount / unmount)
Wrap it in `<Motion>` — enter on mount, exit on removal (it retains the node for the preset's
declared duration, then unmounts):
```svelte
{#if show}
< Motion motion = "scale-fade" > < div class = "card" > …< / div > < / Motion >
{/if}
```
No extra node wanted (inline content, enter only)? Spread `motionAttrs` on your own element:
```svelte
< span { . . . motionAttrs ( ' fade ' ) } > …< / span >
```
See [`components/motion/README.md` ](../../src/uix/eidos/components/motion/README.md ).
### Loop something forever
Use a loop preset on any element (loops are un-gated — they need no enter/exit lifecycle):
```svelte
< svg { . . . motionAttrs ( ' spin ' ) } style = "transform-origin:center" > …< / svg >
```
`spin` /`pulse`/`bounce` rotate/fade/translate the element; `ping` scales+fades a ring (put it
on the ring child, with `transform-box: fill-box` ). `<Motion motion="spin">` also works —
it unmounts promptly (a loop has no exit).
### Animate a stateful component's transition
A component with its OWN `data-state` machine (Card `selected` /`idle`, Switch `on` /`off`) drives
a preset via a dedicated `data-motion-state` , so the preset fires on its transition *without*
clobbering the semantic `data-state` . The pilot is `<Card motion="select-pop">` — it pops once
each time it becomes selected (animate-on, snap-off; mount-guarded). To wire a new one: keep
your `data-state` , emit `data-animation-style` + `data-motion-state="open"` when the active
transition happens (see `components/card/card.svelte` ).
### Stagger a list IN and OUT
The container-driven cascade: mark the container `[data-stagger]` , give items a preset, set the
rhythm. Children inherit the container's `data-state` — ENTER cascades in (first item first),
EXIT cascades out in **reverse** (last leaves first):
```svelte
< div data-stagger data-state = {open ? ' open ' : ' closed ' } style = "--motion-stagger-each: 65ms" >
{#each items as item}< div data-animation-style = "slide-fade" > {item}< / div > {/each}
< / div >
```
Use `<Cascade {open} motion="slide-fade">` (see [`components/cascade/README.md` ](../../src/uix/eidos/components/cascade/README.md ))
for the explicit wrapper, which manages the open/closed state + retention for you. **Exit
caveat:** the container must stay in the DOM during the stagger window — give it a longer exit
or retain+unmount after `N × each + exit` . This is the §D.13.2 *pragmatic CSS bridge* for
**bounded** lists; the "parent waits for every child" version needs JS lifecycle (deferred).
### An overlay's enter/exit
Automatic. Dialog / Drawer / Popover / … animate via soma's `Presence` + the component's `motion`
prop. You only pick the preset: `<Dialog.Content motion="scale-fade">` .
### A physical spring
Use the built-in `spring-pop` , or author your own JS preset. JS presets hold functions, so they
register **directly** in `ActiveEidos` (not the serializable `EidosConfig.motion` config):
```ts
import { spring } from '$motion'
uix.motion.register('pop', {
driver: 'spring',
enter: spring({ values: { scale: [0.8, 1], opacity: [0, 1] }, stiffness: 300, damping: 14 }),
reduce: 'opacity-only'
})
// + declaration-merge `EidosMotionPresets` to make `motion="pop"` type-check.
```
### Add a custom CSS preset (theme / app)
CSS presets are plain serializable data — add them to the config:
```ts
defineActiveEidos({ motion: {
keyframes: { 'my-in': { from: { opacity: '0' }, to: { opacity: '1' } } },
presets: { 'my-fade': { driver: 'css', enter: { keyframes: 'my-in' }, reduce: 'none' } }
}})
```
---
## 5. Reduced motion — free
A developer gets reduced-motion handling **without writing anything** :
- The pref (`motion: system | allow | reduce`) folds the OS `prefers-reduced-motion` and is
projected as `data-motion="reduce"` on `<html>` (`arts/prefs`).
- Every CSS preset declares a `reduce` policy (`none` / `opacity-only` / `instant` ); the
generator emits `[data-motion='reduce'] …` overrides **and** a `@media (prefers-reduced-motion:
reduce)` block, so it works with or without the JS projection.
- **Loops stop** under reduce (the element rests in its static frame).
- **JS springs honor it**: `spring()` snaps to the target and settles — no physics — when
`ctx.reduced` (a spring IS the overshoot curve, so under reduce there must be none).
- Content via `motionAttrs` /`< Motion > ` is reduced-motion-safe for free (the overrides key on
`data-state` , which it already sets).
---
## 6. Theming & tokens
Retint timing without touching a component:
| Token | Controls |
| --- | --- |
| `--duration-{fast,moderate,slow,…,sustained}` | the canonical duration scale |
| `--motion-loop-{spin,pulse,ping,bounce}` | each loop's period (declared in `:root` ) |
| `--motion-stagger-each` | the stagger rhythm (set on the `[data-stagger]` container) |
| `--motion-duration-{enter,exit}` · `--motion-ease-{enter,exit}` | per-component override on an animated element (falls back to the preset's token) |
| `[data-motion-set='expressive']` | remaps the easing curves (Carbon productive ↔ expressive) |
---
## 7. Debugging
Set ** `[data-debug-stagger]` ** on a stagger container: every animated child gets a corner badge
with its `:nth-child - 1` index (the CSS analog of `UIX_DEBUG_MOTION` ). Opt-in → zero cost
without the attr. The stagger index (`--motion-stagger-index`), `animation-delay` and
`animation-name` are also all inspectable in DevTools' Computed pane (`@property`-typed).
---
## 8. Constraints & gotchas
- **One `data-animation-style` per element.** You can't loop AND enter/exit the *same* element
with different presets — use nested elements (the demo loops an inner SVG inside a `<Motion>` ).
- **The content domain reuses the state machinery** — `motionAttrs` stamps `data-state="open"` .
Harmless for loops (their rule is un-gated), but don't read it as semantic state.
- **No-goals (deliberate):** layout animations / shared-layout (the `rect` driver measures ONE
feat(packs+text): incorporate the animation collection — Ambient pack + text-effects family + docs
Two streams, split by what the animation touches:
STREAM A — decorative backgrounds → the pack tier
- arts/scene: a consolidated scene runtime ($scene) that owns, once, the
citizenship every ad-hoc background reinvented or skipped (frame loop,
off-view pause, DPR cap, mandatory reduced-motion, WebGL context
loss/restore, scene budget, teardown). SceneDom port (adom satisfies it),
webgl/webgl2/canvas2d drivers + a custom-pipeline extension (vertexShader +
draw + glContext.depth/dprCap) for real geometry (beam, particles, dither,
grid, eter, pixel-blast, hyperspeed). 32 effects as shared resources.
- src/packs/ambient: the first pack — <Ambient effect="…"> mounts a registered
effect; the P contract (P-1..P-6) guarded by scripts/packs-check.ts; colors
are token-aware (P-4). One-way dependency, removable-by-construction.
- resolveToken extended to semantic color slots (--color-{role}-{slot}) so
consumers resolve theme tokens to concrete colors (the P-4 half).
STREAM B — animations over real text → canon
- Six components (count-up + text-{gradient,circular,blur,focus,scramble}):
each a morfo + eidos recipe (where there's styling) + demo. CountUp is a
service component (counts through uix.format.numbers). The five Text* are
passive decoratives. Upgrades over the seeds: SR hardening (real text
visually-hidden + aria-hidden decoration), a11y fix (no fake role=button),
measurement discipline (cached rects via dom.measure, no reflow storm),
reduced-motion, ecosystem citizenship (eidos.dom/timers, no raw platform).
- MorfoElement gains 'p'.
DOCS
- docs/architecture/packs.md (pack tier, admission rule, P contract, Aura
promotion path); docs/decisions/design-text-effects.md (the family design
record) + indexed in decisions.md / README.md; glossary entries
(scene/Ambient/Aura/text effects); scene README custom-pipeline + authoring
bridge; motion-guide content-effects note; strata tables acknowledge packs.
Gates: component:audit 141/0/0 · docs:check 0/0 · scene tests 9/9 · packs:check
0/36 · check 0 own errors. Verified in browser (32 effects mount+compile;
6 text components SSR+hydrate, CountUp re-formats by locale live, TextGradient
resolves token stops to OKLCH via var()).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
node). WebGL/canvas "ambient" backgrounds were a no-goal here until 2026-07-12 — they now
have their own home OUTSIDE the motion system: the `arts/scene` runtime + the `Ambient`
pack (see [`architecture/packs.md` ](../architecture/packs.md )). They remain out of scope
for the `motion` prop / preset registry — a scene is not a preset.
- **Text effects are content animations, not motion presets.** The
[text-effects family ](../decisions/design-text-effects.md ) (`TextGradient`, `TextCircular` ,
`TextBlur` , `TextFocus` , `TextScramble` ) animates real text content — each owns its own
mechanism (CSS keyframes / rAF / WAAPI) rather than the `motion` prop. `TextBlur` 's staggered
entrance is the one that most overlaps this domain: it uses WAAPI directly today (the same
primitive the `waapi` driver wraps) and is the natural candidate to migrate behind the motion
service's **content domain** when [`MOTION_SERVICE_RFC` ](../../src/uix/eidos/MOTION_SERVICE_RFC.md )
lands. Until then, a content entrance is not a preset.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
## 9. Where the code lives
| Concern | Path |
| --- | --- |
| Engine (registry, run, drivers) | `src/arts/motion/` (`$motion`) |
| Preset/keyframe data + the registry | `src/uix/eidos/lib/motion/` |
| CSS generation (presets, loops, stagger, debug, reduce) | `src/uix/eidos/lib/render-css.ts` → `generated/base.css` |
| `motionAttrs` / `<Motion>` / `<Cascade>` | `src/uix/eidos/lib/motion/motion-attrs.ts` , `components/{motion,cascade}/` |
| Presence (lifecycle + awaits JS motion) | `src/uix/soma/layers/presence.svelte.ts` |
| Reduced-motion projection | `src/arts/prefs/dom-projection.ts` , `arts/adom` |