Staggered Animations
3 min read
Animations
The AnimationController always runs from 0.0 to 1.0. With Interval(start, end, curve: ...), each child animation only "fires" inside its window:
Timeline: 0.0 ────────────────── 0.5 ────────────────── 1.0
Item 1: [ animates here ]
Item 2: [ animates here ]
Item 3: [ animates here ]
Each item's Interval produces a CurvedAnimation that maps the controller's 0..1 into the item's local 0..1 for the slice. Outside the slice, the value clamps at the boundary.
Code in action — list entrance
class _StaggeredListState extends State<StaggeredList>
with SingleTickerProviderStateMixin {
late final _ctrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
final items = const ['One', 'Two', 'Three', 'Four', 'Five'];
@override
void initState() {
super.initState();
_ctrl.forward();
}
@override
void dispose() { _ctrl.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) => ListView.builder(
itemCount: items.length,
itemBuilder: (_, i) {
final start = i / items.length;
final end = (i + 1) / items.length;
final slide = Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _ctrl,
curve: Interval(start, end, curve: Curves.easeOutCubic),
));
final fade = CurvedAnimation(
parent: _ctrl,
curve: Interval(start, end),
);
return SlideTransition(
position: slide,
child: FadeTransition(opacity: fade, child: ListTile(title: Text(items[i]))),
);
},
);
}
When to reach for staggered
| Situation | Approach |
|---|---|
| List items entering one after another | Single controller + per-item Interval |
| Multi-step UI reveal (icon → title → buttons) | Single controller + 3 intervals |
| Two animations that partially overlap | Intervals that share a range (e.g. 0.3–0.7 and 0.5–1.0) |
| Independent loops on different timelines | Multiple controllers (not staggered — concurrent) |
| Quick win, no controller setup | flutter_staggered_animations package |
Common mistakes to avoid
// ❌ Recomputing intervals + tweens inside builder every frame
itemBuilder: (_, i) {
final slide = Tween(...).animate(CurvedAnimation(...)); // recreated every rebuild
return SlideTransition(position: slide, ...);
}
// ✅ Cache them — compute once per item index when items change
// ❌ Using one controller per item
// → N tickers, complex orchestration, worse perf
// ✅ Single controller, intervals for each item
// ❌ Intervals that don't add up to a full sweep
// e.g. intervals [0.0, 0.3], [0.3, 0.6] for 5 items → last 2 items have no time
// ✅ Generic formula: start = i/N, end = (i+1)/N
// ❌ Mixing direction of travel without thought
// All items slide from the right + fade in → polished
// Random directions → chaotic, distracting
// ❌ Long total duration for many items
// 1500ms for 20 items = 75ms per item → choppy and rushed
// ✅ Tune total duration AND overlap so each item is perceivable (~150-200ms feels good)
Interview follow-ups
-
Why use one controller for staggered animations instead of one per item? One controller is simpler to coordinate (one start/stop call), uses fewer tickers, and guarantees timing relationships stay in sync. Multiple controllers are needed only when items animate independently (different start times triggered by different events).
-
What's the difference between staggered and sequential animations? Sequential = item 2 starts after item 1 ends. Staggered = item 2 can overlap item 1's tail. Most polished UI animations are staggered with some overlap — it feels natural and less robotic than strict sequence.
-
When would you reach for the
flutter_staggered_animationspackage? When you want a clean, declarativeAnimationLimiter+AnimationConfiguration.staggeredList(...)API, especially forListView/GridViewentry animations. It hides the controller bookkeeping. For one-off custom choreography, hand-rolled intervals are still the right call. -
How do you stagger animations across items that appear at different times (e.g. paginated list)? Different problem from "one controller animating N items at startup." Each newly added item needs its own short-running animation when it appears. Use
AnimatedSwitcherorAnimatedListwith per-item transitions, orflutter_staggered_animations'AnimationLimiterwhich scopes timing to a single appearance pass.
How helpful was this content?
Please sign in to rate this article.