Skip to main content
Skottie is a Lottie animation renderer built on Skia. It allows you to load and render After Effects animations exported via Bodymovin/Lottie, providing full programmatic control over animation properties, colors, text, and more.

Skottie Component

The Skottie component renders Lottie animations on the canvas.

Props

NameTypeDescription
animationSkSkottieAnimationCompiled animation from Skia.Skottie.Make()
framenumberThe frame to display (can be animated)

Basic Example

import { Canvas, Group, Skottie, Skia } from "@shopify/react-native-skia";

const legoAnimationJSON = require("./assets/lego_loader.json");
const animation = Skia.Skottie.Make(JSON.stringify(legoAnimationJSON));

const SkottieExample = () => {
  return (
    <Canvas style={{ width: 400, height: 300 }}>
      <Group transform={[{ scale: 0.5 }]}>
        <Skottie animation={animation} frame={41} />
      </Group>
    </Canvas>
  );
};

Creating Animations

Load Lottie JSON data using Skia.Skottie.Make():
import { Skia } from "@shopify/react-native-skia";

const legoAnimationJSON = require("./assets/lego_loader.json");
const animation = Skia.Skottie.Make(
  JSON.stringify(legoAnimationJSON)
);

if (!animation) {
  throw new Error("Failed to create animation");
}

With External Assets

Many Lottie animations include external assets like fonts and images:
import { Skia } from "@shopify/react-native-skia";

const basicSlotsJSON = require("./assets/basic_slots.json");

const assets = {
  "NotoSerif": Skia.Data.fromBytes(fontBytes),
  "img_0.png": Skia.Data.fromBytes(imageBytes),
};

const animation = Skia.Skottie.Make(
  JSON.stringify(basicSlotsJSON),
  assets
);

Animated Playback

Combine Skottie with Reanimated for smooth animation:
import {
  Canvas,
  Skottie,
  useClock,
  Group,
  Skia,
} from "@shopify/react-native-skia";
import { useDerivedValue } from "react-native-reanimated";

const legoAnimationJSON = require("./assets/lego_loader.json");
const animation = Skia.Skottie.Make(
  JSON.stringify(legoAnimationJSON)
);

const AnimatedSkottie = () => {
  const clock = useClock();
  
  const frame = useDerivedValue(() => {
    const fps = animation.fps();
    const duration = animation.duration();
    const totalFrames = duration * fps;
    const currentFrame = Math.floor((clock.value / 1000) * fps) % totalFrames;
    return currentFrame;
  });
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Group transform={[{ scale: 0.5 }]}>
        <Skottie animation={animation} frame={frame} />
      </Group>
    </Canvas>
  );
};

Animation Properties

Basic Information

Get information about your animation:
// Duration in seconds
const duration = animation.duration();

// Frames per second
const fps = animation.fps();

// Lottie version
const version = animation.version();

// Animation dimensions
const size = animation.size(); // { width: 800, height: 600 }

Dynamic Properties

Skottie allows runtime modification of animation properties without recreating the animation.

Color Properties

Modify colors programmatically:
import { Skia } from "@shopify/react-native-skia";

const animationJSON = require("./assets/fingerprint.json");
const animation = Skia.Skottie.Make(
  JSON.stringify(animationJSON)
);

// Get all color properties
const colorProps = animation.getColorProps();
// Returns: [{ key: string, value: SkColor }, ...]

// Modify a color
if (colorProps.length > 0) {
  animation.setColor(
    colorProps[0].key,
    Skia.Color("rgb(60, 120, 255)")
  );
}

Text Properties

Change text content and size:
// Get all text properties
const textProps = animation.getTextProps();
// Returns: [{ key: string, value: { text: string, size: number } }, ...]

// Set text
animation.setText("hello!", "World", 164);

Opacity Properties

Control opacity:
// Get all opacity properties
const opacityProps = animation.getOpacityProps();
// Returns: [{ key: string, value: number }, ...]

// Set opacity (0-100)
animation.setOpacity(opacityProps[0].key, 50);

Transform Properties

Modify transforms:
// Get all transform properties
const transformProps = animation.getTransformProps();
// Returns: [{ 
//   key: string,
//   value: {
//     anchor: { x: number, y: number },
//     position: { x: number, y: number },
//     scale: { x: number, y: number },
//     rotation: number,
//     skew: number,
//     skewAxis: number
//   }
// }, ...]

// Set transform
animation.setTransform(
  transformProps[0].key,
  { x: 0, y: 0 },      // anchor
  { x: 100, y: 100 },  // position
  { x: 2, y: 2 },      // scale
  45,                  // rotation
  0,                   // skew
  0                    // skewAxis
);

Slot Management

Slots are designer-created placeholders for dynamic content replacement.

Getting Slot Information

const slotInfo = animation.getSlotInfo();
// Returns:
// {
//   colorSlotIDs: string[],
//   imageSlotIDs: string[],
//   scalarSlotIDs: string[],
//   textSlotIDs: string[],
//   vec2SlotIDs: string[]
// }

Setting Slots

Color Slots

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

const slotInfo = animation.getSlotInfo();
if (slotInfo.colorSlotIDs.length > 0) {
  animation.setColorSlot(
    slotInfo.colorSlotIDs[0],
    Skia.Color("cyan")
  );
}

Text Slots

const textSlotProps = {
  text: "Hello World",
  textSize: 24,
  fillColor: Skia.Color("blue"),
};

animation.setTextSlot("TextSlotID", textSlotProps);

Scalar Slots

animation.setScalarSlot("OpacitySlot", 0.5);

