Flutter Hero Animations: Creating Seamless Screen Transitions
Have you ever noticed how some mobile apps feel incredibly smooth when navigating between screens? When you tap on a thumbnail and it elegantly expands into a full-screen image, or when a card smoothly transitions to a detail page—that's the magic of Hero animations. In Flutter, Hero animations are one of the most delightful ways to create polished, professional user experiences.
Hero animations automatically animate widgets from one screen to another, creating a visual connection that makes your app feel cohesive and responsive. In this article, we'll explore how Hero animations work, when to use them, and how to implement them effectively in your Flutter applications.
What Are Hero Animations?
A Hero animation in Flutter is a shared element transition between two routes. When you navigate from one screen to another, Flutter automatically animates a widget that exists on both screens, creating a seamless visual transition. The widget "flies" from its position on the source screen to its position on the destination screen.
The name "Hero" comes from the idea that this widget is the "hero" of the transition—it's the element that draws the user's attention and guides them through the navigation flow.
Basic Hero Animation
Creating a Hero animation is surprisingly simple. You wrap a widget in a Hero widget on both screens and give them the same tag. Flutter handles the rest automatically.
Here's a basic example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hero Animation Demo',
home: FirstScreen(),
);
}
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: Center(
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
child: Hero(
tag: 'hero-image',
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50),
),
),
),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: Hero(
tag: 'hero-image',
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(150),
),
),
),
),
);
}
}
In this example, we have a blue circular container that starts as a small circle (100x100) on the first screen and expands to a larger circle (300x300) on the second screen. The Hero widget with the tag 'hero-image' creates the smooth transition between these two states.
Understanding Hero Tags
The tag parameter is crucial for Hero animations. It's a unique identifier that tells Flutter which widgets should be animated together. Both Hero widgets must have the same tag for the animation to work.
Tags should be unique within the widget tree. If you have multiple Hero animations on the same screen, each must have a different tag. A common pattern is to use the object's ID or a combination of identifiers:
Hero(
tag: 'product-${product.id}',
child: ProductImage(imageUrl: product.imageUrl),
)
This ensures that each product in a list gets its own unique Hero animation when navigating to its detail page.
Real-World Example: Image Gallery
One of the most common use cases for Hero animations is creating an image gallery where thumbnails expand into full-screen images. Let's build a complete example:
import 'package:flutter/material.dart';
class ImageGallery extends StatelessWidget {
final List imageUrls = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg',
'https://example.com/image3.jpg',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Image Gallery')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: imageUrls.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FullScreenImage(
imageUrl: imageUrls[index],
tag: 'image-$index',
),
),
);
},
child: Hero(
tag: 'image-$index',
child: Image.network(
imageUrls[index],
fit: BoxFit.cover,
),
),
);
},
),
);
}
}
class FullScreenImage extends StatelessWidget {
final String imageUrl;
final String tag;
const FullScreenImage({
Key? key,
required this.imageUrl,
required this.tag,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Hero(
tag: tag,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
),
),
),
),
);
}
}
In this example, each thumbnail image has a unique Hero tag based on its index. When you tap a thumbnail, it smoothly animates to fill the screen. Tapping the full-screen image pops back to the gallery with a reverse animation.
Customizing Hero Animations
While Hero animations work great out of the box, you can customize them using the flightShuttleBuilder parameter. This allows you to control what appears during the transition animation.
Hero(
tag: 'custom-hero',
flightShuttleBuilder: (
BuildContext flightContext,
Animation animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
final Hero toHero = toHeroContext.widget as Hero;
return RotationTransition(
turns: animation,
child: toHero.child,
);
},
child: Container(
width: 100,
height: 100,
color: Colors.purple,
),
)
In this example, the Hero widget rotates during the transition. The flightShuttleBuilder receives the animation object, which you can use to create custom effects like rotation, scaling, or color transitions.
Hero with Placeholder
Sometimes you want the Hero animation to work even when the destination widget hasn't loaded yet. You can use the placeholderBuilder parameter to show a placeholder during the transition:
Hero(
tag: 'hero-with-placeholder',
placeholderBuilder: (context, size, child) {
return Container(
width: size.width,
height: size.height,
color: Colors.grey[300],
child: Center(child: CircularProgressIndicator()),
);
},
child: FutureBuilder(
future: loadImageUrl(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.network(snapshot.data!);
}
return CircularProgressIndicator();
},
),
)
This is particularly useful when loading images or other asynchronous content, as it provides visual feedback during the transition.
Common Patterns and Best Practices
1. Use Meaningful Tags
Always use descriptive, unique tags. If you're animating items from a list, include the item's ID in the tag to ensure uniqueness:
Hero(tag: 'product-card-${product.id}', child: ...)
2. Keep Hero Widgets Simple
Hero widgets should contain relatively simple widgets. Complex widget trees can cause performance issues during the animation. If you need to animate something complex, consider wrapping just the visual element that should transition.
3. Match Visual Elements
For the best visual effect, the source and destination Hero widgets should represent the same logical element, even if their sizes or positions differ. This creates a clear visual connection for users.
4. Consider Performance
Hero animations are generally performant, but be mindful when animating large images or complex widgets. Consider using cached images or optimizing your widgets for smooth animations.
5. Handle Edge Cases
If a Hero widget might not exist on the destination screen (for example, if navigation can happen from multiple places), make sure to handle those cases gracefully. You might want to check if the destination screen exists before navigating.
Advanced: Hero with Custom Routes
When using custom page routes, you might need to ensure Hero animations work correctly. Here's an example with a custom page route:
class CustomPageRoute extends PageRoute {
final WidgetBuilder builder;
CustomPageRoute({required this.builder});
@override
Widget buildTransitions(
BuildContext context,
Animation animation,
Animation secondaryAnimation,
Widget child,
) {
return FadeTransition(
opacity: animation,
child: child,
);
}
@override
Widget buildPage(
BuildContext context,
Animation animation,
Animation secondaryAnimation,
) {
return builder(context);
}
@override
bool get maintainState => true;
@override
Duration get transitionDuration => Duration(milliseconds: 300);
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
}
When using custom routes, make sure to return the child widget in buildTransitions so that Hero animations can work properly. The child parameter contains the destination screen with its Hero widgets.
Troubleshooting Common Issues
Hero Animation Not Working
If your Hero animation isn't working, check these common issues:
- Tag mismatch: Ensure both Hero widgets have exactly the same tag string.
- Missing Hero widget: Both screens must have a Hero widget with the matching tag.
- Navigation context: Make sure you're using the correct BuildContext for navigation.
- Widget tree issues: Ensure Hero widgets are direct children of the routes, not buried deep in the widget tree.
Animation Looks Janky
If the animation appears choppy or stuttering:
- Check if you're doing heavy computations during the animation.
- Ensure images are properly cached.
- Consider using RepaintBoundary to isolate the Hero widget.
- Profile your app to identify performance bottlenecks.
Conclusion
Hero animations are a powerful tool for creating polished, professional Flutter applications. They provide visual continuity that helps users understand navigation flow and creates a sense of quality and attention to detail.
By following the patterns and best practices outlined in this article, you can implement Hero animations that enhance your app's user experience. Remember to keep tags unique, keep Hero widgets simple, and always test your animations on different devices to ensure smooth performance.
Start experimenting with Hero animations in your Flutter apps today, and watch as your navigation transitions become smoother and more delightful. Your users will notice the difference!