Flutter Motion Kit

Previewable Flutter animations + pitfall guide · one-click reuse from Claude Code

← Back

Hero shared-element transition

hero · difficulty 2/5 · verified Flutter 3.32 / 2026-06

Tap a grid item and the element "flies" to its spot on the detail page. Both ends share one Hero tag and the framework takes over the transition.

▶︎ Running for real (Flutter 3.32 · self-hosted, not a recording)

Code

// ✅ Recommended: the list page and detail page share the same unique tag, and the framework handles the shared-element flight automatically.
// Paste straight into DartPad (https://dartpad.dev) to run.
import 'package:flutter/material.dart';

void main() => runApp(const _App());

class _App extends StatelessWidget {
  const _App();
  @override
  Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData(useMaterial3: true),
    home: const _GridPage(),
  );
}

const _colors = [
  Colors.red,
  Colors.green,
  Colors.blue,
  Colors.orange,
  Colors.purple,
  Colors.teal,
];

class _GridPage extends StatelessWidget {
  const _GridPage();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hero gallery')),
      body: GridView.count(
        crossAxisCount: 3,
        padding: const EdgeInsets.all(12),
        mainAxisSpacing: 12,
        crossAxisSpacing: 12,
        children: [
          for (final (i, c) in _colors.indexed)
            GestureDetector(
              onTap: () => Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (_) => _DetailPage(index: i, color: c),
                ),
              ),
              child: Hero(
                tag: 'box-$i', // unique tag, matching the detail page
                child: Container(
                  decoration: BoxDecoration(
                    color: c,
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

class _DetailPage extends StatelessWidget {
  const _DetailPage({required this.index, required this.color});
  final int index;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Hero(
          tag: 'box-$index', // same tag as the list page
          child: Container(
            width: 260,
            height: 260,
            decoration: BoxDecoration(
              color: color,
              borderRadius: BorderRadius.circular(24),
            ),
          ),
        ),
      ),
    );
  }
}

⚠️ Pitfalls (4)

Two Heroes with the same tag on screen at once throw an exception.
✅ Tags must be globally unique (compose with an id, e.g. 'box-$i'); never duplicate on the same screen.
official-docs · source
A Hero inside a nested Navigator (e.g. within a BottomNavigationBar) won't fly across the outer route.
✅ Keep both ends under the same Navigator, or supply a flightShuttleBuilder for the flying widget.
official-docs · source
When start/end size or corner radius differ a lot, the default rect interpolation can jump or clip.
✅ Use createRectTween for a smoother flight path and keep a consistent BoxFit / shape on the child.
official-docs · source
The origin leaves a gap while the Hero is in flight.
✅ Fill it with placeholderBuilder when you need a placeholder.
official-docs · source

Official docs