Vec2 Slots

animation.setVec2Slot("PositionSlot", { x: 100, y: 200 });

Image Slots

animation.setImageSlot("ImageSlot", "assetName");

Getting Slot Values

const color = animation.getColorSlot("ColorSlotID");
const scalar = animation.getScalarSlot("ScalarSlotID");
const vec2 = animation.getVec2Slot("Vec2SlotID");
const text = animation.getTextSlot("TextSlotID");

Complete Example with Dynamic Properties

import { 
  Canvas, 
  Skia, 
  useClock,
  Group,
  Skottie
} from "@shopify/react-native-skia";
import { useDerivedValue } from "react-native-reanimated";

const animationJSON = require("./assets/fingerprint.json");

// Create and configure animation
const animation = Skia.Skottie.Make(
  JSON.stringify(animationJSON)
);

if (!animation) {
  throw new Error("Failed to create animation");
}

// Modify properties
const colorProps = animation.getColorProps();
if (colorProps.length > 0) {
  animation.setColor(
    colorProps[0].key,
    Skia.Color("rgb(60, 120, 255)")
  );
}

// Set color slots
const slotInfo = animation.getSlotInfo();
if (slotInfo.colorSlotIDs.length > 0) {
  animation.setColorSlot(
    slotInfo.colorSlotIDs[0],
    Skia.Color("magenta")
  );
}

const SkottiePlayer = () => {
  const clock = useClock();
  
  const frame = useDerivedValue(() => {
    const fps = animation.fps();
    const duration = animation.duration();
    const totalFrames = duration * fps;
    return Math.floor((clock.value / 1000) * fps) % totalFrames;
  });
  
  return (
    <Canvas style={{ width: 400, height: 400 }}>
      <Group transform={[{ scale: 0.5 }]}>
        <Skottie animation={animation} frame={frame} />
      </Group>
    </Canvas>
  );
};

Applying Effects

Skottie components don’t follow standard painting rules. Use the layer property:
import { 
  Canvas, 
  Skottie, 
  Skia, 
  Group, 
  Paint, 
  Blur 
} from "@shopify/react-native-skia";

const legoAnimationJSON = require("./assets/lego_loader.json");
const animation = Skia.Skottie.Make(
  JSON.stringify(legoAnimationJSON)
);

const BlurredSkottie = () => {
  return (
    <Canvas style={{ flex: 1 }}>
      <Group layer={<Paint><Blur blur={10} /></Paint>}>
        <Skottie animation={animation} frame={41} />
      </Group>
    </Canvas>
  );
};

With Color Matrix

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

<Group
  layer={
    <Paint>
      <ColorMatrix
        matrix={[
          0, 0, 0, 0, 1,
          0, 1, 0, 0, 0,
          0, 0, 1, 0, 0,
          0, 0, 0, 1, 0,
        ]}
      />
    </Paint>
  }
>
  <Skottie animation={animation} frame={frame} />
</Group>

Direct Rendering (Advanced)

For advanced use cases, render directly to a surface:
import { Skia } from "@shopify/react-native-skia";

const surface = Skia.Surface.MakeOffscreen(800, 600);
if (!surface) {
  throw new Error("Failed to create surface");
}

const canvas = surface.getCanvas();

// Seek to specific frame
animation.seekFrame(41);

// Render
animation.render(canvas);

surface.flush();
const image = surface.makeImageSnapshot();

Performance Tips

  • Cache animations outside render when possible
  • Reuse animation instances for multiple components
  • Use appropriate scaling to avoid large draw areas
  • Limit the number of simultaneous animations
  • Consider using static frames for non-animated states
  • Optimize Lottie files before exporting from After Effects

Troubleshooting

Animation Not Loading

  • Verify JSON is valid Lottie format
  • Check that Skia.Skottie.Make() didn’t return null
  • Ensure JSON is stringified before passing to Make()

Missing Assets

  • Provide all external assets in the assets object
  • Check asset names match exactly
  • Verify font and image data is properly loaded

Performance Issues

  • Reduce animation complexity in After Effects
  • Scale down animation dimensions
  • Limit frame rate
  • Use fewer simultaneous animations
  • Consider using video for complex animations

Property Changes Not Visible

  • Set properties before rendering first frame
  • Verify property keys match animation structure
  • Check property value types and ranges

Common Patterns

Loading State

const LoadableSkottie = () => {
  const [animation, setAnimation] = useState<SkSkottieAnimation | null>(null);
  
  useEffect(() => {
    const json = require("./assets/animation.json");
    const anim = Skia.Skottie.Make(JSON.stringify(json));
    setAnimation(anim);
  }, []);
  
  if (!animation) {
    return <Text>Loading...</Text>;
  }
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Skottie animation={animation} frame={0} />
    </Canvas>
  );
};

Controlled Playback

const ControlledSkottie = () => {
  const [isPlaying, setIsPlaying] = useState(true);
  const clock = useClock({ paused: !isPlaying });
  
  const frame = useDerivedValue(() => {
    if (!isPlaying) return 0;
    const fps = animation.fps();
    const duration = animation.duration();
    return Math.floor((clock.value / 1000) * fps) % (duration * fps);
  });
  
  return (
    <>
      <Canvas style={{ flex: 1 }}>
        <Skottie animation={animation} frame={frame} />
      </Canvas>
      <Button 
        title={isPlaying ? "Pause" : "Play"}
        onPress={() => setIsPlaying(!isPlaying)}
      />
    </>
  );
};

See Also

Build docs developers (and LLMs) love