Skip to main content
React Native Skia has deep integration with Reanimated 3, allowing you to use shared values directly in Skia components with zero bridge overhead.

Installation

React Native Skia works with Reanimated 3.0.0 and above:
npm install react-native-reanimated
Follow the Reanimated installation guide to complete setup.

How It Works

React Native Skia automatically detects when you pass Reanimated shared values to component props. These values are read directly on the UI thread, enabling:
  • Zero bridge communication - No messages between JS and native
  • 60+ FPS animations - Smooth performance even with complex graphics
  • Automatic updates - Canvas re-renders when shared values change
  • Worklet support - Run custom code on the UI thread
import { Canvas, Circle } from "@shopify/react-native-skia";
import { useSharedValue, withSpring } from "react-native-reanimated";

const AnimatedDemo = () => {
  const cx = useSharedValue(50);

  // This runs on the UI thread!
  const handlePress = () => {
    cx.value = withSpring(200);
  };

  return (
    <Canvas style={{ width: 256, height: 256 }}>
      <Circle cx={cx} cy={128} r={40} color="blue" />
    </Canvas>
  );
};

Shared Values in Props

Any Skia component prop can accept a shared value:
const x = useSharedValue(0);
const y = useSharedValue(0);
const r = useSharedValue(40);
const color = useSharedValue("red");
const opacity = useSharedValue(1);
const strokeWidth = useSharedValue(2);

<Circle 
  cx={x} 
  cy={y} 
  r={r} 
  color={color} 
  opacity={opacity} 
  style="stroke" 
  strokeWidth={strokeWidth} 
/>

Transforms

Animate transforms with shared values:
import { useSharedValue, withTiming } from "react-native-reanimated";

const rotation = useSharedValue(0);
const scale = useSharedValue(1);
const translateX = useSharedValue(0);

useEffect(() => {
  rotation.value = withRepeat(
    withTiming(2 * Math.PI, { duration: 3000, easing: Easing.linear }),
    -1,
    false
  );
  scale.value = withRepeat(
    withSequence(
      withTiming(1.5, { duration: 1000 }),
      withTiming(1, { duration: 1000 })
    ),
    -1,
    false
  );
}, []);

return (
  <Canvas style={{ flex: 1 }}>
    <Group 
      transform={[
        { translateX },
        { rotate: rotation },
        { scale }
      ]}
    >
      <Circle cx={128} cy={128} r={40} color="purple" />
    </Group>
  </Canvas>
);

Derived Values

Compute animated values based on other shared values:
import { useDerivedValue } from "react-native-reanimated";

const scrollY = useSharedValue(0);

// Automatically updates when scrollY changes
const headerOpacity = useDerivedValue(() => {
  return Math.max(0, Math.min(1, 1 - scrollY.value / 100));
});

const headerScale = useDerivedValue(() => {
  return Math.max(0.8, 1 - scrollY.value / 200);
});

return (
  <Group 
    opacity={headerOpacity}
    transform={[{ scale: headerScale }]}
  >
    <Text text="Header" x={32} y={32} font={font} />
  </Group>
);

Path Interpolation

Smooth morphing between paths:
import { Skia } from "@shopify/react-native-skia";
import { useDerivedValue, withTiming } from "react-native-reanimated";

const AnimatedPathDemo = () => {
  const progress = useSharedValue(0);

  const path1 = Skia.Path.Make();
  path1.moveTo(20, 100);
  path1.quadTo(128, 0, 236, 100);

  const path2 = Skia.Path.Make();
  path2.moveTo(20, 100);
  path2.quadTo(128, 200, 236, 100);

  useEffect(() => {
    progress.value = withRepeat(
      withTiming(1, { duration: 1000 }),
      -1,
      true
    );
  }, []);

  const path = useDerivedValue(() => {
    return path1.interpolate(path2, progress.value)!;
  });

  return (
    <Canvas style={{ width: 256, height: 256 }}>
      <Path path={path} color="blue" style="stroke" strokeWidth={3} />
    </Canvas>
  );
};

Color Interpolation

Smooth color transitions:
import { interpolateColor, useDerivedValue } from "react-native-reanimated";

const progress = useSharedValue(0);

const backgroundColor = useDerivedValue(() => {
  return interpolateColor(
    progress.value,
    [0, 0.5, 1],
    ["#FF0000", "#00FF00", "#0000FF"]
  );
});

useEffect(() => {
  progress.value = withRepeat(
    withTiming(1, { duration: 3000 }),
    -1,
    true
  );
}, []);

return (
  <Canvas style={{ flex: 1 }}>
    <Fill color={backgroundColor} />
  </Canvas>
);

