Motion API
Use Velvet's interruptible springs, WAAPI orchestration, travel interpolation, reduced-motion handling, and React lifecycle hooks anywhere.
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
// 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
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
| Export | Contract |
|---|---|
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
| Field | Type | Default or behavior |
|---|---|---|
preset | gentle, smooth, snappy, brisk, bouncy, elastic | overrides conflicting physical fields |
easing | spring, ease, ease-in, ease-out, ease-in-out, linear | spring |
stiffness | number | 300 without component fallback |
damping | number | 34 without component fallback |
mass | number | 1 |
precision | number | 0.1 |
initialVelocity | number | 0 |
duration | milliseconds | timing-easing override |
delay | milliseconds | 0 |
skip | boolean, auto, or never | follows 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
| Preset | Stiffness | Damping | Mass | Character |
|---|---|---|---|---|
| gentle | 560 | 68 | 1.85 | deliberate and heavy |
| smooth | 580 | 60 | 1.35 | default opening |
| snappy | 350 | 34 | 0.9 | compact controls |
| brisk | 350 | 28 | 0.65 | fast state change |
| bouncy | 240 | 19 | 0.7 | visible overshoot |
| elastic | 260 | 20 | 1 | softer rebound |
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.
| Value | Meaning |
|---|---|
[0, 1] | interpolate with CSS calc() |
["24px", "0px"] | interpolate compatible CSS values |
({ progress }) => value | compute any property |
({ tween }) => tween(from, to) | generate interpolation explicitly |
| literal | keep the property fixed through travel |
false, null, "ignore" | leave the property untouched |
<Sheet.Backdrop travelAnimation={{ opacity: [0, 0.42] }} />
<Sheet.Content travelAnimation={{
translateY: ["28px", "0px"],
scale: [0.97, 1],
borderRadius: ({ progress }) => `${32 - progress * 8}px`,
}} />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
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
<Sheet.View
enteringAnimationSettings="smooth"
steppingAnimationSettings="snappy"
exitingAnimationSettings={{ stiffness: 520, damping: 44, mass: 1 }}
/>| Callback | Use |
|---|---|
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.