Skip to main content
React Native Skia supports animated image formats including GIF and animated WebP. You can play, pause, and control animation playback with precise frame control. The easiest way to display animated images is with the useAnimatedImageValue hook, which integrates seamlessly with React Native Reanimated.

Basic Usage

import { Canvas, Image, useAnimatedImageValue } from "@shopify/react-native-skia";

const AnimatedImages = () => {
  // Automatically updates on every frame
  const bird = useAnimatedImageValue(
    require("./assets/birdFlying.gif")
  );
  
  return (
    <Canvas style={{ width: 320, height: 180 }}>
      <Image
        image={bird}
        x={0}
        y={0}
        width={320}
        height={180}
        fit="contain"
      />
    </Canvas>
  );
};
The useAnimatedImageValue hook returns a shared value that:
  • Starts as null while loading
  • Updates with a new SkImage on every frame once loaded
  • Automatically loops the animation

Controlling Playback

Use a shared value to pause and resume the animation:
import { Canvas, Image, useAnimatedImageValue } from "@shopify/react-native-skia";
import { Pressable } from "react-native";
import { useSharedValue } from "react-native-reanimated";

const AnimatedImagesWithControl = () => {
  const isPaused = useSharedValue(false);
  
  const bird = useAnimatedImageValue(
    require("./assets/birdFlying.gif"),
    isPaused
  );
  
  return (
    <Pressable onPress={() => {
      isPaused.value = !isPaused.value;
    }}>
      <Canvas style={{ width: 320, height: 180 }}>
        <Image
          image={bird}
          x={0}
          y={0}
          width={320}
          height={180}
          fit="contain"
        />
      </Canvas>
    </Pressable>
  );
};

Complete Example with State

import { Canvas, Image, useAnimatedImageValue } from "@shopify/react-native-skia";
import { Pressable, StyleSheet, Text } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import { useState } from "react";

const AnimatedImagePlayer = () => {
  const [isPlaying, setIsPlaying] = useState(true);
  const isPaused = useSharedValue(false);
  
  const animation = useAnimatedImageValue(
    require("./assets/animation.gif"),
    isPaused
  );
  
  const togglePlayback = () => {
    isPaused.value = !isPaused.value;
    setIsPlaying(!isPlaying);
  };
  
  return (
    <>
      <Canvas style={styles.canvas}>
        <Image
          image={animation}
          x={0}
          y={0}
          width={320}
          height={180}
          fit="contain"
        />
      </Canvas>
      <Pressable style={styles.button} onPress={togglePlayback}>
        <Text>{isPlaying ? "Pause" : "Play"}</Text>
      </Pressable>
    </>
  );
};

const styles = StyleSheet.create({
  canvas: {
    width: 320,
    height: 180,
  },
  button: {
    padding: 10,
    backgroundColor: "#007AFF",
    alignItems: "center",
    marginTop: 10,
  },
});

Manual API

For advanced control, use the useAnimatedImage hook which returns an SkAnimatedImage instance.

Loading Animated Images

import { useAnimatedImage } from "@shopify/react-native-skia";

const bird = useAnimatedImage(
  require("./assets/birdFlying.gif")
);

SkAnimatedImage Methods

The SkAnimatedImage instance provides several methods:
MethodReturnsDescription
getCurrentFrame()SkImageReturns the current frame as a regular SkImage
decodeNextFrame()numberDecodes and advances to the next frame. Returns -1 if finished
currentFrameDuration()numberReturns the duration of the current frame in milliseconds
getFrameCount()numberReturns the total number of frames in the animation

Manual Frame Control

import { useAnimatedImage } from "@shopify/react-native-skia";

const ManualControl = () => {
  const bird = useAnimatedImage(
    require("./assets/birdFlying.gif")
  );
  
  if (!bird) {
    return null;
  }
  
  // Get the current frame as an SkImage
  const image = bird.getCurrentFrame();
  
  // Advance to next frame
  const nextFrame = bird.decodeNextFrame();
  if (nextFrame === -1) {
    console.log("Animation finished");
  }
  
  // Get animation info
  const duration = bird.currentFrameDuration();
  const frameCount = bird.getFrameCount();
  
  console.log({
    frameCount,
    currentDuration: duration,
    image,
  });
};

Custom Animation Loop

import { Canvas, Image, useAnimatedImage } from "@shopify/react-native-skia";
import { useDerivedValue, useSharedValue, useFrameCallback } from "react-native-reanimated";
import { useEffect } from "react";

const CustomAnimationLoop = () => {
  const animatedImage = useAnimatedImage(
    require("./assets/animation.gif")
  );
  
  const currentFrame = useSharedValue<SkImage | null>(null);
  
  useFrameCallback(() => {
    if (!animatedImage) return;
    
    // Decode next frame
    const result = animatedImage.decodeNextFrame();
    
    // Loop back to start if finished
    if (result === -1) {
      // Animation finished, could restart or stop
    }
    
    // Get current frame
    currentFrame.value = animatedImage.getCurrentFrame();
  });
  
  return (
    <Canvas style={{ width: 320, height: 180 }}>
      <Image
        image={currentFrame}
        x={0}
        y={0}
        width={320}
        height={180}
        fit="contain"
      />
    </Canvas>
  );
};

