Skip to content

Animations

SymbioteNative ships a from-scratch, framework-agnostic port of React Native’s Animated API. The value graph, easing curves, interpolation, and composition helpers (timing, spring, decay, parallel, sequence, stagger, loop, delay) live once in @symbiote-native/engine — every adapter re-exports the same Animated namespace and the same createAnimatedComponent mechanism, so Animated.Value, Animated.spring, and the rest behave identically no matter which framework renders them.

import { Animated } from '@symbiote-native/react';
const pulse = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.loop(
Animated.timing(pulse, { toValue: 1, duration: 1400, useNativeDriver: true }),
).start();
}, []);
const scale = pulse.interpolate({ inputRange: [0, 0.5, 1], outputRange: [1, 1.3, 1] });
<Animated.View style={{ transform: [{ scale }] }} />;

Why not Svelte’s transition:/animate:/in:/out:?

Section titled “Why not Svelte’s transition:/animate:/in:/out:?”

Those directives — along with use:, class:, and style: — are Svelte’s own element-only directives, rejected on a component tag (“This type of directive is not valid on components”). Every SymbioteNative primitive is now a lowercase intrinsic tag (<view>, <pressable>, …), which Svelte’s compiler treats as a plain element, so these directives do compile there today — this is different from when the primitives were capitalized components. Whether transition:/animate: actually animate anything through the DOM shim’s Fabric-backed properties is unverified; nothing in this adapter tests or supports that path. Animated plus the $effect-driven lifecycle shown above remains the supported route for animation; {@attach fn} remains your route to the underlying host node for anything else.

useNativeDriver: true hands the entire animation curve to a native-side driver (NativeAnimated) — once started, zero JS runs per frame, and the animation keeps running even if the JS thread is busy. useNativeDriver: false (the default) runs the same math in JS and commits a new value through the engine on every frame — necessary for properties the native driver can’t touch (most non-transform/opacity style properties), at the cost of a commit per frame.

Both drivers use the exact same Animated.Value/Animated.timing call — useNativeDriver is the only thing that changes. See examples/react/App.tsx’s AnimatedDemo for a JS-driven dot and a native-driven dot running side by side, with DEBUG=1 logging showing the difference directly (a native run logs one native: startAnimatingNode; a JS run logs a commit … incremental roughly once per frame).

The full RN Animated surface ported, not just timing:

  • Animated.ValueXY — a 2D value pair, typically paired with PanResponder for drag gestures.
  • Animated.spring/Animated.decay — physics-based animations, same config shape as RN’s.
  • Tracking — passing another Animated.Value (or ValueXY) as toValue makes the animation chase a moving target instead of a fixed number, the same “tracking” mechanism RN uses for gesture-follow effects.
  • add/subtract/multiply/divide/modulo/diffClamp — operators that combine animated values, e.g. a collapsing header driven by diffClamp(scrollY, 0, HEADER_HEIGHT).
  • Animated.event(...) — maps a native event’s payload directly onto an Animated.Value with no JS in the loop (onScroll={Animated.event(...)}).
  • Animated.parallel/sequence/stagger/loop/delay — the same composition helpers RN ships, unchanged.

examples/react/App.tsx’s AnimatedParityDemo exercises all of this in one place: a ValueXY-driven drag box clamped with PanResponder, a spring that chases a moving lead value (tracking), and a diffClamp-driven collapsing header.

SymbioteNative’s Animated is the RN Animated API, not Reanimated — there is no worklet system, no useSharedValue/useAnimatedStyle, and no gesture-handler integration beyond what PanResponder already provides. react-native-reanimated itself is not tested against SymbioteNative’s engine; it hooks much deeper into RN’s runtime than a plain native-view library does, so treat it as unverified rather than assuming it works the same way a wrapped third-party native view would.