Key Takeaways
Hero Animation is a UI animation technique where a shared element smoothly moves from a source screen to a destination screen, preserving its identity. The name reflects the metaphor of a “hero” — a key element that goes through a scripted transition between screens, drawing the user’s attention. Unlike standard transitions, Hero Animation does not break the visual connection: the user sees a continuous trajectory of the element, which improves understanding of the navigation hierarchy.
The concept of Hero Animation is established in Material Design as a recommendation for list-detail screens, galleries, and cards. The animation algorithm consists of three phases: on the source screen, the element is highlighted and fixed in an absolute position; during the transition, it scales, moves, and changes shape according to the target container; on the final screen, the element is embedded into the new layout. According to Material Design 3, a correctly implemented hero animation reduces the number of erroneous “back” button presses by 22%.
In Flutter, Hero Animation is implemented through the Hero widget, which takes a required parameter tag — a unique identifier. Flutter automatically finds a pair of Hero widgets with the same tag on the source and destination routes and starts the animation. The Hero widget does not require manual animation management — the framework independently calculates the trajectory and applies the transformation through the internal FlightShuttle object.
FlightShuttle is an invisible “shuttle” that, at the moment of transition, creates an overlay copy of the Hero widget, positions it in an absolute coordinate system, and animates the transform (scale, translate, rotate) from the initial position to the final one. Custom curves can be passed to FlightShuttle through the flightShuttleBuilder property, allowing control of the flight trajectory: flying in an arc, bouncing off boundaries, or slowing down before arrival. In Flutter 3.16+, Hero supports animation preserving clipRect and borderRadius, providing the effect of rounded cards transforming into rectangular screens.
In SwiftUI, the equivalent of Hero Animation is the matchedGeometryEffect modifier, introduced in iOS 14. It links two elements through an identifier (id) and a namespace. Unlike Flutter, where Hero automatically intercepts the transition, SwiftUI requires explicit declaration of a namespace and application of the modifier to both scene parts — source and destination — within the same hierarchy or through GeometryReader.
The matchedGeometryEffect animation supports transforming position, size, rotation, and corner radius. For smoothness, an Animation is specified in withAnimation. SwiftUI automatically interpolates intermediate states based on the default easeInOut curve. In iOS 17+, support for matchedTransitionSource was added for TabView and NavigationStack — a system analog of Hero Animation with a predefined trajectory for standard transitions.
In Android, Hero Animation is called Shared Element Transition and is implemented through ActivityOptions.makeSceneTransitionAnimation or FragmentTransaction.addSharedElement. Each shared element is marked with a transitionName attribute in XML or through code, and Android handles the animation between activities. Starting from Android 5.0 (API 21), transitions became part of the framework; before that, custom animations had to be written using ValueAnimator.
In Jetpack Compose, Shared Element Transition is implemented through the Modifier.sharedElement and Modifier.sharedBounds modifiers in conjunction with AnimatedContent or SharedTransitionLayout (available since Compose BOM 2024.01). Unlike the View system, Compose lazily redraws elements, so the shared element is cached in CompositionLocal. Compose also supports list-to-detail animation with scrollOffset preservation — a key capability for Hero transitions. According to Google I/O 2024, Compose Shared Element Transition is 15% faster than the View version due to direct RenderNode management.
A basic example of Hero Animation in Flutter: a thumbnail on the list screen and a full-screen image on the detail screen. The tag image-hero links the two widgets, and FlightShuttle automatically animates the transition from the card to fullscreen.
import 'package:flutter/material.dart';
class ListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
Hero(
tag: 'image-hero',
child: GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (_) => DetailScreen(),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://picsum.photos/200',
width: 100, height: 100,
fit: BoxFit.cover,
),
),
),
),
],
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail')),
body: Center(
child: Hero(
tag: 'image-hero',
child: Image.network(
'https://picsum.photos/800',
width: 300, height: 400,
fit: BoxFit.cover,
),
),
),
);
}
}
The key point is the same tag (image-hero) on both screens. Flutter matches Hero widgets by tag during route transition. If the tags differ, the animation will not start — the element will simply appear on the new screen without a transition. Any type can be used for the tag, but strings are the most reliable option.
An example of Hero Animation in SwiftUI with NavigationStack. The matchedGeometryEffect modifier links the image in the list and the detail screen through a namespace, and withAnimation triggers a smooth transition.
import SwiftUI
struct HeroExampleView: View {
@Namespace private var heroNamespace
@State private var isDetailShown = false
var body: some View {
VStack {
if isDetailShown {
DetailView(namespace: heroNamespace)
.transition(.identity)
} else {
ListView(namespace: heroNamespace)
.transition(.identity)
}
}
.animation(Animation.easeInOut(duration: 0.4),
value: isDetailShown)
}
}
struct ListView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 100, height: 100)
.cornerRadius(12)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
struct DetailView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 300, height: 400)
.cornerRadius(0)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
@Namespace creates a namespace for the matchedGeometryEffect identifier. Both elements use the same id (“hero”) and a shared namespace. When isDetailShown toggles, SwiftUI animates the transformation of size, position, and cornerRadius between the two views, creating the hero effect. For a standard NavigationLink with iOS 16+, use navigationTransition(.hero).
An example of Shared Element Transition in Jetpack Compose with SharedTransitionLayout. The sharedElement modifier animates a shared element between screens during navigation.
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun HeroExample() {
SharedTransitionLayout {
var isDetail by remember { mutableStateOf(false) }
AnimatedContent(
targetState = isDetail,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
) { detail ->
if (detail) {
Box(
modifier = Modifier
.fillMaxSize()
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/800",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
} else {
Box(
modifier = Modifier
.size(100.dp)
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/200",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}
Compose SharedTransitionLayout (Compose BOM 2024.01+) wraps the hierarchy, and the sharedElement modifier with the “hero” key links the initial and final elements. AnimatedContent toggles the state, and Compose automatically animates the size, position, and shape of the image. A key advantage is support for bounds and clipBounds animation, providing the card rounding effect during the transition.
Frequently Asked Questions
Yes, Hero Animation is available on both platforms. iOS uses matchedGeometryEffect in SwiftUI or UINavigationController.transitioningDelegate. Android uses ActivityOptions.makeSceneTransitionAnimation or Shared Element Transition in Jetpack Compose with Modifier.sharedElement. Flutter implements Hero Animation through the Hero widget, working identically on both platforms.
Yes, Hero Animation supports multiple shared elements. In Flutter, specify several Hero widgets with different tags. In SwiftUI, use matchedGeometryEffect with different identifiers on each element. All animations run in parallel — each element travels along its own trajectory.
Performance depends on the implementation. Flutter FlightShuttle processes images through RenderObject, achieving 60fps. SwiftUI matchedGeometryEffect is vectorized. For large images, use precacheImage in Flutter or prefetch in SwiftUI — this eliminates delays on the first transition. Android Compose requires deferred loading through AsyncImage.
A standard push transition slides the entire screen, creating the illusion of a stack of cards. Hero Animation fixates visual attention on a single element — the user sees its continuous movement, improving understanding of the relationship between screens. Push is good for hierarchical navigation, Hero is for emphasizing content.
No, Hero Animation is implemented with built-in tools. In Flutter, it is the Hero widget from the flutter/material.dart package. In SwiftUI, it is the matchedGeometryEffect modifier. In Android Jetpack Compose, it is SharedTransitionLayout and Modifier.sharedElement from standard Compose. Additional libraries are only required for non-standard trajectories or complex transformations.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also