Frame-by-Frame Control

import { Canvas, Image, useAnimatedImage } from "@shopify/react-native-skia";
import { Button, View } from "react-native";
import { useState } from "react";

const FrameByFrame = () => {
  const animatedImage = useAnimatedImage(
    require("./assets/animation.gif")
  );
  const [frame, setFrame] = useState(0);
  
  if (!animatedImage) return null;
  
  const nextFrame = () => {
    const result = animatedImage.decodeNextFrame();
    if (result !== -1) {
      setFrame(f => f + 1);
    }
  };
  
  const image = animatedImage.getCurrentFrame();
  const totalFrames = animatedImage.getFrameCount();
  
  return (
    <View>
      <Canvas style={{ width: 320, height: 180 }}>
        <Image
          image={image}
          x={0}
          y={0}
          width={320}
          height={180}
          fit="contain"
        />
      </Canvas>
      <Button 
        title={`Next Frame (${frame + 1}/${totalFrames})`}
        onPress={nextFrame}
      />
    </View>
  );
};

Supported Formats

React Native Skia supports:
  • GIF - Animated GIF images
  • WebP - Animated WebP images
Both formats support:
  • Variable frame durations
  • Looping
  • Transparency

Loading Animated Images

From Bundle

const animation = useAnimatedImageValue(
  require("./assets/animation.gif")
);

From Network

const animation = useAnimatedImageValue(
  "https://example.com/animation.gif"
);

From Native Resources

const animation = useAnimatedImageValue(
  "AnimationName" // iOS/Android bundle
);

Performance Considerations

Frame Rate

  • Animations decode one frame at a time
  • Heavy animations may impact performance
  • Consider reducing frame count for better performance
  • Use lower resolution animations when possible

Memory Usage

  • Each frame is decoded on demand
  • Large animations consume more memory
  • Consider using video for long animations
  • Monitor memory usage on lower-end devices

Optimization Tips

// Good: Conditional rendering
const OptimizedAnimation = () => {
  const [showAnimation, setShowAnimation] = useState(false);
  const animation = useAnimatedImageValue(
    showAnimation ? require("./assets/heavy.gif") : null
  );
  
  return (
    <Canvas style={{ flex: 1 }}>
      {animation && (
        <Image image={animation} x={0} y={0} width={320} height={180} />
      )}
    </Canvas>
  );
};

Common Patterns

Loading State

const AnimationWithLoader = () => {
  const animation = useAnimatedImageValue(
    require("./assets/animation.gif")
  );
  
  return (
    <Canvas style={{ width: 320, height: 180 }}>
      {!animation ? (
        <Text x={100} y={90} text="Loading..." />
      ) : (
        <Image
          image={animation}
          x={0}
          y={0}
          width={320}
          height={180}
          fit="contain"
        />
      )}
    </Canvas>
  );
};

Multiple Animations

const MultipleAnimations = () => {
  const isPaused = useSharedValue(false);
  const anim1 = useAnimatedImageValue(require("./assets/anim1.gif"), isPaused);
  const anim2 = useAnimatedImageValue(require("./assets/anim2.gif"), isPaused);
  
  return (
    <Canvas style={{ width: 640, height: 180 }}>
      <Image image={anim1} x={0} y={0} width={320} height={180} />
      <Image image={anim2} x={320} y={0} width={320} height={180} />
    </Canvas>
  );
};

Conditional Playback

import { useSharedValue } from "react-native-reanimated";
import { useEffect } from "react";

const ConditionalPlayback = ({ isVisible }) => {
  const isPaused = useSharedValue(!isVisible);
  
  useEffect(() => {
    isPaused.value = !isVisible;
  }, [isVisible]);
  
  const animation = useAnimatedImageValue(
    require("./assets/animation.gif"),
    isPaused
  );
  
  return (
    <Canvas style={{ width: 320, height: 180 }}>
      <Image image={animation} x={0} y={0} width={320} height={180} />
    </Canvas>
  );
};

Troubleshooting

Animation Not Playing

  • Ensure the image loaded successfully (check if not null)
  • Verify the shared value for pausing is set correctly
  • Check that the file is a valid animated format

Poor Performance

  • Reduce animation dimensions
  • Decrease frame count
  • Lower frame rate
  • Consider using video instead
  • Use smaller file sizes

Memory Issues

  • Unload animations when not visible
  • Use conditional rendering
  • Limit number of simultaneous animations
  • Monitor device memory usage

See Also

Build docs developers (and LLMs) love