···11+---
22+title: Micro-Animations Spec
33+updated: 2026-04-27
44+---
55+66+## Summary
77+88+Add polished micro-animations across the app using `flutter_animate` to bring
99+life to transitions, state changes, and user interactions. The goal is subtle,
1010+fast, purposeful motion — not decorative. Every animation must serve
1111+orientation ("where am I?"), feedback ("did that work?"), or continuity
1212+("what just changed?").
1313+1414+## Package
1515+1616+**`flutter_animate`** (pub.dev) — declarative, composable animation chains
1717+via extension methods on `Widget`. Chosen over raw `AnimationController` /
1818+`ImplicitlyAnimatedWidget` for consistency and velocity: a single API for
1919+fade, slide, scale, blur, shimmer, and custom effects, with built-in
2020+stagger support.
2121+2222+Add to `pubspec.yaml`:
2323+2424+```yaml
2525+dependencies:
2626+ flutter_animate: ^4.5.2
2727+```
2828+2929+## Animation Inventory
3030+3131+### 1. Feed & List Items — Staggered Entrance
3232+3333+**Where:** Post cards in feed, notification rows, search results, follow
3434+audit results, list members, saved posts.
3535+3636+**Effect:** Each item fades in + slides up as it enters the viewport for the
3737+first time (initial load or pagination append).
3838+3939+```dart
4040+child
4141+ .animate()
4242+ .fadeIn(duration: 200.ms, curve: Curves.easeOut)
4343+ .slideY(begin: 0.05, end: 0, duration: 200.ms, curve: Curves.easeOut)
4444+```
4545+4646+Stagger: 50ms offset per item (capped at 10 items per batch to avoid long
4747+entrance sequences on large pages).
4848+4949+**Constraint:** Only on first appearance. Scrolling back to an already-seen
5050+item must not re-animate. Track via a `Set<String>` of post URIs (or item
5151+keys) in the feed cubit/bloc.
5252+5353+### 2. Action Feedback — Like, Repost, Bookmark
5454+5555+**Where:** Post action bar icons.
5656+5757+**Effect:** On tap, the icon scales up briefly then settles back, with a
5858+color crossfade to the active tint.
5959+6060+```dart
6161+icon
6262+ .animate(onPlay: (c) => c.forward())
6363+ .scale(begin: 1.0, end: 1.3, duration: 120.ms, curve: Curves.easeOut)
6464+ .then()
6565+ .scale(begin: 1.3, end: 1.0, duration: 100.ms, curve: Curves.easeOutBack)
6666+```
6767+6868+The color change uses `AnimatedSwitcher` or `ColorTween` on the existing
6969+icon — `flutter_animate`'s `.tint()` effect is an alternative.
7070+7171+### 3. Screen Transitions — Fade-Through
7272+7373+**Where:** All `GoRouter` page transitions (currently using default Material
7474+platform transitions).
7575+7676+**Effect:** Material fade-through (outgoing screen fades out + scales down
7777+slightly, incoming screen fades in + scales up slightly). Duration 300ms.
7878+7979+Implement via a custom `TransitionPage` wrapper:
8080+8181+```dart
8282+class FadeThroughPage<T> extends CustomTransitionPage<T> {
8383+ FadeThroughPage({required super.child, super.key})
8484+ : super(
8585+ transitionsBuilder: (context, animation, secondaryAnimation, child) {
8686+ return FadeThroughTransition(
8787+ animation: animation,
8888+ secondaryAnimation: secondaryAnimation,
8989+ child: child,
9090+ );
9191+ },
9292+ transitionDuration: const Duration(milliseconds: 300),
9393+ );
9494+}
9595+```
9696+9797+Use `animations` package's `FadeThroughTransition` or implement manually
9898+with `flutter_animate` chained fade + scale.
9999+100100+### 4. Skeleton / Shimmer Loading
101101+102102+**Where:** Feed loading placeholders, profile header loading, notification
103103+list loading — anywhere a shimmer placeholder is shown.
104104+105105+**Effect:** Replace static grey boxes with a shimmer sweep using
106106+`.shimmer(duration: 1200.ms, color: theme.colorScheme.surfaceContainerHigh)`.
107107+108108+```dart
109109+Container(
110110+ height: 16, width: 120,
111111+ decoration: BoxDecoration(
112112+ color: theme.colorScheme.surfaceContainerHighest,
113113+ borderRadius: BorderRadius.zero, // square geometry per UI refactor
114114+ ),
115115+)
116116+ .animate(onPlay: (c) => c.repeat())
117117+ .shimmer(duration: 1200.ms, color: theme.colorScheme.surfaceContainerHigh)
118118+```
119119+120120+### 5. Bottom Navigation Bar — Icon Transition
121121+122122+**Where:** Bottom nav bar active/inactive icon swap.
123123+124124+**Effect:** Active icon scales up from 1.0 to 1.15 with a fade crossfade
125125+between outlined → filled icon variants. Duration 150ms.
126126+127127+### 6. Snackbar / Toast Entrance
128128+129129+**Where:** All snackbar and toast messages.
130130+131131+**Effect:** Slide up from bottom + fade in (200ms). On dismiss, fade out +
132132+slide down (150ms).
133133+134134+### 7. FAB / Action Button
135135+136136+**Where:** Compose FAB, gallery FAB, scroll-to-top button.
137137+138138+**Effect:** Scale-in on appear (`scaleXY` 0 → 1, 200ms, `Curves.easeOutBack`).
139139+Scale-out on disappear (reverse).
140140+141141+### 8. Pull-to-Refresh Indicator
142142+143143+**Where:** Feed and list pull-to-refresh.
144144+145145+**Effect:** The refresh indicator rotates continuously during refresh, then
146146+scales down + fades out on completion (200ms).
147147+148148+### 9. Profile Header — Parallax
149149+150150+**Where:** Profile screen banner image.
151151+152152+**Effect:** Subtle parallax on scroll — banner moves at 0.5x scroll speed.
153153+Implemented via `SliverAppBar`'s existing `flexibleSpace` with a
154154+`Transform.translate` driven by scroll offset, not `flutter_animate`.
155155+156156+### 10. Empty State Illustrations
157157+158158+**Where:** Empty feeds, no search results, no notifications.
159159+160160+**Effect:** Fade in + gentle scale from 0.95 to 1.0 (300ms, ease-out).
161161+Prevents the "flash" of empty state content.
162162+163163+## Animation Tokens
164164+165165+Centralise timing and curves in a single file to keep motion consistent:
166166+167167+**`lib/core/theme/animation_tokens.dart`**
168168+169169+```dart
170170+abstract final class Anim {
171171+ // Durations
172172+ static const fast = Duration(milliseconds: 150);
173173+ static const normal = Duration(milliseconds: 250);
174174+ static const slow = Duration(milliseconds: 400);
175175+176176+ // Curves
177177+ static const enter = Curves.easeOut;
178178+ static const exit = Curves.easeIn;
179179+ static const emphasis = Curves.easeOutBack;
180180+181181+ // Stagger
182182+ static const staggerOffset = Duration(milliseconds: 50);
183183+ static const maxStaggerItems = 10;
184184+}
185185+```
186186+187187+All animations in the codebase reference these tokens — no magic numbers
188188+in widget files.
189189+190190+## Reduced Motion
191191+192192+Respect the platform's "reduce motion" accessibility setting:
193193+194194+```dart
195195+final reduceMotion = MediaQuery.of(context).disableAnimations;
196196+```
197197+198198+When `reduceMotion` is true, skip all non-essential animations. Essential
199199+transitions (screen changes) use a simple crossfade at 150ms. Loading
200200+shimmers continue (they convey information, not decoration).
201201+202202+Wrap in a utility:
203203+204204+```dart
205205+extension AnimateAccessibility on Widget {
206206+ Widget animateIfAllowed(BuildContext context, List<Effect> effects) {
207207+ if (MediaQuery.of(context).disableAnimations) return this;
208208+ return animate(effects: effects);
209209+ }
210210+}
211211+```
212212+213213+## Performance
214214+215215+- All animations use `flutter_animate`'s transform-based effects (GPU
216216+ composited) — avoid layout-triggering properties like width/height
217217+ animation on list items.
218218+- Stagger caps at 10 items to avoid jank on low-end devices.
219219+- Profile the animation layer with Flutter DevTools' performance overlay
220220+ before merge — target 0 skipped frames on a mid-range Android device
221221+ (Pixel 6a equivalent).
222222+- Feed item animations are fire-once; re-scrolling does not re-trigger.
223223+224224+## Testing
225225+226226+- Unit test `Anim` token values (sanity check, constants don't drift).
227227+- Widget tests for animated widgets use `tester.pumpAndSettle()` to
228228+ complete animations, then assert final visual state.
229229+- Widget tests for reduced-motion: set `MediaQuery.disableAnimations`
230230+ to `true`, verify no `Animate` widget in the tree.
231231+- Golden tests for shimmer placeholders (capture mid-animation frame).
232232+233233+## Scope & Constraints
234234+235235+- **No animated illustrations or Lottie.** All motion is CSS-style
236236+ property animation — no custom vector art or frame-based animation.
237237+- **No animation preferences screen.** We respect the OS-level reduced
238238+ motion toggle only. A per-app toggle is out of scope.
239239+- **No physics-based animations** (springs, flings) in this pass.
240240+ `flutter_animate` supports them but they're harder to keep consistent.
241241+ Evaluate in a future milestone.
+21-1
docs/tasks/phase-8.md
···11---
22title: Phase 8 Task Breakdown
33-updated: 2026-04-11
33+updated: 2026-04-27
44---
5566## M27 - Follow Hygiene: Detect & Remove Inactive/Problematic Follows
7788Completed [2026-04-11](../../CHANGELOG.md#2026-04-11)
99+1010+## M28 - Micro-Animations with flutter_animate
1111+1212+Spec: [animate.md](../specs/animate.md)
1313+1414+- [x] Add `flutter_animate` dependency
1515+- [ ] Create `lib/core/theme/animation_tokens.dart` with centralised durations, curves, stagger constants
1616+- [ ] Add reduced-motion utility (`animateIfAllowed` extension)
1717+ - [ ] Add setting for users to turn off animations
1818+- [ ] Feed & list items: staggered fade-in + slide-up on first appearance (track seen items to avoid re-animation)
1919+- [ ] Action feedback: scale-bounce on like / repost / bookmark tap
2020+- [ ] Screen transitions: fade-through `TransitionPage` wrapper for GoRouter
2121+- [ ] Shimmer loading: replace static skeleton placeholders with `.shimmer()` sweep
2222+- [ ] Bottom nav bar: scale + crossfade on active icon change
2323+- [ ] Snackbar / toast: slide-up entrance, fade-out dismiss
2424+- [ ] FAB / action buttons: scale-in on appear, scale-out on disappear
2525+- [ ] Pull-to-refresh: rotate + fade-out on completion
2626+- [ ] Profile header: parallax banner on scroll
2727+- [ ] Empty state: gentle fade + scale entrance
2828+- [ ] Widget tests for all animated widgets (pumpAndSettle, reduced-motion branch)