Swoosh/docs/superpowers/specs/2026-03-19-micro-animations-design.md

9.9 KiB

Micro-Animations Design — Swoosh (v2)

Date: 2026-03-19 Status: Approved Style: Expressive & playful — bouncy springs, overshoot, lively feedback

Overview

Add 32 micro-animations + 8 navigation transitions across all screens to make the app feel alive. Create a shared SwooshMotion object with predefined animation specs ensuring consistent character throughout the app.

The app now includes: multi-account support, search with filters, swipe-to-dismiss, multi-image galleries, writing statistics dashboard, HTML preview, and hashtag support.

Shared Animation Specs — SwooshMotion

Central object in ui/animation/SwooshMotion.kt:

Name Type Parameters Use case
Bouncy Spring dampingRatio=0.65, stiffness=400 FAB entrance, chips, badges
BouncyQuick Spring dampingRatio=0.7, stiffness=1000 Press feedback (~150ms settle)
Snappy Spring dampingRatio=0.7, stiffness=800 Expand/collapse, dialogs
Gentle Spring dampingRatio=0.8, stiffness=300 Cards, content reveal
Quick Tween 200ms, FastOutSlowInEasing Fade, color transitions
StaggerDelayMs Long 50 List item entrances
RevealDelayMs Long 80 Content reveal sequences

Reduced Motion Support

SwooshMotion checks Settings.Global.ANIMATOR_DURATION_SCALE. When reduced motion is enabled, all springs fall back to snap() and all tweens use 0ms duration.

Stagger Pattern

Each item uses a MutableState<Boolean> toggled via LaunchedEffect(Unit) { delay(StaggerDelayMs * index); visible = true }. AnimatedVisibility has no built-in delay.

Already-Animated Tracking (LazyColumn)

Hoist a mutableStateMapOf<String, Boolean>() of already-animated item keys in FeedScreen. Check key !in animatedKeys before triggering entrance. Only first ~8 visible items on initial load animate. Items from infinite scroll appear instantly.


Feed Screen — 12 animations

F1. FAB entrance (SPRING/Bouncy)

Scale 0→1 with overshoot on screen open. Hidden during search mode.

  • animateFloatAsState with Bouncy spring on scale modifier
  • LaunchedEffect(Unit) sets target to 1f
  • FAB already conditionally hidden when isSearchActive (line 249)

F2. FAB press (SPRING/BouncyQuick)

Shrinks to 85%, snaps back. Settles before navigation fires.

  • Modifier.pointerInput press → animateFloatAsState 0.85f→1f
  • Navigation fires immediately; animation interrupted by transition (fine)

F3. Post cards staggered entrance (SLIDE/Gentle)

Cascading slide-in from bottom, 50ms delay per item. "Waterfall" effect.

  • AnimatedVisibility per item in LazyColumn with slideInVertically + fadeIn
  • mutableStateMapOf<String, Boolean> tracking (see shared section)
  • Applies to both SwipeablePostCard and search-mode PostCard
  • Capped at first 8 items, infinite scroll items appear instantly

F4. "Show more" expand (SPRING/Snappy)

AnimatedContent(targetState = expanded) with fadeIn + expandVertically / fadeOut + shrinkVertically. Replaces discrete text swap in PostCardContent.

F5. Empty states (FADE/Quick + scale)

All empty states (connection error, filter empty, search no results, normal empty) wrapped in AnimatedVisibility with fadeIn + scaleIn(0.9f).

F6. Queue status chip (BOUNCE/Bouncy)

  • UPLOADING: rememberInfiniteTransition pulse alpha 0.6→1.0
  • Status change: bounce scale + animateColorAsState
  • FAILED: shake via Animatable offset oscillation (-4dp→4dp→0dp, 3 cycles)

F7. Snackbar (SLIDE/Snappy)

Both error snackbar (line 578) and pin confirmation snackbar (line 562) — AnimatedVisibility with slideInVertically + overshoot.

F8. Search bar (SLIDE/Quick) — NEW

SearchTopBar slides in from top with fade when search activates. Reverse on close.

  • AnimatedVisibility wrapping the if (isSearchActive) branch (line 157)
  • slideInVertically(initialOffsetY = { -it }) + fadeIn / reverse

F9. Filter chips bar (FADE/Quick) — NEW

FilterChipsBar fades in/out when toggling between search and normal mode.

  • Already has animateColorAsState for chip selection (line 715)
  • Add AnimatedVisibility wrapper for the if (!isSearchActive) block (line 263)

F10. Account switcher items (SLIDE/Gentle) — NEW

Inside AccountSwitcherBottomSheet (line 954), account ListItems get staggered slide-in.

  • ModalBottomSheet has built-in slide animation
  • Add stagger pattern to account items inside the sheet

F11. Pinned section header (FADE/Quick) — NEW

"📌 Pinned" header at line 790 — AnimatedVisibility with fadeIn + slideInVertically when pinned posts exist.

F12. Account switch overlay (FADE/Quick) — NEW

"Switching account..." overlay (line 288) — crossfade entrance with scale on the spinner.


Composer Screen — 9 animations

C1. Image grid preview (SCALE/Bouncy)

ImageGridPreview (line 556) — each image thumbnail scales in with bounce when added. Scale-out on removal.

  • AnimatedVisibility per grid item with scaleIn using Bouncy

After link loads, card slides up + fades in. Loading: PulsingPlaceholder replaces LinearProgressIndicator (line 317).

C3. Schedule chip (SPRING/Bouncy)

Chip at line 365 pops in with scaleIn using Bouncy when state.scheduledAt != null.

C4. Publish button (BOUNCE/BouncyQuick)

Button at line 421 — bounce on click, loading pulse during submit, AnimatedContent icon swap to checkmark on success.

C5. Character counter color (FADE/Quick)

Three-tier animateColorAsState: onSurfaceVariant → tertiary (>280) → error (>500). Matches current logic at line 211.

C6. Action buttons staggered (SCALE/Gentle)

Publish button (line 421) + Row of [Draft, Schedule] (line 433). Two-step stagger: button first, then row.

C7. Error text (SLIDE/Snappy)

Error at line 407 — AnimatedVisibility with slideInHorizontally + fadeIn.

C8. Hashtag chips (SPRING/Bouncy) — NEW

Extracted tags FlowRow (line 225) — staggered scaleIn per chip as tags appear/change.

C9. Edit/Preview crossfade (FADE/Quick) — NEW

Crossfade(targetState = state.isPreviewMode) wrapping the if (state.isPreviewMode) branch (line 167). Smooth transition between edit and preview modes.


Detail Screen — 5 animations

D1. Content reveal (FADE/Gentle)

Sequential: status row → text → tags → image gallery → link preview → PostStatsSection. 80ms delay each.

  • Stagger pattern with AnimatedVisibility per section
  • PostStatsSection (line 307) already has internal AnimatedVisibility for expand/collapse

D2. Status badge entrance (SPRING/Bouncy)

First in reveal sequence (index 0). Scale-in with bounce.

D3. Delete dialog (SCALE/Snappy)

Replace AlertDialog at line 312 with AnimatedDialog wrapper.

D4. PostStatsSection (SLIDE/Gentle)

Last in reveal sequence. Slides up. Internal expand/collapse already animated.

D5. Pin toggle feedback (BOUNCE/Bouncy) — NEW

Pin icon in TopAppBar (line 78 area) — bouncy scale pulse when toggling featured state. animateFloatAsState scale 1→1.2→1.


Settings Screen — 3 animations

S1. Account card (FADE/Quick) — NEW

Current account Card (line 78) — fade-in with subtle scale on screen entry.

S2. Disconnect confirmation dialog (SCALE/Snappy)

New behavior: Add confirmation dialog before both "Disconnect Current Account" (line 139) and "Disconnect All Accounts" (line 165). Uses AnimatedDialog wrapper.

S3. Theme chip selection (SPRING/Bouncy) — NEW

In ThemeModeSelector (line 184), selected chip gets a brief bouncy scale pulse on selection change.


Stats Screen — 3 animations (all NEW)

ST1. Stats cards staggered entrance (SCALE/Bouncy)

Four StatsCard composables (lines 68-98) — staggered scale-in with bounce. 50ms delay per card.

ST2. Writing stats reveal (SLIDE/Gentle)

OutlinedCard at line 109 — slide-up with fade after cards complete. Internal WritingStatRows cascade.

ST3. Number count-up (FADE/Quick)

Stat values animate from 0 to target via animateIntAsState. Subtle but adds life to the dashboard.


Navigation Transitions — 8 routes

Each composable() receives enterTransition, exitTransition, popEnterTransition, popExitTransition.

Route Enter Pop Exit
Setup fadeIn(500ms) — (inclusive popUpTo)
Feed fadeIn(300ms) fadeOut(200ms)
Composer slideInVertically + fadeIn slideOutVertically + fadeOut
Detail slideInHorizontally + fadeIn slideOutHorizontally + fadeOut
Settings slideInHorizontally slideOutHorizontally
Stats slideInHorizontally slideOutHorizontally
Preview slideInVertically + fadeIn slideOutVertically + fadeOut
AddAccount slideInVertically + fadeIn slideOutVertically + fadeOut

File Structure

ui/
├── animation/
│   └── SwooshMotion.kt          # Shared specs + reduced motion
├── components/
│   ├── AnimatedDialog.kt        # Scale-in dialog wrapper
│   └── PulsingPlaceholder.kt    # Pulsing loading placeholder
├── feed/
│   └── FeedScreen.kt            # F1-F12
├── composer/
│   └── ComposerScreen.kt        # C1-C9
├── detail/
│   └── DetailScreen.kt          # D1-D5
├── settings/
│   └── SettingsScreen.kt        # S1-S3
├── stats/
│   └── StatsScreen.kt           # ST1-ST3
└── navigation/
    └── NavGraph.kt              # 8 route transitions

New files: 3 (SwooshMotion.kt, AnimatedDialog.kt, PulsingPlaceholder.kt) Modified files: 6 (FeedScreen.kt, ComposerScreen.kt, DetailScreen.kt, SettingsScreen.kt, StatsScreen.kt, NavGraph.kt)

Testing Strategy

  • Existing unit tests pass unchanged (animations don't affect business logic)
  • Manual verification on emulator per animation
  • Test with "Remove animations" accessibility setting for reduced-motion fallback