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/theming/motion-guide.md

11 KiB

title type audience status source
Motion — implementation guide guide human + agent current 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 eidos-motion.md; for the design decisions & history (incl. the retired "coordinated" engine) read MOTION_SERVICE_RFC.md; for the engine API read 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

<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

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.


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

{#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:

<span {...motionAttrs('fade')}>…</span>

See components/motion/README.md.

Loop something forever

Use a loop preset on any element (loops are un-gated — they need no enter/exit lifecycle):

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

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

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:

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 node), and WebGL "ambient" backgrounds (demos/animations/background/* — a separate axis).

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

Powered by TurnKey Linux.