Animations are often where apps start to feel alive instead of just functional.
And if you're just getting started with Flutter, it's easy to assume:
"I'll need a bunch of animation libraries to make things look good."
We used to think the same.
But in reality, Flutter already gives you a powerful set of built-in animation tools - no plugins required.
In this blog, we'll walk through real UI animations we've built in production apps, using only Flutter's default capabilities.
Why Avoid Plugins (At First)?
From our experience:
- Plugins increase dependency risk
- Updates can break things unexpectedly
- Debugging becomes harder
Flutter's built-in animations are:
- Lightweight
- Performant
- Fully customizable
And honestly more than enough for most use cases.
Animations You Can Build Without Plugins
1. Button Click Animation (Scale Effect)
On tap, the button slightly scales down and then smoothly returns to its original size.
It's a subtle effect, but it plays a big role - without feedback like this, users can feel unsure if their action was registered. This small touch makes the UI feel responsive and polished.
Where to use?
- Buttons
- Cards
- Any tappable UI
import 'package:flutter/material.dart';
class AnimatedButton extends StatefulWidget {
@override
_AnimatedButtonState createState() => _AnimatedButtonState();
}
class _AnimatedButtonState extends State<AnimatedButton> {
double _scale = 1.0;
void _onTapDown(_) => setState(() => _scale = 0.95);
void _onTapUp(_) => setState(() => _scale = 1.0);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
child: AnimatedScale(
scale: _scale,
duration: Duration(milliseconds: 150),
child: Container(
padding: EdgeInsets.all(16),
color: Colors.blue,
child: Text("Tap Me", style: TextStyle(color: Colors.white)),
),
),
);
}
}
2. Expand / Collapse Cards
When tapped, the container smoothly expands or collapses to reveal or hide content.
Rather than content appearing abruptly, this creates a more natural flow, making the UI easier to read and noticeably more polished.
Where to use?
- FAQs
- Settings
- Dashboards
class ExpandableCard extends StatefulWidget {
@override
_ExpandableCardState createState() => _ExpandableCardState();
}
class _ExpandableCardState extends State<ExpandableCard> {
bool isExpanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => isExpanded = !isExpanded),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
padding: EdgeInsets.all(16),
height: isExpanded ? 150 : 70,
color: Colors.grey[200],
child: Column(
children: [
Text("Tap to Expand"),
if (isExpanded) Text("More content here...")
],
),
),
);
}
}
3. Image Zoom (Hero Animation)
The image smoothly moves from one screen to another, as if it's the same element transitioning between pages.
This creates a seamless experience and makes the app feel more polished.
Where to use?
- Profile pictures
- Product previews
- Galleries
Hero(
tag: "imageHero",
child: Image.network("https://via.placeholder.com/150"),
)
Next screen:
Hero(
tag: "imageHero",
child: Image.network("https://via.placeholder.com/600"),
)
4. Fade-In Screens
Instead of instantly appearing, the screen fades in smoothly.
This removes the harshness of sudden transitions and creates a more pleasant, polished user experience.
Where to use?
- Onboarding
- Splash to home
- First load screens
class FadeScreen extends StatefulWidget {
@override
_FadeScreenState createState() => _FadeScreenState();
}
class _FadeScreenState extends State<FadeScreen> {
double opacity = 0;
@override
void initState() {
super.initState();
Future.delayed(Duration(milliseconds: 200), () {
setState(() => opacity = 1);
});
}
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: Duration(milliseconds: 500),
opacity: opacity,
child: Scaffold(
body: Center(child: Text("Welcome")),
),
);
}
}
5. Smooth List Insert / Remove
When items are added or removed, they animate smoothly instead of just appearing or disappearing.
This makes the changes feel more natural and easier to follow.
Where to use?
- Chat apps
- Todo lists
- Notifications
AnimatedList(
initialItemCount: items.length,
itemBuilder: (context, index, animation) {
return SizeTransition(
sizeFactor: animation,
child: ListTile(
title: Text(items[index]),
),
);
},
)
6. Page Transitions (Custom Navigation)
This controls how screens transition when navigating between them.
While default transitions work fine, custom ones can make the experience feel smoother and more intentional.
Where to use?
- Screen navigation
- Onboarding flows
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (_, __, ___) => NextScreen(),
transitionsBuilder: (_, animation, __, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);
7. Toggle UI Animations
The switch smoothly slides and updates its color when toggled.
This clear visual feedback helps users instantly understand the current state, making interactions feel more intuitive.
Where to use?
- Settings
- Filters
- Feature toggles
class ToggleAnimation extends StatefulWidget {
@override
_ToggleAnimationState createState() => _ToggleAnimationState();
}
class _ToggleAnimationState extends State<ToggleAnimation> {
bool isOn = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => isOn = !isOn),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
width: 60,
height: 30,
decoration: BoxDecoration(
color: isOn ? Colors.green : Colors.grey,
borderRadius: BorderRadius.circular(20),
),
alignment: isOn ? Alignment.centerRight : Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.all(4),
child: CircleAvatar(radius: 10),
),
),
);
}
}
8. Tween Animation (Advanced Control)
This approach lets you animate values such as size, position, or opacity from one state to another.
It offers much finer control, making it ideal for cases where built-in animation widgets don't quite meet your needs.
Where to use?
- Custom animations
- Complex UI interactions
class TweenExample extends StatefulWidget {
@override
_TweenExampleState createState() => _TweenExampleState();
}
class _TweenExampleState extends State<TweenExample>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late Animation<double> animation;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
);
animation = Tween<double>(begin: 0, end: 200).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (_, child) {
return Container(
width: animation.value,
height: 50,
color: Colors.blue,
);
},
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
Common Mistakes Beginners Make
Overusing animations
Just because you can animate something doesn't mean you should.
If everything is moving, nothing really stands out - and the UI starts to feel noisy instead of smooth.
Making animations too slow
Long animations often feel frustrating instead of smooth.
In most cases, anything above ~500ms starts to feel laggy and hurts the experience.
No consistency in motion
If every screen uses a different animation style, the app starts to feel disconnected.
Consistency in motion is just as important as consistency in colors or fonts.
Ignoring performance
Animations can get expensive if not handled carefully.
Avoid rebuilding heavy widgets unnecessarily while animations are running - it can quickly impact performance.
Final Thoughts
If you're just starting with Flutter animations, don't jump to plugins.
Start with:
- AnimatedContainer
- AnimatedOpacity
- Hero
- AnimatedList
- Tween + AnimationController
These alone can help you build smooth, production-level UI.
From our experience, mastering these basics gives you:
- Better control
- Cleaner code
- More confidence
And honestly most apps don't need more than this.
Top comments (0)