Skip to main content
React Native Skia works seamlessly with React Native Gesture Handler to create interactive, touch-driven graphics and animations.

Installation

Install React Native Gesture Handler:
npm install react-native-gesture-handler
Follow the installation guide to complete setup.

Basic Gesture Example

import { Canvas, Circle } from "@shopify/react-native-skia";
import { GestureDetector, Gesture } from "react-native-gesture-handler";
import { useSharedValue } from "react-native-reanimated";

const DraggableCircle = () => {
  const translateX = useSharedValue(128);
  const translateY = useSharedValue(128);

  const gesture = Gesture.Pan()
    .onChange((e) => {
      translateX.value += e.changeX;
      translateY.value += e.changeY;
    });

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

Pan Gesture

Drag and move elements:
import { GestureDetector, Gesture } from "react-native-gesture-handler";
import { useSharedValue, withDecay } from "react-native-reanimated";

const PanDemo = () => {
  const x = useSharedValue(100);
  const y = useSharedValue(100);

  const pan = Gesture.Pan()
    .onChange((e) => {
      x.value += e.changeX;
      y.value += e.changeY;
    })
    .onEnd((e) => {
      // Add momentum with decay
      x.value = withDecay({
        velocity: e.velocityX,
        clamp: [40, 216],
      });
      y.value = withDecay({
        velocity: e.velocityY,
        clamp: [40, 216],
      });
    });

  return (
    <GestureDetector gesture={pan}>
      <Canvas style={{ width: 256, height: 256 }}>
        <Circle cx={x} cy={y} r={40} color="purple" />
      </Canvas>
    </GestureDetector>
  );
};

Tap Gesture

Handle taps and touches:
import { Gesture } from "react-native-gesture-handler";
import { withSpring } from "react-native-reanimated";

const TapDemo = () => {
  const scale = useSharedValue(1);

  const tap = Gesture.Tap()
    .onStart(() => {
      scale.value = withSpring(1.3);
    })
    .onEnd(() => {
      scale.value = withSpring(1);
    });

  return (
    <GestureDetector gesture={tap}>
      <Canvas style={{ width: 256, height: 256 }}>
        <Group transform={[{ scale }]}>
          <Circle cx={128} cy={128} r={40} color="red" />
        </Group>
      </Canvas>
    </GestureDetector>
  );
};

Pinch Gesture

Scale elements with two fingers:
import { Gesture } from "react-native-gesture-handler";
import { useSharedValue } from "react-native-reanimated";

const PinchDemo = () => {
  const scale = useSharedValue(1);
  const savedScale = useSharedValue(1);

  const pinch = Gesture.Pinch()
    .onUpdate((e) => {
      scale.value = savedScale.value * e.scale;
    })
    .onEnd(() => {
      savedScale.value = scale.value;
    });

  return (
    <GestureDetector gesture={pinch}>
      <Canvas style={{ width: 256, height: 256 }}>
        <Group transform={[{ scale }]}>
          <Image image={myImage} x={0} y={0} width={256} height={256} />
        </Group>
      </Canvas>
    </GestureDetector>
  );
};

Rotation Gesture

Rotate elements with two fingers:
import { Gesture } from "react-native-gesture-handler";

const RotationDemo = () => {
  const rotation = useSharedValue(0);
  const savedRotation = useSharedValue(0);

  const rotate = Gesture.Rotation()
    .onUpdate((e) => {
      rotation.value = savedRotation.value + e.rotation;
    })
    .onEnd(() => {
      savedRotation.value = rotation.value;
    });

  return (
    <GestureDetector gesture={rotate}>
      <Canvas style={{ width: 256, height: 256 }}>
        <Group transform={[{ rotate: rotation }]}>
          <RoundedRect 
            x={64} 
            y={64} 
            width={128} 
            height={128} 
            r={20} 
            color="green" 
          />
        </Group>
      </Canvas>
    </GestureDetector>
  );
};

Combining Gestures

Use Gesture.Simultaneous to enable multiple gestures at once:
import { Gesture } from "react-native-gesture-handler";

const CombinedGesturesDemo = () => {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const scale = useSharedValue(1);
  const rotation = useSharedValue(0);

  const savedScale = useSharedValue(1);
  const savedRotation = useSharedValue(0);

  const pan = Gesture.Pan()
    .onChange((e) => {
      translateX.value += e.changeX;
      translateY.value += e.changeY;
    });

  const pinch = Gesture.Pinch()
    .onUpdate((e) => {
      scale.value = savedScale.value * e.scale;
    })
    .onEnd(() => {
      savedScale.value = scale.value;
    });

  const rotate = Gesture.Rotation()
    .onUpdate((e) => {
      rotation.value = savedRotation.value + e.rotation;
    })
    .onEnd(() => {
      savedRotation.value = rotation.value;
    });

  const composed = Gesture.Simultaneous(pan, pinch, rotate);

  return (
    <GestureDetector gesture={composed}>
      <Canvas style={{ width: 256, height: 256 }}>
        <Group 
          transform={[
            { translateX },
            { translateY },
            { scale },
            { rotate: rotation },
          ]}
        >
          <Image image={myImage} x={0} y={0} width={256} height={256} />
        </Group>
      </Canvas>
    </GestureDetector>
  );
};

Interactive Drawing

Create a drawing app:
import { Canvas, Path, Skia } from "@shopify/react-native-skia";
import { Gesture } from "react-native-gesture-handler";
import { useState } from "react";

const DrawingDemo = () => {
  const [paths, setPaths] = useState<SkPath[]>([]);
  const [currentPath, setCurrentPath] = useState<SkPath | null>(null);

  const pan = Gesture.Pan()
    .onStart((e) => {
      const path = Skia.Path.Make();
      path.moveTo(e.x, e.y);
      setCurrentPath(path);
    })
    .onChange((e) => {
      if (currentPath) {
        currentPath.lineTo(e.x, e.y);
        setCurrentPath(currentPath.copy());
      }
    })
    .onEnd(() => {
      if (currentPath) {
        setPaths([...paths, currentPath]);
        setCurrentPath(null);
      }
    });

  return (
    <GestureDetector gesture={pan}>
      <Canvas style={{ flex: 1, backgroundColor: "white" }}>
        {paths.map((path, index) => (
          <Path
            key={index}
            path={path}
            color="black"
            style="stroke"
            strokeWidth={3}
            strokeCap="round"
            strokeJoin="round"
          />
        ))}
        {currentPath && (
          <Path
            path={currentPath}
            color="black"
            style="stroke"
            strokeWidth={3}
            strokeCap="round"
            strokeJoin="round"
          />
        )}
      </Canvas>
    </GestureDetector>
  );
};

Velocity-Based Animations

Use gesture velocity for natural animations:
import { Gesture } from "react-native-gesture-handler";
import { withDecay } from "react-native-reanimated";

const VelocityDemo = () => {
  const translateX = useSharedValue(0);

  const pan = Gesture.Pan()
    .onChange((e) => {
      translateX.value += e.changeX;
    })
    .onEnd((e) => {
      translateX.value = withDecay({
        velocity: e.velocityX,
        clamp: [0, 200],
      });
    });

  return (
    <GestureDetector gesture={pan}>
      <Canvas style={{ width: 256, height: 256 }}>
        <Circle cx={translateX} cy={128} r={30} color="orange" />
      </Canvas>
    </GestureDetector>
  );
};

Gesture State

React to gesture state changes:
import { Gesture, State } from "react-native-gesture-handler";
import { withTiming } from "react-native-reanimated";

const GestureStateDemo = () => {
  const opacity = useSharedValue(1);
  const scale = useSharedValue(1);

  const pan = Gesture.Pan()
    .onBegin(() => {
      opacity.value = withTiming(0.6);
      scale.value = withTiming(1.1);
    })
    .onChange((e) => {
      // Handle pan...
    })
    .onFinalize(() => {
      opacity.value = withTiming(1);
      scale.value = withTiming(1);
    });

  return (
    <GestureDetector gesture={pan}>
      <Canvas style={{ width: 256, height: 256 }}>
        <Group opacity={opacity} transform={[{ scale }]}>
          <Circle cx={128} cy={128} r={50} color="cyan" />
        </Group>
      </Canvas>
    </GestureDetector>
  );
};

Snapping

Snap to specific positions:
import { Gesture } from "react-native-gesture-handler";
import { withTiming } from "react-native-reanimated";

const SnapDemo = () => {
  const translateX = useSharedValue(64);
  const snapPoints = [64, 128, 192];

  const findNearestSnap = (value: number) => {
    'worklet';
    return snapPoints.reduce((prev, curr) =>
      Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
    );
  };

  const pan = Gesture.Pan()
    .onChange((e) => {
      translateX.value += e.changeX;
    })
    .onEnd(() => {
      const nearest = findNearestSnap(translateX.value);
      translateX.value = withTiming(nearest, { duration: 200 });
    });

  return (
    <GestureDetector gesture={pan}>
      <Canvas style={{ width: 256, height: 256 }}>
        {/* Snap guides */}
        {snapPoints.map((x, i) => (
          <Circle key={i} cx={x} cy={128} r={4} color="lightgray" />
        ))}
        {/* Draggable element */}
        <Circle cx={translateX} cy={128} r={20} color="blue" />
      </Canvas>
    </GestureDetector>
  );
};

Hit Testing

Detect if a touch is within a specific shape:
const isPointInCircle = (
  px: number,
  py: number,
  cx: number,
  cy: number,
  r: number
) => {
  'worklet';
  const dx = px - cx;
  const dy = py - cy;
  return dx * dx + dy * dy <= r * r;
};

const tap = Gesture.Tap()
  .onStart((e) => {
    if (isPointInCircle(e.x, e.y, 128, 128, 50)) {
      // Handle tap on circle
      scale.value = withSpring(1.2);
    }
  });

Long Press

Handle long press gestures:
import { Gesture } from "react-native-gesture-handler";
import { withSequence, withTiming } from "react-native-reanimated";

const LongPressDemo = () => {
  const scale = useSharedValue(1);

  const longPress = Gesture.LongPress()
    .minDuration(500)
    .onStart(() => {
      scale.value = withSequence(
        withTiming(1.3, { duration: 100 }),
        withTiming(1, { duration: 100 })
      );
    });

  return (
    <GestureDetector gesture={longPress}>
      <Canvas style={{ width: 256, height: 256 }}>
        <Group transform={[{ scale }]}>
          <Circle cx={128} cy={128} r={50} color="indigo" />
        </Group>
      </Canvas>
    </GestureDetector>
  );
};

Gesture Boundaries

Constrain gestures to specific bounds:
const clamp = (value: number, min: number, max: number) => {
  'worklet';
  return Math.min(Math.max(value, min), max);
};

const pan = Gesture.Pan()
  .onChange((e) => {
    translateX.value = clamp(translateX.value + e.changeX, 0, 200);
    translateY.value = clamp(translateY.value + e.changeY, 0, 200);
  });

Performance Tips

  • Use onChange instead of onUpdate for better performance
  • Minimize state updates - use shared values instead
  • Use runOnJS sparingly to call JS functions from gestures
  • Batch related value updates together
  • Use withDecay for natural momentum scrolling

Build docs developers (and LLMs) love