image

09 Dec 2025

9K

35K

Flutter Hero Animation for Seamless Image Transitions

In modern mobile applications, user experience is paramount. Smooth and visually appealing transitions between different screens or states can significantly enhance the perceived quality of an app. Flutter provides a powerful set of animation widgets, and one of the most elegant for shared element transitions is the Hero widget. This article will delve into using Flutter's Hero Animation specifically for creating stunning image transitions, allowing an image to fly from one screen to another with a fluid, captivating effect.

What is Hero Animation?

A Hero animation is a type of shared element transition in Flutter. It's used when a widget (often an image) "travels" from one route (screen) to another, providing a visual continuity that makes the user interface feel connected and dynamic. When a user taps on an image in a gallery view, for instance, that image can smoothly expand and animate to fill a larger space on a detail view, rather than simply disappearing and reappearing.

How Hero Animation Works

The core concept behind Hero Animation is surprisingly simple yet effective:

  • Two Hero widgets, one on the source route and one on the destination route, are given identical tag properties.
  • When a navigation occurs (e.g., using Navigator.push()), Flutter's framework identifies these two Hero widgets.
  • It then calculates the visual transformation needed to make the widget appear to fly from its position and size on the source screen to its position and size on the destination screen.
  • The original widget on the source screen is temporarily removed, and a copy (the "flying hero") animates across the screen.
  • Once the animation completes, the "flying hero" is removed, and the destination Hero widget takes over, displaying the content.

This mechanism creates an illusion of a single, continuous element moving across the screen.

Implementing Hero Animation for Image Transitions

Let's walk through a practical example of implementing a Hero animation for image transitions. We'll create a simple app with a gallery screen displaying multiple images and a detail screen that shows a larger version of a selected image.

1. Project Setup (main.dart)

First, ensure you have a basic Flutter project. Your main.dart can be simple, setting up your MaterialApp and pointing to your home screen.


import 'package:flutter/material.dart';
import 'home_screen.dart'; // We'll create this file

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hero Animation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HomeScreen(),
    );
  }
}

2. The Source Screen (home_screen.dart)

This screen will display a grid of images. Each image will be wrapped in a Hero widget. When an image is tapped, we navigate to the detail screen, passing the image URL and its unique hero tag.


import 'package:flutter/material.dart';
import 'detail_page.dart'; // We'll create this file

class HomeScreen extends StatelessWidget {
  final List<String> imageUrls = [
    'https://picsum.photos/id/1018/200/300',
    'https://picsum.photos/id/1015/200/300',
    'https://picsum.photos/id/1016/200/300',
    'https://picsum.photos/id/1025/200/300',
    'https://picsum.photos/id/103/200/300',
    'https://picsum.photos/id/104/200/300',
    'https://picsum.photos/id/106/200/300',
    'https://picsum.photos/id/108/200/300',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image Gallery'),
      ),
      body: GridView.builder(
        padding: const EdgeInsets.all(8.0),
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 8.0,
          mainAxisSpacing: 8.0,
          childAspectRatio: 0.75, // Adjust as needed
        ),
        itemCount: imageUrls.length,
        itemBuilder: (context, index) {
          final imageUrl = imageUrls[index];
          final String heroTag = 'imageHero-$index'; // Unique tag for each image

          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => DetailPage(
                    imageUrl: imageUrl,
                    heroTag: heroTag,
                  ),
                ),
              );
            },
            child: Hero(
              tag: heroTag,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(8.0),
                child: Image.network(
                  imageUrl,
                  fit: BoxFit.cover,
                  loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
                    if (loadingProgress == null) return child;
                    return Center(
                      child: CircularProgressIndicator(
                        value: loadingProgress.expectedTotalBytes != null
                            ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
                            : null,
                      ),
                    );
                  },
                  errorBuilder: (context, error, stackTrace) =>
                      const Center(child: Icon(Icons.error, color: Colors.red)),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

3. The Destination Screen (detail_page.dart)

This screen will display the selected image in a larger format. Crucially, the image here is also wrapped in a Hero widget, using the exact same tag that was passed from the source screen.


import 'package:flutter/material.dart';

class DetailPage extends StatelessWidget {
  final String imageUrl;
  final String heroTag;

  const DetailPage({
    Key? key,
    required this.imageUrl,
    required this.heroTag,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image Detail'),
      ),
      body: Center(
        child: Hero(
          tag: heroTag, // MUST match the tag from the source Hero
          child: Image.network(
            imageUrl,
            fit: BoxFit.contain, // Adjust to how you want the image to display
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height * 0.7, // Occupy 70% of screen height
            loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
              if (loadingProgress == null) return child;
              return Center(
                child: CircularProgressIndicator(
                  value: loadingProgress.expectedTotalBytes != null
                      ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
                      : null,
                ),
              );
            },
            errorBuilder: (context, error, stackTrace) =>
                const Center(child: Icon(Icons.error, color: Colors.red, size: 50)),
          ),
        ),
      ),
    );
  }
}

Key Considerations and Best Practices

  • Unique Tags: Each pair of source and destination Hero widgets must have a globally unique tag. If two `Hero` widgets on the same route have the same tag, or if two transitions happen simultaneously with conflicting tags, you might encounter unexpected behavior or errors.
  • Widget Consistency: While not strictly enforced, it's best practice for the child of the Hero widget to be visually similar or ideally the same widget type on both the source and destination routes. For instance, if you use an Image.network on the source, use an Image.network on the destination. Slight differences in properties like fit are fine, as these are part of the animated transition.
  • Nesting: The Hero widget should typically wrap the specific widget that you want to animate, not an entire screen section.
  • Performance: While Hero animations are highly optimized, using a very large number of concurrent Hero animations or very complex children within Hero widgets might impact performance. For most standard use cases (like image transitions), this is not an issue.
  • Flexibility: The Hero widget can also animate non-image widgets, such as text or containers, as long as the tag is unique and the widgets are suitable for the transformation.

Conclusion

Flutter's Hero animation provides a remarkably simple yet powerful way to implement visually appealing shared element transitions, particularly for images. By simply assigning a unique tag to Hero widgets on both your source and destination routes, you can elevate your app's user experience with fluid, engaging animations. This makes your application feel more polished, interactive, and modern, contributing significantly to user satisfaction. Experiment with different image sizes and layouts to discover the full potential of this versatile Flutter widget.

Related Articles

Dec 19, 2025

Building a Widget List with Sticky

Building a Widget List with Sticky Header in Flutter Creating dynamic and engaging user interfaces is crucial for modern applications. One common UI pattern th

Dec 19, 2025

Mastering Transform Scale & Rotate Animations in Flutter

Mastering Transform Scale & Rotate Animations in Flutter Flutter's powerful animation framework allows developers to create visually stunning and highly intera

Dec 19, 2025

Building a Countdown Timer Widget in Flutter

Building a Countdown Timer Widget in Flutter Countdown timers are a fundamental component in many modern applications, ranging from e-commerce platforms indica