Motion

Motion API

Use Velvet's interruptible springs, WAAPI orchestration, travel interpolation, reduced-motion handling, and React lifecycle hooks anywhere.

View as Markdown

Velvet's motion engine is a framework-free public module. Use the same sampled springs, interruption policy, reduced-motion settlement, and progress interpolation outside Sheet without importing React or any component runtime.

Choose the smallest entry

tsx
// Any JavaScript or framework
import {
  animate,
  springEasing,
  resolveTravelStyles,
} from "velvet-ui/motion";

// React lifecycle helpers only
import {
  useAnimate,
  useReducedMotion,
  useSpringEasing,
} from "velvet-ui/motion/react";

velvet-ui/motion has no React dependency. velvet-ui/motion/react adds only the hooks and their lifecycle ownership. Neither entry imports Sheet, Scroll, Toast, or a stylesheet.

Animate any element

tsx
import { animate } from "velvet-ui/motion";

const playback = animate(card, {
  transform: ["translateY(16px) scale(.98)", "translateY(0) scale(1)"],
  opacity: [0, 1],
}, {
  id: "card-enter",
  preset: "snappy",
  commit: true,
});

const status = await playback.finished;
// "finished" | "cancelled" | "skipped"

id is local to one element. Starting another Velvet animation with the same element and id cancels the old playback; unrelated browser or library animations are untouched. Pass id: false only when concurrent effects are intentional.

finished never rejects during normal cancellation. By default the final keyframe is committed to inline styles and the completed WAAPI animation is released. Set commit: false when another owner must retain final-style control.

Public functions

ExportContract
animate(element, keyframes, options)Interruptible, reduced-motion-safe WAAPI playback
sampleSpring(settings, { from, to, v0 })Finite normalized spring samples and settle duration
springEasing(settings, sampleOptions)CSS linear() easing and duration
resolveMotionTiming(settings)Normalized duration and easing
prefersReducedMotion(target?)Current preference for an Element, Document, Window, or global window
resolveTravelStyles(map, progress, context?)Resolve a progress map without touching the DOM
applyTravelStyles(element, map, progress, context?)Resolve and apply the same map
linearEasingFromSamples(values)Safe CSS easing serialization
decimateSpring(values, amount?)Reduce a sampled curve for CSS or WAAPI

SpringSettings

FieldTypeDefault or behavior
presetgentle, smooth, snappy, brisk, bouncy, elasticoverrides conflicting physical fields
easingspring, ease, ease-in, ease-out, ease-in-out, linearspring
stiffnessnumber300 without component fallback
dampingnumber34 without component fallback
massnumber1
precisionnumber0.1
initialVelocitynumber0
durationmillisecondstiming-easing override
delaymilliseconds0
skipboolean, auto, or neverfollows reduced motion

Physics are normalized to finite operating ranges. A zero mass, infinite stiffness, NaN endpoint, or equal endpoints cannot create an endless loop or non-finite keyframe list. Unknown settings fall back to a smooth spring.

Preset values

PresetStiffnessDampingMassCharacter
gentle560681.85deliberate and heavy
smooth580601.35default opening
snappy350340.9compact controls
brisk350280.65fast state change
bouncy240190.7visible overshoot
elastic260201softer rebound
tsx
const timing = springEasing("smooth");
const samples = sampleSpring(
  { stiffness: 520, damping: 44, mass: 1 },
  { from: 0, to: 240, v0: 0.4 },
);

A recognized preset wins over physical values in the same object. Use either a preset or custom physics, not both.

TravelAnimation

travelAnimation and stackingAnimation accept a property map or { properties: map }. Each value may be a literal, [from, to], a callback, false, or null.

ValueMeaning
[0, 1]interpolate with CSS calc()
["24px", "0px"]interpolate compatible CSS values
({ progress }) => valuecompute any property
({ tween }) => tween(from, to)generate interpolation explicitly
literalkeep the property fixed through travel
false, null, "ignore"leave the property untouched
tsx
<Sheet.Backdrop travelAnimation={{ opacity: [0, 0.42] }} />
<Sheet.Content travelAnimation={{
  translateY: ["28px", "0px"],
  scale: [0.97, 1],
  borderRadius: ({ progress }) => `${32 - progress * 8}px`,
}} />
tsx
const styles = resolveTravelStyles({
  opacity: [0, 1],
  translateY: ["12px", "0px"],
  "--scrim-strength": [0, 0.42],
}, progress);

Object.assign(node.style, styles);

Non-finite progress becomes zero. Invalid entries, unsafe object keys, and a property callback that throws are isolated instead of corrupting the rest of the map. Supply onError(error, detail) in the context when the application needs reporting.

Transform composition

The transform aliases are translate, translateX/Y/Z, scale/X/Y/Z, rotate/X/Y/Z, and skew/X/Y. Velvet combines them into one transform in declaration order. A literal transform can also participate, but product CSS should not animate the same transform property at the same time.

React hooks

tsx
function SaveButton() {
  const animate = useAnimate();
  const reduceMotion = useReducedMotion();

  return (
    <button onClick={(event) => {
      animate(event.currentTarget, {
        transform: ["scale(.96)", "scale(1)"],
      }, {
        id: "press",
        preset: "snappy",
        skip: reduceMotion,
      });
    }}>
      Save
    </button>
  );
}

useAnimate cancels every playback it owns when the component unmounts. useReducedMotion is hydration-safe and remains subscribed when the operating-system setting changes. useSpringEasing memoizes a CSS easing for render-time integration; pass a stable custom settings object.

Sheet integration

tsx
<Sheet.View
  enteringAnimationSettings="smooth"
  steppingAnimationSettings="snappy"
  exitingAnimationSettings={{ stiffness: 520, damping: 44, mass: 1 }}
/>
CallbackUse
onTravel({ progress, range, progressAtDetents })synchronize product UI
onTravelStart()start expensive visual work
onTravelEnd()settle derived state
onTravelStatusChange(status)observe idleOutside, entering, idleInside, stepping, exiting

Avoid writing progress to React state on every frame. Update an imperative ref, CSS variable, canvas, or external animation when continuous rendering is required.

Reduced motion

All spring entry points read prefers-reduced-motion when motion starts, not when the module loads. skip="auto" follows the current media query, skip={true} forces immediate settlement, and skip="never" deliberately opts out for motion that conveys essential state.

Skipped motion still applies the final keyframe and resolves its playback as skipped; a missing element or unavailable WAAPI follows the same safe path. Keep the final keyframe complete enough to represent the settled UI.