Intermediate · Lesson 5 · 13 min read
Animations That Feel Right
Implicit animations, explicit controllers, Hero transitions and staggered sequences — with the performance rules that keep them smooth.
Updated July 29, 2026
What you will learn
- Use implicit animations for the majority of cases
- Drive explicit animations with AnimationController
- Add shared-element transitions with Hero
- Keep animations at 60fps and know what makes them stutter
Flutter has two animation systems. Implicit animations animate to a new value whenever you rebuild with a different one — no controller, no lifecycle. Explicit animations give you a controller you start, stop, reverse and sequence. Reach for implicit first; roughly four out of five animations need nothing more.
Implicit animations
class LikeButton extends StatefulWidget {
const LikeButton({super.key});
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
bool _liked = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _liked = !_liked),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutBack,
padding: EdgeInsets.all(_liked ? 16 : 12),
decoration: BoxDecoration(
color: _liked ? Colors.pink.shade50 : Colors.grey.shade200,
shape: BoxShape.circle,
),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) =>
ScaleTransition(scale: animation, child: child),
child: Icon(
_liked ? Icons.favorite : Icons.favorite_border,
key: ValueKey(_liked), // required so the switcher sees a change
color: _liked ? Colors.pink : Colors.grey,
),
),
),
);
}
}| Widget | Animates |
|---|---|
AnimatedContainer | Size, colour, padding, decoration, alignment |
AnimatedOpacity | Fade in and out |
AnimatedPositioned | Position inside a Stack |
AnimatedAlign | Alignment within the parent |
AnimatedSwitcher | Swapping one child for another |
AnimatedSize | A child growing or shrinking |
TweenAnimationBuilder | Any custom value you can tween |
// Animate any value — here a counter rolling up to a new number
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: score.toDouble()),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOut,
builder: (context, value, child) => Text(
value.toStringAsFixed(0),
style: Theme.of(context).textTheme.displayMedium,
),
)Explicit animations
Use an AnimationController when you need to loop, reverse on demand, sequence several properties, or drive animation from a gesture. The controller needs a TickerProvider — that is what the SingleTickerProviderStateMixin supplies.
class PulsingDot extends StatefulWidget {
const PulsingDot({super.key});
@override
State<PulsingDot> createState() => _PulsingDotState();
}
class _PulsingDotState extends State<PulsingDot>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
)..repeat(reverse: true);
late final Animation<double> _scale = Tween<double>(begin: 0.85, end: 1.15)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
@override
void dispose() {
_controller.dispose(); // never skip this
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scale,
child: const CircleAvatar(radius: 8),
);
}
}AnimatedBuilder(
animation: _controller,
// Built once, reused for all 60 frames per second
child: const ExpensiveChart(),
builder: (context, child) => Transform.rotate(
angle: _controller.value * 2 * math.pi,
child: child,
),
)Staggered sequences
One controller can drive several animations at different times using Interval. The controller runs 0 → 1; each interval maps a slice of that range.
late final _controller =
AnimationController(vsync: this, duration: const Duration(milliseconds: 900));
// Fades in over the first 40% of the timeline
late final _fade = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
);
// Slides up during the middle 50%
late final _slide = Tween<Offset>(
begin: const Offset(0, 0.25),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: const Interval(0.2, 0.7, curve: Curves.easeOutCubic),
));Hero transitions
A Hero flies a widget from one screen to the next. Give the same tag to a widget on both routes and Flutter interpolates position, size and shape for you.
// List screen
Hero(
tag: 'product-${product.id}',
child: Image.network(product.imageUrl, height: 80, fit: BoxFit.cover),
)
// Detail screen — same tag, different size
Hero(
tag: 'product-${product.id}',
child: Image.network(product.imageUrl, height: 320, fit: BoxFit.cover),
)Keeping animations smooth
- Always profile in release mode. Debug builds are far slower; jank you see in debug may not exist in production, and vice versa.
- Animate transforms and opacity rather than layout properties where you can — moving a widget is cheaper than re-laying out a subtree.
OpacityandClipRRectcan trigger expensive offscreen buffers.FadeTransitionandBorderRadiuson a decoration are cheaper.- Never do work in a builder that runs 60 times a second — no parsing, no allocation of large objects, no
DateTime.now()formatting. - Use
RepaintBoundaryaround a constantly animating widget so it does not force its neighbours to repaint. - Respect accessibility: check
MediaQuery.disableAnimationsOf(context)and skip or shorten animations when users have reduced motion enabled.
final reduceMotion = MediaQuery.disableAnimationsOf(context);
AnimatedContainer(
duration: reduceMotion
? Duration.zero
: const Duration(milliseconds: 300),
// ...
)Key takeaways
- Start with implicit animations; they cover most needs with no lifecycle to manage.
AnimationControllerrequires a ticker provider and must be disposed.- Transition widgets beat
AnimatedBuilder; pass static subtrees viachildeither way. - Profile animations in release mode and honour the user's reduced-motion setting.
Practice
Animated onboarding
Build a three-page onboarding flow with a PageView. Animate a progress indicator between pages, stagger each page's title, illustration and body text with one controller, and use a Hero to fly the app logo into the home screen when onboarding completes.
Show hints
PageController.pagegives a fractional value you can drive animations with directly.- Wrap the illustration in a
RepaintBoundaryif the progress indicator is animating alongside it.