Hero Transitions: Shared Element Navigation
Hero Transitions: Shared Element Navigation
A Hero transition is Flutter's built-in shared-element animation: a widget visually "flies" from its position on one route to its position on the destination route during a navigation push or pop. The effect makes the relationship between screens instantly understandable — a thumbnail expands into a full detail view, a product card morphs into a product page, or an avatar slides from a list into a profile header.
Flutter manages the entire flight automatically. All you do is wrap the same logical widget on both routes inside a Hero widget and give both instances the same tag. The framework detects matching tags at navigation time and orchestrates the animation between them.
Hero widgets on the same screen share a tag, Flutter throws an assertion error in debug mode. For list-to-detail flows, use the item's unique ID as the tag (e.g. 'hero-product-\${product.id}').Minimal Hero Example
Wrapping a widget with Hero takes three lines: the widget itself, a tag, and matching tags on both routes. The hero on the source route is called the source hero; the one on the destination is the destination hero.
Example 1 — Basic Hero: List to Detail
// ─── Source route: product grid ───────────────────────────────────
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProductDetailPage(product: product),
),
);
},
child: Hero(
tag: 'product-image-\${product.id}', // unique per item
child: Image.network(
product.imageUrl,
width: 120,
height: 120,
fit: BoxFit.cover,
),
),
)
// ─── Destination route: product detail ────────────────────────────
Hero(
tag: 'product-image-\${product.id}', // same tag
child: Image.network(
product.imageUrl,
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
)
During the push, Flutter calculates the start size/position (120×120, grid cell) and the end size/position (full-width, 300 px tall). It then lifts the hero widget out of both routes and animates it in an overlay, resizing and repositioning it across the screen while both routes fade. On pop, the animation reverses automatically.
How Flutter Orchestrates the Flight
Understanding the mechanics helps you avoid common pitfalls:
- At navigation time, Flutter finds every
Herowith a matching tag in the old and new route. - It places a placeholder in both routes (so layout does not shift) and renders the hero itself in a full-screen overlay above both routes.
- An
AnimationControllerdriven by the route transition drives the hero from its source rect to its destination rect. - The overlay widget is built using the destination hero's child by default (you can override this with
flightShuttleBuilder). - On completion the hero is "landed" into the destination route and the overlay is removed.
InheritedWidget values (like Theme or MediaQuery) that differ between routes. If your hero uses Theme.of(context), wrap it in an explicit Theme widget so the correct theme travels with it.Customising the Flight with flightShuttleBuilder
By default, the widget displayed during flight is the destination hero's child. flightShuttleBuilder lets you substitute a completely different widget for the duration of the flight — useful for morphing shapes, showing a loading state, or blending both the source and destination visuals.
Example 2 — Custom Flight Widget with flightShuttleBuilder
Hero(
tag: 'avatar-\${user.id}',
// The widget rendered DURING the flight (not the source or destination child)
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
// Crossfade between source and destination children
return AnimatedBuilder(
animation: animation,
builder: (context, _) {
final Widget fromChild =
(fromHeroContext.widget as Hero).child;
final Widget toChild =
(toHeroContext.widget as Hero).child;
return Stack(
fit: StackFit.expand,
children: [
Opacity(opacity: 1.0 - animation.value, child: fromChild),
Opacity(opacity: animation.value, child: toChild),
],
);
},
);
},
child: CircleAvatar(
backgroundImage: NetworkImage(user.avatarUrl),
radius: 24,
),
)
The five parameters of flightShuttleBuilder give you everything you need: the animation value (0.0 → 1.0 on push, 1.0 → 0.0 on pop), the flight direction, and both hero contexts so you can extract and blend the source and destination children.
Controlling Placeholder Behaviour
While the hero is in flight, a placeholder occupies its position in each route's layout. By default this placeholder is a transparent box with the same size as the hero. You can customise it with placeholderBuilder:
placeholderBuilder: (context, heroSize, child) => SizedBox.fromSize(size: heroSize)— invisible box (default-like).- Supply a shimmer or a blurred copy of the image to prevent jarring layout shifts on the source page while the hero is mid-flight.
Hero inside a widget that clips its children (e.g. a ClipRRect wrapping the Hero). Clipping prevents the hero from rendering in the overlay. Instead, apply any clipping inside the hero's child, or use Hero's own flightShuttleBuilder to animate the border-radius during flight.Animating Border Radius During Flight
A popular effect is animating from a circular avatar (small route) to a rectangular image (detail route). Because the hero's size is interpolated automatically, you only need to animate the border-radius inside flightShuttleBuilder using the animation value:
- Use
BorderRadiusTweeninsideflightShuttleBuilderto lerp betweenBorderRadius.circular(50)andBorderRadius.zero. - Wrap the result in a
ClipRRectwith the tweened radius.
Summary
The Hero widget makes shared-element transitions effortless: match tags on both routes and Flutter handles the flight. For custom flight visuals, supply a flightShuttleBuilder; for custom placeholders, use placeholderBuilder. Always ensure tags are unique per route and avoid wrapping Hero inside clipping widgets. With these tools you can create the polished screen-to-screen animations that define professional-grade Flutter applications.