Xzero UI
Pro library

motion

Use the public xzero-ui MCP server at https://mcp.xzero.co.uk/mcp. The complete Free and Pro library is available through this service. No account, sign-in, token, or upstream fallback is required. If the service is unavailable, retry later; do not use upstream services.

Motion shows cause and effect: what opened, what changed, where it came from, what is ready next. If an animation explains nothing, remove it.

Contents

Tokens

Import from @/components/xzero/lib/motion-tokens (installed with the first Xzero item).

TokenValueUse for
spring.smooth0.4s, bounce 0Panels, height, drawers, progress, layout shifts. Never overshoots
spring.snappy0.26s, bounce 0.12Press feedback, small indicators, icon swaps
spring.morph0.42s, bounce 0.16Shared highlights (layoutId), width that follows new content, shape morphs
duration.instant / fast / exit / standard / considered0.12 / 0.16 / 0.18 / 0.24 / 0.48sTweens for color, opacity, and small fades
ease.enter, ease.exit, ease.standard, ease.inOutcubic-bezier arraysEntrances, exits, color changes, on-screen moves
stagger.item / line / word / char0.035 / 0.08 / 0.04 / 0.016sLists and text, total under about 0.4s
blur.subtle / soft / text2 / 4 / 8pxBrief crossfades only

CSS transitions use the matching variables: var(--duration-fast) with var(--ease-standard) for color and opacity, var(--duration-spring) (580ms) with var(--ease-spring) for transforms. CSS has --duration-instant, -fast, -standard, -considered, and -spring; there is no --duration-exit.

Choosing a spring

Rules

  1. Animate transform and opacity (and brief filter: blur). Animate width or height only when the size change is the information, and then on a spring.
  2. One continuous gesture per interaction. A state change is one spatial movement, not a fade, then a slide, then a scale on separate timings.
  3. Everything is interruptible: use springs for anything the user can reverse, so reversing mid-way never jumps.
  4. Nothing resizes in one frame. Animate the size or reserve the space.
  5. Numbers count to their new value with font-variant-numeric: tabular-nums and a stable width (animated-counter, BillingPrice).
  6. Presses scale to about 0.97, except an element that anchors a popover or menu: answer that press with color, because the popup positions against the pressed size.
  7. Exits are faster than entrances (duration.exit, ease.exit).
  8. One orchestrated entrance per page at most. No scroll reveal on every section, no loops unless they show live status.
  9. Keep the animated element mounted. Changing its key or conditionally swapping its component restarts the animation (see composition.md).

Patterns with code

Selected highlight that glides between options (what segmented-control and tabs do):

import { motion, useReducedMotion } from "motion/react";
import { motionTokens } from "@/components/xzero/lib/motion-tokens";

const reduce = useReducedMotion();
{options.map(option => (
  <button key={option.value} onClick={() => setValue(option.value)} aria-pressed={value === option.value}>
    {value === option.value && (
      <motion.span layoutId="view-highlight" className={styles.highlight}
        transition={reduce ? { duration: 0 } : motionTokens.spring.morph} aria-hidden="true" />
    )}
    <span className={styles.label}>{option.label}</span>
  </button>
))}

Panel that opens with height and opacity together, no overshoot:

<AnimatePresence initial={false}>
  {open && (
    <motion.div key="details"
      initial={{ height: 0, opacity: 0 }}
      animate={{ height: "auto", opacity: 1 }}
      exit={{ height: 0, opacity: 0, transition: { duration: motionTokens.duration.exit, ease: [...motionTokens.ease.exit] } }}
      transition={reduce ? { duration: 0 } : motionTokens.spring.smooth}
      style={{ overflow: "hidden" }}>
      {children}
    </motion.div>
  )}
</AnimatePresence>

Hover and press in CSS:

.row { transition: background-color var(--duration-fast) var(--ease-standard); }
.row:hover, .row[data-selected] { background: var(--surface-muted); }
.press { transition: transform var(--duration-spring) var(--ease-spring); }
.press:active { transform: scale(.97); }
@media (prefers-reduced-motion: reduce) { .press, .row { transition: none; } }

Save confirmation in place: pass the promise to the Xzero item (onConfirm, onSave) or set loading on button, then change its label to "Saved". Do not add a toast for a foreground action.

Reduced motion

Every animation has a branch:

const reduce = useReducedMotion();
<motion.div transition={reduce ? { duration: 0 } : motionTokens.spring.smooth} />

Keep the final state, focus, and feedback. Remove travel, loops, parallax, and autoplay. A counter jumps straight to its value; a panel appears at full height.