Text on Path Animation

Animate text following a path:
import { Canvas, TextPath, useFont, Skia } from "@shopify/react-native-skia";
import { useDerivedValue, useSharedValue, withRepeat, withTiming } from "react-native-reanimated";

const AnimatedTextPath = () => {
  const font = useFont(require("./font.ttf"), 16);
  const progress = useSharedValue(0);

  const path1 = Skia.Path.Make();
  path1.moveTo(20, 100);
  path1.quadTo(128, 50, 236, 100);

  const path2 = Skia.Path.Make();
  path2.moveTo(20, 100);
  path2.quadTo(128, 150, 236, 100);

  useEffect(() => {
    progress.value = withRepeat(
      withTiming(1, { duration: 700 }),
      -1,
      true
    );
  }, []);

  const animatedPath = useDerivedValue(() => {
    return path1.interpolate(path2, progress.value)!;
  });

  if (!font) return null;

  return (
    <Canvas style={{ width: 256, height: 200 }}>
      <TextPath 
        path={animatedPath} 
        text="Animated Text on Path" 
        font={font} 
        color="blue"
      />
    </Canvas>
  );
};

Frame Callbacks

Run code on every frame:
import { useFrameCallback } from "react-native-reanimated";

const rotation = useSharedValue(0);
const time = useSharedValue(0);

useFrameCallback((frameInfo) => {
  // frameInfo.timestamp is in milliseconds
  time.value = frameInfo.timestamp / 1000;
  rotation.value = (time.value * Math.PI) % (Math.PI * 2);
});

Worklets

Functions that run on the UI thread must be marked with 'worklet':
const animateToTarget = (target: number) => {
  'worklet';
  x.value = withSpring(target);
  y.value = withSpring(target);
};

// Use in animations
const progress = useDerivedValue(() => {
  'worklet';
  return Math.sin(time.value);
});

Running on UI Thread

Explicitly run code on the UI thread:
import { runOnUI } from "react-native-reanimated";

const updateValues = (newX: number, newY: number) => {
  'worklet';
  x.value = withSpring(newX);
  y.value = withSpring(newY);
};

const handlePress = () => {
  runOnUI(updateValues)(100, 200);
};

Animated Props Hook

While not necessary for basic usage, you can use useAnimatedProps for complex scenarios:
import { useAnimatedProps } from "react-native-reanimated";

const progress = useSharedValue(0);

const animatedProps = useAnimatedProps(() => {
  const angle = progress.value * Math.PI * 2;
  return {
    cx: 128 + Math.cos(angle) * 50,
    cy: 128 + Math.sin(angle) * 50,
  };
});

return (
  <Canvas style={{ width: 256, height: 256 }}>
    <Circle {...animatedProps} r={20} color="red" />
  </Canvas>
);

Shader Uniforms

Animate shader uniforms:
import { Skia } from "@shopify/react-native-skia";
import { useDerivedValue } from "react-native-reanimated";

const time = useSharedValue(0);

const source = Skia.RuntimeEffect.Make(`
  uniform float time;
  uniform vec2 resolution;

  half4 main(vec2 xy) {
    vec2 uv = xy / resolution;
    return vec4(sin(time + uv.x), cos(time + uv.y), sin(time), 1.0);
  }
`);

const uniforms = useDerivedValue(() => {
  return {
    time: time.value,
    resolution: [256, 256],
  };
});

useFrameCallback((info) => {
  time.value = info.timestamp / 1000;
});

return (
  <Canvas style={{ width: 256, height: 256 }}>
    <Fill>
      <Shader source={source} uniforms={uniforms} />
    </Fill>
  </Canvas>
);

Performance Best Practices

Do’s

  • ✅ Use shared values for high-frequency updates
  • ✅ Use useDerivedValue for computed values
  • ✅ Mark worklet functions with 'worklet'
  • ✅ Batch related animations together
  • ✅ Use cancelAnimation to stop unused animations

Don’ts

  • ❌ Don’t use useState for animation values
  • ❌ Don’t trigger re-renders on every frame
  • ❌ Don’t perform heavy computations in derived values
  • ❌ Don’t forget to cleanup animations on unmount
  • ❌ Don’t nest too many derived values

Debugging

Enable Reanimated debugging:
import { startMapper } from "react-native-reanimated";

// Add logging to track value changes
const rotation = useSharedValue(0);

useEffect(() => {
  const id = startMapper(() => {
    'worklet';
    console.log('Rotation:', rotation.value);
  }, [rotation]);

  return () => stopMapper(id);
}, []);

Build docs developers (and LLMs) love