Skip to main content

Animation Model

Screen transitions in react-native-screen-transitions are driven by a continuous progress value that flows through a worklet function called screenStyleInterpolator. The progress value represents the animation state:
  • 0 = Screen B is fully visible (before transition begins)
  • 0–1 = Transition in progress (Screen A visible → Screen B visible)
  • 1 = Screen A is fully hidden, Screen B is fully visible
  • 1–2 = Dismissal in progress (Screen B returns to Screen A)
  • 2 = Back at original state

Slot-Based Return Format

Custom animations use a modular slot-based return format that separates concerns:
const screenStyleInterpolator = ({ progress, bounds, layouts, isReverse }) => {
  "worklet";

  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
    backdrop: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 0.7]),
      },
    },
    surface: {
      style: {
        backgroundColor: 'transparent',
      },
      props: {
        pointerEvents: 'none',
      },
    },
    "my-id": {
      style: {
        transform: [{ scale: interpolate(progress, [0, 1], [0.8, 1]) }],
      },
    },
  };
};
The return object can have any of these slots:
SlotPurposeType
contentMain screen content layer{ style: StyleProp<ViewStyle> }
backdropSemi-transparent backdrop/dimming{ style: StyleProp<ViewStyle> }
surfaceCustom surface layer (with surfaceComponent){ style: ..., props: ... }
["id"]Custom elements registered with styleId{ style: StyleProp<ViewStyle> }
Each slot accepts style and optional props to pass to the underlying component.

Basic Opacity Fade

Here’s a simple fade transition:
const fadeInterpolator = ({ progress }) => {
  "worklet";
  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
    backdrop: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 0.5]),
      },
    },
  };
};

<Transition.Stack screenStyleInterpolator={fadeInterpolator}>
  {/* screens */}
</Transition.Stack>

Transform Animations

Combine multiple transforms for complex motion:
const slideAndScaleInterpolator = ({ progress }) => {
  "worklet";
  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0.8, 1]),
        transform: [
          {
            translateY: interpolate(progress, [0, 1], [50, 0]),
          },
          {
            scale: interpolate(progress, [0, 1], [0.95, 1]),
          },
        ],
      },
    },
    backdrop: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 0.7]),
      },
    },
  };
};

Dismissal Animation (Progress 1–2)

Control what happens when the user drags to dismiss:
const dismissalInterpolator = ({ progress, isReverse }) => {
  "worklet";
  const outputRange = isReverse
    ? [1, 2]
    : [0, 1];

  return {
    content: {
      style: {
        opacity: interpolate(progress, outputRange, [1, 0]),
        transform: [
          {
            translateY: interpolate(progress, outputRange, [0, 100]),
          },
        ],
      },
    },
  };
};

Interpolator Props

Your interpolator function receives an object with these properties:
PropTypeDescription
progressAnimated.SharedValue<number>Current animation progress (0–2)
bounds(options) => BoundsResultFunction to query shared element bounds
layouts{ content?: LayoutRect }Measured layout dimensions
isReversebooleanTrue if this is a dismissal (progress 1–2)

Progress Interpolation

Use Reanimated’s interpolate to map progress to animation values:
import { interpolate } from 'react-native-reanimated';

// Simple mapping: 0→0, 1→1
interpolate(progress, [0, 1], [0, 1])

// Three-point mapping: 0→0, 0.5→1, 1→0 (bell curve)
interpolate(progress, [0, 0.5, 1], [0, 1, 0])

// Dismissal support: 0→0, 1→1, 2→0
interpolate(progress, [0, 1, 2], [0, 1, 0])

// Non-linear easing
interpolate(progress, [0, 1], [0, 1], Extrapolate.CLAMP)

Layout-Aware Animations

Access measured layout dimensions to create responsive animations:
const responsiveInterpolator = ({ progress, layouts }) => {
  "worklet";
  const screenHeight = layouts.content?.height ?? 600;

  return {
    content: {
      style: {
        transform: [
          {
            translateY: interpolate(progress, [0, 1], [screenHeight, 0]),
          },
        ],
      },
    },
  };
};
For snap points with "auto", layouts.content.height contains the intrinsic content height.

Shared Element Animations with Bounds

