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.
// ✅ 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),
),
),
),
),
);
}
}