Implementing Hero Transition Animations Between Screens in Mobile Applications
A hero transition is when an element from one screen "flies" to another screen, becoming part of it. A product card expands to full-screen detail view, an avatar from a list smoothly moves into a profile header. Users understand the spatial relationship between screens without explanation.
The complexity isn't the animation itself—it's correctly synchronizing the lifecycle of two screens so the element appears to move continuously, not disappear from one screen and appear on another.
Flutter: Hero Widget
Flutter implements hero transitions natively via the Hero widget:
// Product list screen
Hero(
tag: 'product-image-${product.id}', // unique tag
child: CachedNetworkImage(
imageUrl: product.imageUrl,
fit: BoxFit.cover,
),
)
// Product details screen
Hero(
tag: 'product-image-${product.id}',
child: CachedNetworkImage(
imageUrl: product.imageUrl,
fit: BoxFit.contain,
),
)
Navigator.push with any PageRoute automatically starts a hero animation between Hero widgets with the same tag. Duration is 300 ms by default, controlled via transitionDuration in MaterialPageRoute or custom PageRoute.
Issue: if the image in the list is clipped by ClipRRect but not on the detail screen, the shape jumps during transition. Solution: use Hero with flightShuttleBuilder for a custom widget during flight:
Hero(
tag: 'product-image-${product.id}',
flightShuttleBuilder: (_, animation, __, fromCtx, toCtx) {
return AnimatedBuilder(
animation: animation,
builder: (_, child) => ClipRRect(
borderRadius: BorderRadius.lerp(
BorderRadius.circular(12),
BorderRadius.zero,
animation.value,
)!,
child: child,
),
child: CachedNetworkImage(imageUrl: product.imageUrl, fit: BoxFit.cover),
);
},
child: ...,
)
This animates borderRadius from 12 (card) to 0 (full screen) during transition.
React Native: Shared Element Transition
React Native has no native hero transition. Use react-native-shared-element (real native level) or react-navigation v6 with createSharedElementStackNavigator from react-navigation-shared-element:
// Product list
<SharedElement id={`product.${item.id}.image`}>
<Image source={{ uri: item.imageUrl }} style={styles.thumbnail} />
</SharedElement>
// Detail screen
<SharedElement id={`product.${item.id}.image`}>
<Image source={{ uri: item.imageUrl }} style={styles.fullImage} />
</SharedElement>
Navigator configuration:
const Stack = createSharedElementStackNavigator();
<Stack.Screen
name="ProductDetail"
component={ProductDetailScreen}
sharedElements={(route) => [
{ id: `product.${route.params.product.id}.image`, animation: 'move' },
{ id: `product.${route.params.product.id}.title`, animation: 'fade' },
]}
/>
animation: 'move' moves the element. 'fade' cross-fades in place. 'fade-in' appears at the new location. For images use 'move', for text use 'fade' (resizing text looks ugly).
iOS (SwiftUI): matchedGeometryEffect
@Namespace private var heroNamespace
// In product list
Image(product.imageName)
.matchedGeometryEffect(id: "product-image-\(product.id)", in: heroNamespace)
.frame(width: 80, height: 80)
// In detail view (conditional rendering)
if showDetail {
Image(product.imageName)
.matchedGeometryEffect(id: "product-image-\(product.id)", in: heroNamespace)
.frame(width: UIScreen.main.bounds.width, height: 300)
}
matchedGeometryEffect works within one View hierarchy. For modal screens (sheet, fullScreenCover), it's harder and requires custom transitions via AnyTransition with GeometryEffect. For details, see the Matched Geometry Effect service.
Common Issues
Flicker at transition start: element instantly disappears from source screen when animation starts. In Flutter, Hero hides the original via Opacity by default—this is normal. In React Native, SharedElement sometimes shows both elements simultaneously. Fix via useNativeDriver: true and checking library versions.
Different content: image in list is cached thumbnail, on detail screen it's original high-res. While original loads, hero should show thumbnail. In Flutter, Hero always uses the widget from the source screen during flight, then switches. Ensure CachedNetworkImage on both screens uses the same URL for consistent sizing, or explicitly set memCacheWidth.
Timeline
Hero transition for one element type (image or card) on one platform takes 1 day. Multiple hero element types (image + text + icon) with custom shape animation takes 2–3 days. Cost is calculated individually.