The bounds() function queries the animation state of shared elements:
const sharedElementInterpolator = ({ progress, bounds }) => {
  "worklet";

  const avatarBounds = bounds({ id: 'avatar' });
  const scaleFactor = avatarBounds
    ? avatarBounds.scale
    : 1;

  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
  };
};

Bounds Result Object

The bounds() function returns:
interface BoundsResult {
  x: number;
  y: number;
  width: number;
  height: number;
  scale: number;

  // Full transform matrices
  transform: { translate: [x, y], scale: [sx, sy] };

  // Navigation-style zoom
  navigation: {
    zoom(options?: ZoomOptions): AnimationResult;
  };

  // Anchoring to specific points
  anchor(point: "center" | "topLeft" | ...): AnimationResult;

  // Get snapshot of bounds without animation
  getSnapshot(id: string): LayoutRect | null;
}
Animate a shared element from its source position to fullscreen:
const zoomInterpolator = ({ bounds }) => {
  "worklet";
  return bounds({ id: "hero" }).navigation.zoom({
    target: "fullscreen",
  });
};
This generates optimized transforms for expanding an element to fill the screen while maintaining its aspect ratio.

Deferred First Frames

Return "defer" to delay animation startup until bounds are available:
const deferredInterpolator = ({ progress, bounds }) => {
  "worklet";

  // Wait for bounds to be measured
  const snapshot = bounds.getSnapshot("hero");
  if (!snapshot) return "defer";

  // Now safe to animate
  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
  };
};
Useful when shared element bounds must be measured before animation can proceed smoothly.

Conditionals and Logic

Interpolators are worklets, so you can use normal JavaScript:
const conditionalInterpolator = ({ progress, isReverse, bounds }) => {
  "worklet";

  const hasBounds = bounds.getSnapshot("hero") !== null;

  if (isReverse) {
    // Different animation for dismissal
    return {
      content: {
        style: {
          opacity: interpolate(progress, [1, 2], [1, 0]),
        },
      },
    };
  }

  if (hasBounds) {
    // Animate with bounds
    return bounds({ id: "hero" }).navigation.zoom({
      target: "fullscreen",
    });
  }

  // Fallback animation
  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
  };
};

Style ID for Individual Elements

Animate specific elements within a screen using styleId:
// In your screen component
<Transition.View styleId="title">
  <Text style={{ fontSize: 24 }}>Details</Text>
</Transition.View>

// In your interpolator
const multiElementInterpolator = ({ progress }) => {
  "worklet";
  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
    "title": {
      style: {
        transform: [
          {
            translateY: interpolate(progress, [0, 1], [20, 0]),
          },
        ],
      },
    },
  };
};

Performance Optimization

Use Reanimated Utilities

Always use Reanimated functions inside worklets:
import { interpolate, Extrapolate } from 'react-native-reanimated';

// ✅ Good
interpolate(progress, [0, 1], [0, 1], Extrapolate.CLAMP)

// ❌ Avoid
progress.value > 0.5 ? 1 : 0 // Non-reactive

Avoid Excessive Recalculations

Cache frequently used values:
const screenStyleInterpolator = ({ progress, layouts }) => {
  "worklet";
  const height = layouts.content?.height ?? 600;

  return {
    content: {
      style: {
        transform: [
          { translateY: interpolate(progress, [0, 1], [height, 0]) },
          { scale: interpolate(progress, [0, 1], [0.9, 1]) },
        ],
      },
    },
  };
};

Minimize Shader Compilation

Complex animations trigger more shader compilation. Keep transforms simple when possible.

Troubleshooting

Animation Jumps or Jitters

Ensure you’re using interpolate and not JavaScript conditionals:
// ❌ Causes jitter
opacity: progress.value > 0.5 ? 1 : 0

// ✅ Smooth interpolation
opacity: interpolate(progress, [0, 0.5, 1], [0, 0, 1])

Layout Not Available

If layouts.content is undefined, you may need to measure the content. Use "defer" to wait:
if (!layouts.content?.height) return "defer";

Bounds Snapshot Null

Shared elements may not be measured yet. Use bounds.getSnapshot() to check:
const snapshot = bounds.getSnapshot("avatar");
if (!snapshot) return "defer";

Next Steps