-
Presets: springDefault (bounce 0, 0.4s, most UI) / springSnappy (0.3s, popovers) /
springPress (0.25s, press feedback) / springMomentum (bounce 0.2, 0.4s, drag-and-fling only)
-
Interruptibility: gesture-driven and interactive transitions use a motion spring so they resume
from the current value and can reverse at any time. Pure state toggles may stay CSS transitions.
The Reveal entrance system stays as it is
-
Press feedback is immediate: interactive elements scale to 0.97 on :active in 100-150ms, fired
on pointer-down, not on release. Button already does this at 120ms
-
Popovers, drawers, and dialogs: AnimatePresence plus popoverMotion, with transform-origin
anchored to the trigger, and the same path in and out
-
Reduced motion: AppMotionConfig sets reducedMotion="user" globally, and styles.css has the
0.01ms fallback. Transform and layout animations degrade to a crossfade automatically
-
Easing is limited to the ease-out exponential family: quart cubic-bezier(0.25, 1, 0.5, 1),
quint cubic-bezier(0.22, 1, 0.36, 1), expo cubic-bezier(0.16, 1, 0.3, 1). Decorative bounce
and elastic CSS easing are banned -- they are dated and they steal attention. Gesture-momentum
cases may use spring bounce <= 0.2 (springMomentum)
-
Exit duration is roughly 75% of entrance
-
Staggered lists are legitimate: on the landing surface stagger through Reveal's delayMs, and
cap total sequence length at 500ms. Copying a scroll reveal onto every section is an AI
reflex -- each reveal has to earn what it reveals
-
A reveal MUST enhance content that is already visible by default. Never make visibility depend on
a class-triggered transition: in a headless environment or a hidden tab it never fires and the
block renders blank
-
Motion material is not limited to transform and opacity. Blur, backdrop-filter, clip-path,
mask, and shadow are all legitimate over small areas once you have verified they stay smooth.
Never animate layout properties (width, height, top, left, margin) casually -- if you
need reflow, use grid-template-rows or FLIP
-
will-change goes only on animations you know are expensive, applied on hover or an .animating
class. Never pre-apply it globally
-
Scroll triggers always use IntersectionObserver and unobserve after the first hit
-
prefers-reduced-motion is not optional: every animation needs a degraded path (crossfade or
instant). XID's global fallback lives in styles.css; check any new component-level animation
yourself
-
Perceived performance: micro-interactions target under 80ms (the threshold for "instant").
Optimistic updates are for low-risk actions only -- never for payments or destructive operations
-
XID wiring: landing entrances reuse Reveal (IntersectionObserver plus reduced-motion already
handled). New keyframes are module-level constants. Express state with StyleX conditional values,
never descendant selectors
default / hover / focus-visible / active / disabled / loading / error / success.
A missing state is a hole in the experience, not a follow-up ticket. XID specifics: