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.value returns 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: duration in milliseconds (default 300), easing (default "linear"), onComplete. The easings are "linear", "easeIn", "easeOut" and "easeInOut" (cubic), also exported as Easing.easeInOut and so on.
  • withSpring: stiffness (default 100), damping (default 10), mass (default 1), onComplete. A spring has no duration; a low damping overshoots and wobbles before it settles.
  • withRepeat: count (omit it to repeat forever), reverse (default false), 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 makes withRepeat(withTiming(360, { duration: 1200 })) from 0 spin forever.
  • With reverse: true, runs alternate direction (there and back), and count counts each direction as a run: count: 2 goes there and back once. reverse applies to a withTiming or withSpring inside; 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 finished true when the animation ran to its end and false when it was interrupted: by assigning a number or a new driver, or by cancelAnimation.
  • Only the driver assigned to .value reports. A callback on a driver nested inside withRepeat, withSequence or withDelay is ignored, with a console warning.
  • A withRepeat without count calls 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 interpolateColor binding. A plain shared value or interpolate cannot drive a color.
  • borderColor and borderRadius bind as a whole: one value for every side or corner.
  • Bindings work in the base style only. A binding in hoverStyle, pressStyle or focusStyle is ignored with a styleBinding warning. To react to hover, animate the value from onPointerEnter and onPointerLeave instead.
  • A binding wins over a transition on 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.
  • interpolateColor takes hex colors only.
  • Padding, margin and backgroundImage.scale cannot be animated.

The Animatable column of the style reference marks every property that accepts a binding.