Skip to main content
Pictures provide an immediate-mode drawing API for React Native Skia. While Skia normally works in retained mode, Pictures allow you to execute a variable number of drawing commands, making them perfect for dynamic content that changes frame by frame.

What are Pictures?

A Picture is an immutable recording of drawing operations. Once created, it can be drawn multiple times on any canvas without re-recording. This is ideal when:
  • The number of drawing commands varies per frame
  • You need to execute loops of drawing operations
  • You want to cache complex drawings for reuse
  • You’re creating procedural graphics

Picture Component

The Picture component renders a recorded picture on the canvas.

Props

NameTypeDescription
pictureSkPictureThe picture to render

Basic Example

Here’s a simple example that draws a circle:
import { Canvas, Picture, Skia } from "@shopify/react-native-skia";
import { useMemo } from "react";

const SimplePicture = () => {
  const picture = useMemo(() => {
    const recorder = Skia.PictureRecorder();
    const canvas = recorder.beginRecording(
      Skia.XYWHRect(0, 0, 256, 256)
    );
    
    const paint = Skia.Paint();
    paint.setColor(Skia.Color("lightblue"));
    canvas.drawCircle(128, 128, 100, paint);
    
    return recorder.finishRecordingAsPicture();
  }, []);
  
  return (
    <Canvas style={{ width: 256, height: 256 }}>
      <Picture picture={picture} />
    </Canvas>
  );
};

Animated Circle Trail

This example demonstrates the power of Pictures for variable drawing commands:
import { Canvas, Picture, Skia } from "@shopify/react-native-skia";
import {
  useDerivedValue,
  useSharedValue,
  withRepeat,
  withTiming,
} from "react-native-reanimated";
import { useEffect } from "react";

const size = 256;
const n = 20;
const paint = Skia.Paint();
const recorder = Skia.PictureRecorder();

const CircleTrail = () => {
  const progress = useSharedValue(0);
  
  useEffect(() => {
    progress.value = withRepeat(
      withTiming(1, { duration: 3000 }),
      -1,
      true
    );
  }, []);
  
  const picture = useDerivedValue(() => {
    "worklet";
    const canvas = recorder.beginRecording(
      Skia.XYWHRect(0, 0, size, size)
    );
    
    const numberOfCircles = Math.floor(progress.value * n);
    
    for (let i = 0; i < numberOfCircles; i++) {
      const alpha = ((i + 1) / n) * 255;
      const r = ((i + 1) / n) * (size / 2);
      paint.setColor(
        Skia.Color(`rgba(0, 122, 255, ${alpha / 255})`)
      );
      canvas.drawCircle(size / 2, size / 2, r, paint);
    }
    
    return recorder.finishRecordingAsPicture();
  });
  
  return (
    <Canvas style={{ width: size, height: size }}>
      <Picture picture={picture} />
    </Canvas>
  );
};

Recording Pictures

Creating a Recorder

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

const recorder = Skia.PictureRecorder();

Begin Recording

Define the recording bounds:
const canvas = recorder.beginRecording(
  Skia.XYWHRect(0, 0, width, height)
);

Draw on Canvas

Use the canvas to record drawing commands:
const paint = Skia.Paint();
paint.setColor(Skia.Color("red"));
canvas.drawRect({ x: 0, y: 0, width: 100, height: 100 }, paint);
canvas.drawCircle(50, 50, 25, paint);

Finish Recording

const picture = recorder.finishRecordingAsPicture();

Complex Example: CMY Circles

import { Canvas, Picture, Skia, Group, Paint, Blur } from "@shopify/react-native-skia";
import { useMemo } from "react";
import { BlendMode } from "@shopify/react-native-skia";

const CMYCircles = () => {
  const picture = useMemo(() => {
    const recorder = Skia.PictureRecorder();
    const size = 256;
    const canvas = recorder.beginRecording(
      Skia.XYWHRect(0, 0, size, size)
    );
    const r = 0.33 * size;
    const paint = Skia.Paint();
    paint.setBlendMode(BlendMode.Multiply);
    
    // Cyan circle
    paint.setColor(Skia.Color("cyan"));
    canvas.drawCircle(r, r, r, paint);
    
    // Magenta circle
    paint.setColor(Skia.Color("magenta"));
    canvas.drawCircle(size - r, r, r, paint);
    
    // Yellow circle
    paint.setColor(Skia.Color("yellow"));
    canvas.drawCircle(size / 2, size - r, r, paint);
    
    return recorder.finishRecordingAsPicture();
  }, []);
  
  return (
    <Canvas style={{ width: 256, height: 256 }}>
      <Picture picture={picture} />
    </Canvas>
  );
};

