Hero Animations
Creating seamless shared element transitions between screens using Flutter's Hero widget.
Hero animations (also called shared element transitions ) are a type of animation where a widget appears to "fly" from one screen to another. You've probably seen this many times — for example, tapping a product image in a list, and watching it smoothly transition to a larger version on the details screen.
💡 Key Concept
A Hero animation creates the illusion that a widget is
shared
between
two screens. In reality, there are two separate
Hero
widgets with matching
tags
. Flutter handles the animation between them automatically.
Hero animations are implemented using two
Hero
widgets — one on the source route
and one on the destination route. Both must have the
same tag
.
// SOURCE ROUTE (List Screen)
Hero(
tag:
'product_123'
,
// Unique tag
child: Container(
width:
100
,
height:
100
,
child: Image.network(
'https://example.com/product.jpg'
),
),
)
// DESTINATION ROUTE (Detail Screen)
Hero(
tag:
'product_123'
,
// SAME tag!
child: Container(
width:
300
,
height:
300
,
child: Image.network(
'https://example.com/product.jpg'
),
),
)
✅ Key Requirements
- Same tag – Both Hero widgets must have identical tags
- Similar child widgets – The widgets should be visually similar
- Navigation triggers – The animation starts when you push/pop a route
- Unique tags – Each pair of heroes should have a unique tag
A standard hero animation flies a widget from one route to another, usually changing position and size. Here's a complete example:
class
PhotoHero
extends
StatelessWidget {
const
PhotoHero
({
super
.key,
required
this
.photo,
this
.onTap,
required
this
.width,
});
final
String photo;
final
VoidCallback? onTap;
final
double
width;
@override
Widget
build
(BuildContext context) {
return
SizedBox(
width: width,
child: Hero(
tag: photo,
// Use the photo path as the tag
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
),
),
);
}
}
class
HomeScreen
extends
StatelessWidget {
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(title: Text(
'Hero Animation'
)),
body: Center(
child: PhotoHero(
photo:
'assets/flippers.png'
,
width:
300.0
,
onTap: () {
Navigator.
of
(context).
push
(
MaterialPageRoute<
void
>(
builder: (context) {
return
DetailScreen(photo:
'assets/flippers.png'
);
},
),
);
},
),
),
);
}
}
class
DetailScreen
extends
StatelessWidget {
final
String photo;
DetailScreen
({
required
this
.photo});
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(title: Text(
'Detail'
)),
body: Container(
color: Colors.lightBlueAccent,
padding: EdgeInsets.
all
(
16
),
alignment: Alignment.topLeft,
child: PhotoHero(
photo: photo,
width:
100.0
,
onTap: () {
Navigator.
of
(context).
pop
();
},
),
),
);
}
}
💡 Debugging Tip
Use
timeDilation = 5.0
to slow down animations during development.
This makes it easier to see what's happening during the transition.
Understanding what happens behind the scenes helps you debug and optimize hero animations.
🧠 Behind the Scenes
-
Flutter calculates the
flight path
using
Tween<Rect> -
By default, uses
MaterialRectArcTweenfor curved motion - The hero is rendered in an overlay during the animation
- Both source and destination widgets remain in their routes
- The animation is reversible when you pop the route
// Custom RectTween for different flight behavior
static
RectTween
_createRectTween
(Rect? begin, Rect? end) {
return
MaterialRectCenterArcTween(begin: begin, end: end);
}
// Using in Hero widget
Hero(
tag: photo,
createRectTween: _createRectTween,
// Custom flight path
child:
// ...
)
A radial hero animation transforms the shape of the hero during flight — for example, from a circle to a square . This creates a more dramatic, polished transition.
import
'dart:math'
as
math;
class
RadialExpansion
extends
StatelessWidget {
const
RadialExpansion
({
super
.key,
required
this
.maxRadius,
this
.child,
}) : clipRectSize =
2.0
* (maxRadius / math.
sqrt
(
2
));
final
double
maxRadius;
final
double
clipRectSize;
final
Widget? child;
@override
Widget
build
(BuildContext context) {
return
ClipOval(
child: Center(
child: SizedBox(
width: clipRectSize,
height: clipRectSize,
child: ClipRect(
child: child,
),
),
),
);
}
}
class
Photo
extends
StatelessWidget {
const
Photo
({
super
.key,
required
this
.photo,
this
.color,
required
this
.onTap,
});
final
String photo;
final
Color? color;
final
VoidCallback onTap;
@override
Widget
build
(BuildContext context) {
return
Material(
// Slightly opaque for transparency areas
color: Theme.
of
(context).primaryColor.
withValues
(alpha:
0.25
),
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
);
}
}
class
RadialHeroScreen
extends
StatelessWidget {
static
RectTween
_createRectTween
(Rect? begin, Rect? end) {
return
MaterialRectCenterArcTween(begin: begin, end: end);
}
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(title: Text(
'Radial Hero'
)),
body: Center(
child: Hero(
tag:
'hero_photo'
,
createRectTween: _createRectTween,
child: RadialExpansion(
maxRadius:
150
,
child: Photo(
photo:
'assets/photo.jpg'
,
onTap: () {
Navigator.
of
(context).
pop
();
},
),
),
),
),
);
}
}
💡 How Radial Heroes Work
- Uses ClipOval and ClipRect together
- The intersection of the two clips creates the shape change
- ClipOval scales from minRadius to maxRadius
- ClipRect maintains constant size
- The result is a circle that "grows" into a square
-
Use
MaterialRectCenterArcTweento maintain aspect ratio
Follow these steps to add hero animations to your app:
Hero
on the source screen
Hero
on the destination screen
timeDilation
to debug and adjust
If the tags don't match exactly, the hero animation won't work. The hero will just pop in/out.
Always use the exact same tag on both Hero widgets. Use a unique identifier from your data model (e.g., product ID).
If the hero's child widgets are too different, the transition can look jarring.
The heroes should have virtually identical widget trees for the smoothest animation. The same image with different sizes works well.
If you're using custom animations with controllers, forgetting to dispose them causes memory leaks.
Always call
dispose()
on any animation controllers in your stateful widgets.
If multiple heroes on the same screen share the same tag, Flutter doesn't know which one to animate.
Each pair of heroes should have a unique tag . Use the item's ID or a unique string.
📚 Learning Resources
- Flutter Hero Animations Guide – Official documentation
- Flutter Animation Docs – Complete animation guide
- Hero API Reference – Detailed API documentation
🔗 Related Lessons
- Animations – Foundation of Flutter animations
- Custom Widgets – Building reusable widgets
- Module 6 Overview – All advanced topics
🎯 Key Takeaway
Hero animations
are a powerful way to create seamless transitions between
screens in Flutter. By using two
Hero
widgets with
matching tags
,
you can create the illusion of a shared element flying between routes. Use
standard heroes
for simple position/size changes and
radial heroes
for shape transformations. Always use
unique tags
and keep the widget trees
similar for the best results.