Skip to main content

Surface Slots Overview

The slot-based return format separates animation concerns into distinct layers. Each slot targets a specific visual component:
return {
  content: { style: { ... } },      // Main screen content
  backdrop: { style: { ... } },     // Semi-transparent backdrop
  surface: { style: { ... }, props: { ... } },  // Custom surface layer
  ["styleId"]: { style: { ... } },  // Elements with styleId prop
};
This modular approach is cleaner than the flat contentStyle/backdropStyle format and scales well for complex animations.

Content Slot

Animates the main screen content layer:
return {
  content: {
    style: {
      opacity: interpolate(progress, [0, 1], [0, 1]),
      transform: [
        {
          translateY: interpolate(progress, [0, 1], [100, 0]),
        },
      ],
    },
  },
};
The content slot wraps the entire screen’s view tree and receives style updates each frame.

Backdrop Slot

Animates a semi-transparent backdrop layer that appears behind the content:
return {
  backdrop: {
    style: {
      opacity: interpolate(progress, [0, 1], [0, 0.7]),
      backgroundColor: interpolate(
        progress,
        [0, 1],
        ["rgba(0,0,0,0)", "rgba(0,0,0,0.7)"]
      ),
    },
  },
};
The backdrop is useful for:
  • Dimming the previous screen
  • Creating depth perception
  • Preventing interaction with underlying screens

Surface Slot

Animates a custom surface layer, optionally using a custom component:
return {
  surface: {
    style: {
      backgroundColor: interpolate(
        progress,
        [0, 1],
        ["transparent", "rgba(255,255,255,0.1)"]
      ),
    },
    props: {
      pointerEvents: "none",  // Pass props to surfaceComponent
    },
  },
};
To use a custom surface component, provide surfaceComponent:
<Transition.Stack
  screenStyleInterpolator={({ progress }) => ({
    content: {
      style: { opacity: 1 },
    },
    surface: {
      style: { backgroundColor: 'rgba(0,0,0,0.2)' },
      props: { testID: 'custom-surface' },
    },
  })}
  surfaceComponent={({ style, ...props }) => (
    <View
      {...props}
      style={[
        style,
        {
          // Custom styling
          borderTopLeftRadius: 20,
          borderTopRightRadius: 20,
        },
      ]}
    />
  )}
>
  {/* screens */}
</Transition.Stack>

StyleId Slot

Animate specific elements within the screen using the styleId prop:
// In your screen component
<Transition.View styleId="header">
  <Text style={{ fontSize: 24, fontWeight: 'bold' }}>Details</Text>
</Transition.View>

<Transition.View styleId="description">
  <Text>Description text here</Text>
</Transition.View>

// In your interpolator
return {
  content: {
    style: {
      opacity: interpolate(progress, [0, 1], [0, 1]),
    },
  },
  "header": {
    style: {
      transform: [
        {
          translateY: interpolate(progress, [0, 1], [30, 0]),
        },
      ],
    },
  },
  "description": {
    style: {
      opacity: interpolate(progress, [0, 1], [0, 1]),
    },
  },
};
Multiple elements can be animated independently using their styleIds.

Passing Props via Slots

Each slot can pass props to its underlying component:
return {
  content: {
    style: { opacity: 1 },
    props: {
      pointerEvents: "auto",
    },
  },
  backdrop: {
    style: { opacity: 0.5 },
    props: {
      onPress: () => console.log("Backdrop tapped"),
      testID: "backdrop",
    },
  },
  surface: {
    style: { backgroundColor: "blue" },
    props: {
      accessible: true,
      accessibilityLabel: "Surface layer",
    },
  },
};

Combined Slot Example

A complete animation using all slots:
const fullInterpolator = ({ 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]),
          },
        ],
      },
      props: {
        pointerEvents: interpolate(progress, [0, 1], [0, 1]) > 0.5 ? "auto" : "none",
      },
    },
    backdrop: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 0.6]),
      },
      props: {
        onPress: () => {
          // Handle backdrop press
        },
      },
    },
    surface: {
      style: {
        backgroundColor: interpolate(
          progress,
          [0, 1],
          ["transparent", "rgba(0,0,0,0.1)"]
        ),
      },
      props: {
        pointerEvents: "none",
      },
    },
    "title": {
      style: {
        fontSize: interpolate(progress, [0, 1], [18, 24]),
        fontWeight: interpolate(progress, [0, 0.5, 1], ["400", "600", "700"]),
      },
    },
  };
};

