Flutter Animations for Beginners: AnimatedContainer, AnimatedSwitcher and Hero
Learn Flutter animations the easy way. Use AnimatedContainer, AnimatedOpacity, AnimatedSwitcher, TweenAnimationBuilder and Hero to make your UI feel smooth, with no AnimationController needed.
By Yaqoob Developer · · 5 min read
- #Flutter
- #Animation
- #Hero
- #Flutter UI
- #Beginners
Animations are what make an app feel premium. A button that smoothly grows, a card that fades in, an image that flies to the next screen. Users notice when they are missing.
Many beginners think Flutter animations are hard because of AnimationController, Tween and vsync. But for most UI animations you need none of that. Flutter has implicit animations: widgets that animate by themselves when their values change.
In this guide you will learn:
- AnimatedContainer for size, color, radius and padding
- AnimatedOpacity for fading things in and out
- AnimatedSwitcher for swapping one widget with another
- TweenAnimationBuilder for animating any number
- Hero for flying an image between screens
- How to pick a duration and a curve that feel right
The idea behind implicit animations
A normal Container jumps straight to new values when you call setState. An AnimatedContainer animates from the old values to the new ones over a duration you choose. That is all.
You do not start or stop anything. You change a value with setState, and the widget animates.
AnimatedContainer
Tap the box to toggle it between two styles:
import 'package:flutter/material.dart';
class AnimatedBoxDemo extends StatefulWidget {
const AnimatedBoxDemo({super.key});
@override
State<AnimatedBoxDemo> createState() => _AnimatedBoxDemoState();
}
class _AnimatedBoxDemoState extends State<AnimatedBoxDemo> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
width: _expanded ? 280 : 120,
height: _expanded ? 180 : 120,
decoration: BoxDecoration(
color: _expanded ? Colors.deepPurple : Colors.orange,
borderRadius: BorderRadius.circular(_expanded ? 32 : 60),
),
alignment: Alignment.center,
child: const Text('Tap me', style: TextStyle(color: Colors.white)),
),
),
);
}
}
Width, height, color and corner radius all animate together. AnimatedContainer can animate almost every Container property: padding, margin, alignment, decoration, transform.
A real use: an expandable card, a selected chip, or a "Sign in" button that shrinks into a circle while loading.
AnimatedOpacity
Fade a widget in or out:
AnimatedOpacity(
opacity: _visible ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: const Text('Saved!'),
)
The widget still takes up space when it is invisible. If you want the space to collapse too, combine it with AnimatedSize or AnimatedSwitcher.
Related widgets that work the same way: AnimatedPadding, AnimatedAlign, AnimatedPositioned (inside a Stack), AnimatedScale, AnimatedRotation and AnimatedDefaultTextStyle.
AnimatedSwitcher
When you replace one widget with a different one, AnimatedSwitcher fades (or slides) between them. A counter is the classic example:
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
transitionBuilder: (child, animation) => ScaleTransition(
scale: animation,
child: child,
),
child: Text(
'$_count',
key: ValueKey(_count),
style: Theme.of(context).textTheme.displayMedium,
),
)
The key is the important part. AnimatedSwitcher only animates when the child's key changes. Without ValueKey(_count), Flutter sees "a Text widget, then a Text widget" and thinks nothing changed.
Great uses: switching between a play and pause icon, between a loading spinner and content, or between an empty state and a list.
TweenAnimationBuilder
Sometimes you want to animate a value that no widget has a property for, like a number counting up. TweenAnimationBuilder animates any value and gives it to you in a builder:
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _balance),
duration: const Duration(milliseconds: 800),
curve: Curves.easeOut,
builder: (context, value, _) => Text(
'\$${value.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.headlineLarge,
),
)
When _balance changes, the number counts smoothly to the new value. Perfect for wallet balances, scores and progress percentages. You can see this style of UI in the crypto wallet login UI.
Hero: fly an image to the next screen
Hero animates a widget from one screen to another. Give both widgets the same tag:
// List screen
GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => DetailPage(product: product)),
),
child: Hero(
tag: 'product-${product.id}',
child: Image.network(product.imageUrl, height: 120),
),
)
// Detail screen
Hero(
tag: 'product-${product.id}',
child: Image.network(product.imageUrl, height: 320),
)
When the new screen opens, the image flies from its small position in the list to its large position on the detail page. Going back reverses it. No other code is needed.
Rules for Hero:
- Tags must be unique on each screen. In a list, include the item's id in the tag.
- Use the same kind of child on both sides (an image to an image), or the flight looks strange.
Choosing duration and curve
Good animations are short. Use these as defaults:
| Animation | Duration |
|---|---|
| Small changes (color, icon swap) | 150 to 250 ms |
| Size and layout changes | 300 to 400 ms |
| Page transitions, Hero | 300 to 500 ms |
Anything above 500 ms usually feels slow, and users notice.
For curves:
- Curves.easeOutCubic for things that appear or grow. They start fast and settle gently, which feels responsive.
- Curves.easeInOut for things that move from one place to another.
- Curves.elasticOut or Curves.easeOutBack for a playful bounce. Use sparingly.
When do you need AnimationController?
Implicit animations cover most UI work. You need an explicit AnimationController when:
- The animation repeats forever (a pulsing dot, a loading shimmer)
- You need to pause, reverse or scrub it
- Several animations must run in a precise sequence
That is a topic for its own article. Start with implicit animations and only reach for a controller when they cannot do the job.
Common mistakes
Nothing animates. You changed the value without setState, or you used a normal Container instead of AnimatedContainer.
AnimatedSwitcher jumps instead of animating. Add a key that changes with the content.
"There are multiple heroes that share the same tag". Two Hero widgets on the same screen have the same tag. Add the item id to the tag.
See animation in real apps
My 360 rotating 3D shoe and car app color picker projects use these ideas in a full design. Browse all my Flutter UI projects for source code.
Summary
- Implicit animations animate automatically when you change values with setState.
- Use AnimatedContainer for shape and color, AnimatedOpacity for fades, AnimatedSwitcher (with a key) for swapping widgets.
- TweenAnimationBuilder animates any value, Hero animates between screens.
- Keep durations short and use easeOutCubic as a safe default curve.
Keep reading
More articles
· 5 min read
Flutter Dark Mode: Light and Dark Theme with a Toggle That Remembers
Add dark mode to a Flutter app with Material 3. Create light and dark themes from one seed color, follow the system setting, add a toggle and save the user's choice with shared_preferences.
· 5 min read
Flutter Form Validation: Build a Login Form with TextFormField
Validate user input in Flutter with Form, TextFormField and GlobalKey. Build a complete login form with email and password rules, a show/hide password button and a loading state.
· 5 min read
Flutter Bottom Navigation Bar with Material 3 (NavigationBar Guide)
Build a bottom navigation bar in Flutter with the Material 3 NavigationBar widget: switch screens, keep each tab's state with IndexedStack, add badges and style it to match your app.