Animated values
Animated values are a Reanimated-style animation API. useSharedValue
creates a number that lives on the Bevy side, drivers such as withTiming
and withSpring describe how it moves, and an inline { animated } wrapper
binds it to a style property. React renders once; every frame after that is
computed and applied by Bevy, with no re-renders and no per-frame traffic
from JS.
Usage
import { useSharedValue, withTiming } from "bevy-react";
function Slide() {
const x = useSharedValue(0);
return (
<button
onClick={() => {
x.value = withTiming(200, { duration: 800, easing: "easeInOut" });
}}
style={{ transform: { translateX: { animated: x } } }}
>
<text>Slide</text>
</button>
);
}Shared values
useSharedValue(initial) returns a handle that is stable across re-renders,
one per component instance. initial is used only on the first render.
- Assigning a number (
x.value = 0) sets the value at once and stops any running driver. - Assigning a driver (
x.value = withSpring(1)) starts an animation from the value's current reading on the Bevy side, so interrupting a running animation never jumps. cancelAnimation(x)stops the running driver and freezes the value where it is.- Reading
x.valuereturns the last number assigned from JS, not the animated reading: per-frame values never travel back to JS. Use a completion callback to learn when an animation ends. - One shared value can drive any number of properties on any number of nodes, and you can pass it to other components as a prop.
- Assign drivers in event handlers and effects. An assignment during render restarts the animation on every render.
Shared values and their running animations survive a hot reload that keeps component state. A full reload clears them.
Drivers
| Driver | Moves the value |
|---|---|
withTiming(to, config?) |
Along an easing curve over a fixed duration |
withSpring(to, config?) |
With a damped spring until it rests on to |
withRepeat(driver, config?) |
Runs driver again, forever or count times |
withSequence(...drivers, callback?) |
Runs each driver in turn, from where the previous ended |
withDelay(ms, driver, callback?) |
Holds the current value for ms, then runs driver |
Configs, all fields optional:
withTiming:durationin milliseconds (default300),easing(default"linear"),onComplete. The easings are"linear","easeIn","easeOut"and"easeInOut"(cubic), also exported asEasing.easeInOutand so on.withSpring:stiffness(default100),damping(default10),mass(default1),onComplete. A spring has no duration; a lowdampingovershoots and wobbles before it settles.withRepeat:count(omit it to repeat forever),reverse(defaultfalse),onComplete.
Drivers compose:
x.value = withSequence(
withTiming(110, { duration: 450, easing: "easeOut" }),
withDelay(250, withTiming(-110, { duration: 450 })),
withDelay(250, withSpring(0)),
);withRepeat details:
- Without
reverse, every run starts again from the value the repeat began at, so the value jumps back at the end of each run. That is what makeswithRepeat(withTiming(360, { duration: 1200 }))from0spin forever. - With
reverse: true, runs alternate direction (there and back), andcountcounts each direction as a run:count: 2goes there and back once.reverseapplies to awithTimingorwithSpringinside; any other driver repeats as written.
const opacity = useSharedValue(1);
useEffect(() => {
opacity.value = withRepeat(
withTiming(0, { duration: 500, easing: "easeInOut" }),
{ reverse: true },
);
}, [opacity]);Completion callbacks
A driver can report when it settles. withTiming, withSpring and
withRepeat take onComplete in their config; withSequence takes a
trailing function and withDelay a third argument:
x.value = withSequence(
withTiming(110, { duration: 450 }),
withTiming(0, { duration: 350 }),
(finished) => setRunning(false),
);- The callback runs once, with
finishedtruewhen the animation ran to its end andfalsewhen it was interrupted: by assigning a number or a new driver, or bycancelAnimation. - Only the driver assigned to
.valuereports. A callback on a driver nested insidewithRepeat,withSequenceorwithDelayis ignored, with a console warning. - A
withRepeatwithoutcountcalls back only when it is interrupted. - The callback arrives through the event loop like any event from Bevy, so it may set state.
Bindings
Write { animated: value } in place of a style value. The value is a shared
value, or an interpolate or interpolateColor result that maps it through a
curve (see Interpolation):
<node
style={{
width: { animated: w },
opacity: { animated: fade },
transform: { rotate: { animated: angle } },
}}
/>| Position | Bound value |
|---|---|
opacity |
0 to 1 |
transform: translateX, translateY |
Logical px |
transform: scale, scaleX, scaleY |
Factor |
transform: rotate |
Degrees |
| Layout lengths (listed below) | Logical px |
aspectRatio |
Ratio |
borderRadius |
Logical px, all four corners |
backgroundColor, borderColor, color |
An interpolateColor binding |
backgroundImage: tint |
An interpolateColor binding |
transform3d fields |
See 3D transforms |
filter, backdropFilter, morphFilter params |
See Filters |
| Gradient angles, stops and radii | See Gradients |
The layout lengths are width, height, minWidth, minHeight,
maxWidth, maxHeight, left, right, top, bottom, flexBasis,
gap, rowGap and columnGap.
Rules:
- A bound length is always in pixels and a bound rotation in degrees, whatever unit the static form of the field would use.
- The binding is the field's only value: there is no static value under it.
- A color position needs an
interpolateColorbinding. A plain shared value orinterpolatecannot drive a color. borderColorandborderRadiusbind as a whole: one value for every side or corner.- Bindings work in the base
styleonly. A binding inhoverStyle,pressStyleorfocusStyleis ignored with astyleBindingwarning. To react to hover, animate the value fromonPointerEnterandonPointerLeaveinstead. - A binding wins over a
transitionon the same property (see Style transitions). seed({ animated: value, seed: 16 }) only matters for filter params and element attributes; style fields ignore it.
Element attributes can be animated too: the numeric attributes of SVG shapes
(see <svg>) and those of your own elements
(see Custom elements).
Cost
A shared value is stepped once per Bevy frame by the frame's delta time,
and the nodes bound to it are written only on frames where it changed. An
idle value costs nothing. Transform, opacity and color bindings do not
trigger a relayout. Layout bindings (sizes, positions, gaps, aspectRatio, borderRadius)
relayout the UI on every frame they change, so prefer transform for pure
motion.
Limits
- A shared value is one number. Animate several numbers with several values.
- The JS side cannot read the live value, and easing is limited to the four named curves: a JS easing function cannot run in Bevy.
interpolateColortakes hex colors only.- Padding, margin and
backgroundImage.scalecannot be animated.
The Animatable column of the style reference marks every property that accepts a binding.
bevy-react