Overview
Updated: Sep 24, 2026
Use motion in the toolkit to communicate meaning rather than decorate the interface. Because focus drives the interface instead of a cursor, animation often shows what changed and what is now selected. Motion communicates four things:
- State — a control moving between its default, focused, and pressed appearance.
- Focus movement — where focus came from, where it landed, and what happened when it could not move any further.
- Content change — text swapping, an icon becoming a confirmation, one page replacing another.
- Hierarchy — what sits on top, what is anchored to what, and what is temporary.
Motion is expressed three ways, each documented on its own page:
| Approach | Use it when | Page |
|---|
Interpolated | The change has a known start and end, and a fixed duration and curve. | |
Spring | The target can change mid-flight, or the motion should settle physically. | |
Second-order | Something must move because something else moved. | |
Components own their motion
A toolkit component animates itself. The timing, curve, and sequencing of its states are part of the component, not something the application supplies. Use the component’s own behavior before writing an animation.
| Component | Motion it already owns |
|---|
Container
| Content scale and the material’s visual-state transition on focus and press, driven from one timeline so scale, fills, glow, and stroke never drift apart. |
TextSwitcher
| Crossfade when text changes, with animated and noAnimationFirstView to control it. |
Button
| An action transition through ButtonHandle.animateActionTransition, which scales the icon out, swaps it, and springs the replacement back in. |
Vignette
| Animated edge visibility through animate and animatedEdges. |
Shimmer
| The loading placeholder sweep, including its repeat behavior. |
IndeterminateLoader
| Continuous indeterminate progress. |
Pager
| The slide-and-fade transition between its pages, switchable with animated. |
PageTransition
| Route-level enter and exit, with InAppPageTransitionConfig and AppSwitchPageTransitionConfig. |
Let higher-level components own their material and timing. Compose with the component instead of rebuilding its look, and avoid wrapping it in a competing scale, width, opacity, or focus animation. Two owners for one property produce conflicting motion.
Author your own motion when:
- The animated value belongs to your application rather than to a component — a heading, a live measurement, an app-owned illustration.
- An app-owned element has to respond to a component’s state change. That is second-order motion, and there are APIs for it.
- You are building a component, in which case the motion becomes part of that component’s contract.
- One owner per property. Whatever animates a transform owns that transform for the whole interaction.
- Interruptible. A new input during an animation retargets it; it does not queue behind it and does not restart from the beginning.
- Settles on the latest state. After a burst of rapid input, the final rendered state must equal the final logical state. Rapid activation must never leave a control stuck looking pressed.
- No layout thrash. Animate transforms, opacity, and CSS custom properties. Do not animate width, height, or margins around live content, and do not regenerate geometry every frame.
- No per-frame React state. Use an imperative callback to write to the DOM or a custom property. A state setter per frame re-renders the subtree on every animation frame without improving the result.
- Correct on first paint. A screen renders in its final state. Do not animate from an unstyled state into the intended one on mount.
- Reduced motion is honored — see below.
Two exports let any animation respond to the user’s system preference.
usePrefersReducedMotion()
Returns true when the user has requested reduced motion. It subscribes to the (prefers-reduced-motion: reduce) media query and re-renders consumers when the preference changes at runtime. All consumers share a single media-query listener, and it returns false when window.matchMedia is unavailable, so it is safe during server rendering.
reducedMotionDuration(durationMs, prefersReduced)
| Parameter | Type | Description |
|---|
durationMs
| number
| The duration the animation would otherwise run for. |
prefersReduced
| boolean
| Usually the result of usePrefersReducedMotion(). |
Returns 0 when prefersReduced is true, otherwise durationMs. Pass a named duration through it so the change snaps instead of tweening.
import {
AnimationDurations,
createTransition,
reducedMotionDuration,
usePrefersReducedMotion,
} from '@wearables-ui-toolkit/mrbd';
export function useIconRotationTransition(): string {
const prefersReducedMotion = usePrefersReducedMotion();
return createTransition(
'rotate',
reducedMotionDuration(AnimationDurations.ICON_ROTATION, prefersReducedMotion),
);
}
Five things follow from this:
useSpringAnimation already honors the preference. It snaps to the target, emits the final value, and schedules no animation frames while reduced motion is on. You do not need to special-case it.- Some component-owned motion reads the preference for you.
Container applies state changes and material cross-fades instantly; Button snaps its expansion, icon rotation, and action transition; ButtonRail stops animating its focus-follow scrolling; Scrubber drops its transitions; and PageTransition collapses the route animation to an instant. - The rest of the library’s motion does not read the preference, so gate it from your application. Where a component exposes a motion switch —
animated on Pager, SliderBar, Switch, ProgressIndicator, ProgressRing, CircularProgressBar, IsolatedControl, SwipeIndicator, VolumeIndicator, ZoomIndicator, and TextSwitcher, or animate on Vignette — pass the negation of usePrefersReducedMotion() through it. Presentation motion that has no switch, such as the Tooltip and Toast fades and the SubNavigation show/hide, always animates; keep those surfaces short-lived and never make them the only signal that something changed. - Transitions authored directly in CSS are not covered by either export. Guard them with
@media (prefers-reduced-motion: reduce). - Reduced motion removes the tween, not the change. The end state must still be reached and must still be legible, because for a focus-driven interface the state change is the information.
- Use a component’s built-in motion and its props before writing an application animation. This keeps timing and state ownership coordinated.
- Reference
AnimationDurations, Interpolators, and SpringConfigs by name so related motion stays consistent when the system is tuned. - Drive continuous values through an imperative callback into a transform or a CSS custom property to keep React off the frame path.
- Let animations settle before comparing rendered frames during visual validation.
- Let the component own its scale, size, and opacity rather than wrapping it in a competing animation.
- Use one shared timeline for effects that should look simultaneous.
- Retarget the running animation on new input instead of restarting it.
- Keep an element mounted and drive it from state rather than changing its key to replay an animation.
- Use named timing constants instead of hardcoded durations or easing curves.