Applying Effects

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

const BlurredPicture = () => {
  const picture = useMemo(() => {
    const recorder = Skia.PictureRecorder();
    const canvas = recorder.beginRecording(
      Skia.XYWHRect(0, 0, 256, 256)
    );
    
    const paint = Skia.Paint();
    paint.setColor(Skia.Color("lightblue"));
    canvas.drawCircle(128, 128, 100, paint);
    
    return recorder.finishRecordingAsPicture();
  }, []);
  
  return (
    <Canvas style={{ width: 256, height: 256 }}>
      <Group layer={<Paint><Blur blur={10} /></Paint>}>
        <Picture picture={picture} />
      </Group>
    </Canvas>
  );
};

With Color Filters

import { Canvas, Picture, Group, Paint, ColorMatrix, Skia } 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>
  }
>
  <Picture picture={picture} />
</Group>

Serialization

Pictures can be serialized for debugging or storage:
import { Canvas, Picture, Skia } from "@shopify/react-native-skia";
import { useMemo } from "react";

const SerializedPicture = () => {
  // Create picture
  const picture = useMemo(() => {
    const recorder = Skia.PictureRecorder();
    const canvas = recorder.beginRecording(
      Skia.XYWHRect(0, 0, 100, 100)
    );
    
    const paint = Skia.Paint();
    paint.setColor(Skia.Color("pink"));
    canvas.drawRect({ x: 0, y: 0, width: 100, height: 100 }, paint);
    
    const circlePaint = Skia.Paint();
    circlePaint.setColor(Skia.Color("orange"));
    canvas.drawCircle(50, 50, 50, circlePaint);
    
    return recorder.finishRecordingAsPicture();
  }, []);
  
  // Serialize
  const serialized = useMemo(
    () => picture.serialize(),
    [picture]
  );
  
  // Deserialize
  const copyOfPicture = useMemo(
    () => serialized ? Skia.Picture.MakePicture(serialized) : null,
    [serialized]
  );
  
  return (
    <Canvas style={{ width: 256, height: 256 }}>
      <Picture picture={picture} />
      <Group transform={[{ translateX: 150 }]}>
        {copyOfPicture && <Picture picture={copyOfPicture} />}
      </Group>
    </Canvas>
  );
};
Note: Serialized pictures are only compatible with the same Skia version. Use with the Skia debugger.

Picture Instance Methods

makeShader

Convert a picture into a shader:
import { TileMode, FilterMode } from "@shopify/react-native-skia";

const shader = picture.makeShader(
  TileMode.Repeat,
  TileMode.Repeat,
  FilterMode.Linear
);

serialize

Export as bytes:
const bytes = picture.serialize();
// Returns Uint8Array or null

Advanced Patterns

Procedural Pattern

const pattern = useMemo(() => {
  const recorder = Skia.PictureRecorder();
  const canvas = recorder.beginRecording(
    Skia.XYWHRect(0, 0, 256, 256)
  );
  
  const paint = Skia.Paint();
  
  for (let x = 0; x < 256; x += 20) {
    for (let y = 0; y < 256; y += 20) {
      const hue = ((x + y) / 512) * 360;
      paint.setColor(Skia.Color(`hsl(${hue}, 70%, 60%)`));
      canvas.drawRect(
        { x, y, width: 18, height: 18 },
        paint
      );
    }
  }
  
  return recorder.finishRecordingAsPicture();
}, []);

Reusable Picture

const StarPicture = ({ picture }) => (
  <>
    <Picture picture={picture} />
    <Group transform={[{ translateX: 100 }]}>
      <Picture picture={picture} />
    </Group>
    <Group transform={[{ translateY: 100 }]}>
      <Picture picture={picture} />
    </Group>
  </>
);

Performance Tips

  • Cache pictures with useMemo when content doesn’t change
  • Reuse Paint instances across recordings
  • Keep recorder instances outside render when possible
  • Use useDerivedValue for animated pictures
  • Serialize expensive pictures for debugging

When to Use Pictures

Use Pictures when:
  • Drawing a variable number of shapes
  • Creating procedural graphics
  • Caching complex drawing operations
  • Building custom components with loops
Use Components when:
  • Drawing a fixed number of shapes
  • Using declarative syntax
  • Leveraging React Native Skia’s animation system
  • Building standard UI elements

See Also

Build docs developers (and LLMs) love