Deferred First Frames

Return "defer" to delay rendering until conditions are met:
const screenStyleInterpolator = ({ progress, bounds }) => {
  "worklet";

  // Wait until shared element bounds are measured
  const snapshot = bounds.getSnapshot("hero");
  if (!snapshot) {
    return "defer";
  }

  // Now animate
  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
  };
};
Deferral is useful when:
  • Shared elements need measurement before animation
  • Layout dimensions aren’t yet available
  • Conditional animation requires external state

Dismissal Animation (Progress 1–2)

Handle dismissal with different slot animation:
return {
  content: {
    style: {
      opacity: interpolate(progress, [0, 1, 2], [0, 1, 0]),
      transform: [
        {
          translateY: interpolate(progress, [0, 1, 2], [100, 0, 100]),
        },
      ],
    },
  },
  backdrop: {
    style: {
      opacity: interpolate(progress, [0, 1, 2], [0, 0.7, 0]),
    },
  },
};
Progress 1–2 represents dismissal:
  • At progress 1: Fully presented
  • At progress 2: Back to initial state (dismissed)

Deprecated Flat Format

The old flat return format is deprecated:
// ❌ Deprecated
return {
  contentStyle: { opacity: 1 },
  backdropStyle: { opacity: 0.5 },
};

// ✅ New format
return {
  content: { style: { opacity: 1 } },
  backdrop: { style: { opacity: 0.5 } },
};
The flat format still works but should not be used in new code. It lacks the flexibility of the slot-based approach.

Migration from Flat Format

If upgrading from v3.3 flat format:
// Old code
screenStyleInterpolator: ({ progress }) => ({
  contentStyle: {
    opacity: interpolate(progress, [0, 1], [0, 1]),
  },
  backdropStyle: {
    opacity: interpolate(progress, [0, 1], [0, 0.7]),
  },
})

// New code
screenStyleInterpolator: ({ progress }) => ({
  content: {
    style: {
      opacity: interpolate(progress, [0, 1], [0, 1]),
    },
  },
  backdrop: {
    style: {
      opacity: interpolate(progress, [0, 1], [0, 0.7]),
    },
  },
})
Simply nest the flat contentStyle and backdropStyle objects inside style properties within content and backdrop slots.

Custom Surface Components

Define a custom surface renderer to create unique visual effects:
const GlassMorphismSurface = ({ style, ...props }) => (
  <View
    {...props}
    style={[
      style,
      {
        backgroundColor: "rgba(255, 255, 255, 0.1)",
        backdropFilter: "blur(10px)",
        borderRadius: 12,
        borderWidth: 1,
        borderColor: "rgba(255, 255, 255, 0.2)",
      },
    ]}
  />
);

<Transition.Stack
  screenStyleInterpolator={({ progress }) => ({
    content: {
      style: { opacity: interpolate(progress, [0, 1], [0, 1]) },
    },
    surface: {
      style: { backgroundColor: "transparent" },
    },
  })}
  surfaceComponent={GlassMorphismSurface}
>
  {/* screens */}
</Transition.Stack>

Empty Slots

Omit slots you don’t need:
// Only animate content, no backdrop
return {
  content: {
    style: { opacity: interpolate(progress, [0, 1], [0, 1]) },
  },
};

// Only animate backdrop
return {
  backdrop: {
    style: { opacity: interpolate(progress, [0, 1], [0, 0.5]) },
  },
};

// Only animate specific styleId elements
return {
  "title": {
    style: { fontSize: 24 },
  },
  "description": {
    style: { opacity: 1 },
  },
};

Performance Considerations

  • Fewer slots = better performance: Only animate what you need
  • Reuse interpolations: Cache complex calculations
  • Avoid layout-dependent slots: Use fixed values when possible
  • Test on low-end devices: Ensure smooth 60fps animation

Troubleshooting

Slot Not Updating

Ensure the slot name matches exactly:
// ✅ Correct
<Transition.View styleId="title">
  <Text>Title</Text>
</Transition.View>

return {
  "title": { style: { fontSize: 24 } },  // Exact match
};

// ❌ Wrong
return {
  "Title": { style: { fontSize: 24 } },  // Case mismatch
};

Props Not Applied

Props passed via slots apply to the container, not the content:
// These props apply to the View wrapping content
return {
  content: {
    props: { pointerEvents: "none" },
  },
};
For content-specific props, apply them inside your screen component.

Next Steps