Skip to content
cccskills
FreePublished on GitHub

compose-motion

Jetpack Compose animation foundations - animate*AsState, AnimatedVisibility, Crossfade, updateTransition, SharedTransitionLayout, gestures.

The English summary is being prepared. Showing the original description.

See how to install

Files included (4)

  • SKILL.md13.6 KB
  • references/gestures-compose.md9.7 KB
  • references/recomposition-and-anim.md8.2 KB
  • references/shared-transitions.md11.8 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Version-sensitive. Every API name, SDK gate and browser-support claim below was verified on 2026-09-08 against primary sources. What against, and when, is in _jutsu/VERSIONS.md. If that date is old, re-verify before acting on a version number.

Compose Motion - Sub-skill

Jetpack Compose animation core. Loaded for Android Compose and Compose Multiplatform projects. Concise rules here. Deep-dive in references/. Baseline: Jetpack Compose 1.12.0 (current stable, Aug 2026) - i.e. Compose BOM 2026.08.x. Modern stable APIs only - no swipeable, no animateContentSize hacks where AnimatedContent is correct. Anything below flagged 'Compose 1.x+' is a since marker, not the target.


API Decision Tree

NeedAPI
Single value over timeanimateFloatAsState, animateDpAsState, animateColorAsState, etc.
Visibility / mount-unmountAnimatedVisibility(visible) { ... }
Crossfade between statesCrossfade(target) { state -> ... }
Multi-state coordinatedupdateTransition(target).animateFloat { ... }
Manual control / interruptionAnimatable(initialValue) + animateTo(...)
Looping / infiniterememberInfiniteTransition().animateFloat(...)
Shared elementsSharedTransitionLayout + Modifier.sharedElement(...) (Compose 1.7+)
Layout swap with animAnimatedContent(target) { ... }
Drag / swipeModifier.draggable + Animatable.snapTo/animateTo, or Modifier.anchoredDraggable for snap-points

Rule: climb the ladder only when needed. animate*AsState covers 70% of cases. Reach for Animatable only when you need to interrupt, chain, or read velocity.


Spring API (opinionated defaults)

UseSpec
UI snap (modal, drawer, tab)spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy)
Tactile (button press release, toggle)spring(stiffness = Spring.StiffnessMedium, dampingRatio = 0.85f)
Bouncy reveal (toast, FAB, success)spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
Drag follow (1:1 finger tracking)spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)

Stiffness constants (androidx.compose.animation.core.Spring): StiffnessVeryLow 50f, StiffnessLow 200f, StiffnessMediumLow 400f, StiffnessMedium 1500f (the spring() default), StiffnessHigh 10000f. Higher = faster settle. Damping constants: DampingRatioHighBouncy 0.2f, DampingRatioMediumBouncy 0.5f, DampingRatioLowBouncy 0.75f, DampingRatioNoBouncy 1.0f (the default). Below 1.0 overshoots. Springs ignore durationMillis; if you need a deterministic duration, use tween(...) instead.


animate*AsState - The Bread and Butter

val targetAlpha = if (visible) 1f else 0f
val alpha by animateFloatAsState(
    targetValue = targetAlpha,
    animationSpec = spring(stiffness = Spring.StiffnessMedium),
    label = "alpha",
)
Box(modifier = Modifier.alpha(alpha))

The label shows up in Layout Inspector / Animation Preview - always set it, future-you will thank present-you. Variants ship for Dp, Color, Offset, IntOffset, Size, Rect, Float, Int, and a generic animateValueAsState for custom types via TwoWayConverter.


AnimatedVisibility - Mount / Unmount with Anim

AnimatedVisibility(
    visible = expanded,
    enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
    exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(animationSpec = tween(150)),
) {
    Panel()
}

Combine multiple enter/exit transitions with +. Respect motion-principles: exit shorter and simpler than enter (here 150ms tween fade vs full slide+fade enter). The content composable only runs while visible OR animating - safe to mount expensive children inside.


AnimatedContent - State-Driven Layout Swap

AnimatedContent(
    targetState = currentTab,
    transitionSpec = {
        (slideInHorizontally { it } + fadeIn()) togetherWith
            (slideOutHorizontally { -it } + fadeOut())
    },
    label = "tabs",
) { tab ->
    TabContent(tab)
}

togetherWith runs enter and exit in parallel; using SizeTransform(clip = false) controls how the container resizes between contents. Keys matter: if targetState doesn't change identity, no transition fires.


Crossfade - Simple Fade Between States

Crossfade(targetState = isLoading, label = "loadState") { loading ->
    if (loading) Spinner() else Content()
}

Use when you only need a fade. For anything richer (slide, scale, layout-aware), reach for AnimatedContent. Crossfade does NOT animate size - the container takes the size of the new content immediately.


updateTransition - Multi-Property Coordinated

val transition = updateTransition(targetState = expanded, label = "expand")
val width by transition.animateDp(label = "width") { if (it) 300.dp else 100.dp }
val color by transition.animateColor(label = "color") { if (it) Color.Blue else Color.Gray }
val corner by transition.animateDp(label = "corner") { if (it) 24.dp else 8.dp }

Box(
    Modifier
        .width(width)
        .background(color, RoundedCornerShape(corner)),
)

Use when several properties animate together based on the same state. All children share the same transition timeline, so they finish in sync. Each animate* call accepts its own transitionSpec lambda for per-property tuning.


Animatable - Manual Control

val offsetX = remember { Animatable(0f) }
LaunchedEffect(triggerEvent) {
    offsetX.animateTo(100f, spring())
    offsetX.animateTo(0f, spring(dampingRatio = Spring.DampingRatioMediumBouncy))
}
Box(Modifier.offset { IntOffset(offsetX.value.roundToInt(), 0) })

Reach for Animatable when you need to interrupt (stop()), chain (animateTo returns when finished), read live velocity, or kick off decay (animateDecay). It is the imperative escape hatch for drag-then-fling, snap-back, and any flow animate*AsState cannot express.


SharedTransitionLayout (Compose 1.7+) - Hero Animations

SharedTransitionLayout {
    AnimatedContent(targetState = currentScreen, label = "nav") { screen ->
        when (screen) {
            Screen.List -> ListScreen(
                sharedTransitionScope = this@SharedTransitionLayout,
                animatedVisibilityScope = this@AnimatedContent,
            )
            is Screen.Detail -> DetailScreen(
                item = screen.item,
                sharedTransitionScope = this@SharedTransitionLayout,
                animatedVisibilityScope = this@AnimatedContent,
            )
        }
    }
}

// Inside ListScreen, on the card image:
with(sharedTransitionScope) {
    Image(
        painter = painter,
        contentDescription = null,
        modifier = Modifier.sharedElement(
            sharedContentState = rememberSharedContentState(key = "hero-${item.id}"),
            animatedVisibilityScope = animatedVisibilityScope,
        ),
    )
}

Two scopes plumbed down: SharedTransitionScope (where Modifier.sharedElement extension lives) and AnimatedVisibilityScope (the visibility context that drives the transition). rememberSharedContentState(key) keys MUST match across screens or no animation fires. Deep-dive in references/shared-transitions.md.


InfiniteTransition - Looping

val infinite = rememberInfiniteTransition(label = "loader")
val rotation by infinite.animateFloat(
    initialValue = 0f,
    targetValue = 360f,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Restart,
    ),
    label = "rotation",
)
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.rotate(rotation))

RepeatMode.Restart jumps back to start each cycle; RepeatMode.Reverse ping-pongs. Always provide a pause / off path for accessibility (see Reduced Motion section below). For one-shot decay (fling), use Animatable.animateDecay instead.


Gestures (quick map)

NeedModifier
TapModifier.clickable(onClick = ...)
Tap + long-pressModifier.combinedClickable(onClick = ..., onLongClick = ...)
Drag (single axis)Modifier.draggable(state, orientation)
Multi-touch / customModifier.pointerInput { detectDragGestures { ... } }
Scroll offset readingLazyListState or ScrollState.value
Swipe-to-dismiss / snap pointsModifier.anchoredDraggable(state) (Compose 1.6+, replaces swipeable)
Pinch + pan + rotateModifier.transformable(state)

Deep-dive in references/gestures-compose.md (NestedScrollConnection, conflict resolution, fling decay).


Anti-Patterns (BAD / GOOD)

1. Animating layout size instead of transform

// BAD - animateDpAsState on width forces a layout pass every frame
val w by animateDpAsState(if (expanded) 300.dp else 100.dp)
Box(Modifier.width(w).height(60.dp))
// GOOD - animate scale via graphicsLayer (composite-only, no layout)
val scale by animateFloatAsState(if (expanded) 3f else 1f, label = "scale")
Box(
    Modifier
        .width(100.dp)
        .height(60.dp)
        .graphicsLayer {
            scaleX = scale
            transformOrigin = TransformOrigin(0f, 0.5f)
        },
)

If the actual layout size MUST change (parent depends on it), use Modifier.animateContentSize() so neighbors animate too, or wrap the swap in AnimatedContent.

2. LaunchedEffect(true) / LaunchedEffect(Unit) with hidden inputs

// BAD - re-runs only once, BUT also fires on every recomposition surprise
//        when callers swap the composable instance. Worse: anything captured
//        in the lambda is stale.
LaunchedEffect(true) {
    offsetX.animateTo(target)
}
// GOOD - explicit key tied to the trigger
LaunchedEffect(triggerKey) {
    offsetX.animateTo(target)
}

If you genuinely want "run once", use LaunchedEffect(Unit) on purpose AND ensure you don't capture varying state - or hoist the captured values out. When in doubt, key on the values you read.

3. Forgetting key in a LazyColumn with item animations

// BAD - on insert/delete/reorder, items animate to wrong slots
LazyColumn {
    items(list) { item ->
        AnimatedVisibility(visible = item.expanded) { ItemRow(item) }
    }
}
// GOOD - stable key by id, plus Modifier.animateItem for reorder anim
LazyColumn {
    items(list, key = { it.id }) { item ->
        Row(modifier = Modifier.animateItem()) {
            AnimatedVisibility(visible = item.expanded) { ItemRow(item) }
        }
    }
}

Modifier.animateItem() (Compose 1.7+, replaces animateItemPlacement) handles insert/remove/move automatically when key is stable.

4. Nesting AnimatedContent inside scrolling list items

// BAD - every item runs its own transition graph; scroll = jank
LazyColumn {
    items(list, key = { it.id }) { item ->
        AnimatedContent(targetState = item.state) { state -> Row(state) }
    }
}
// GOOD - lift state, animate only the changing prop on the row
LazyColumn {
    items(list, key = { it.id }) { item ->
        val color by animateColorAsState(if (item.selected) selBg else bg, label = "rowBg")
        Row(Modifier.background(color)) { Content(item) }
    }
}

Rule: heavy animation containers (AnimatedContent, SharedTransitionLayout) belong at screen scope, not per row.


Reduced Motion - Respect It

See ../motion-principles/SKILL.md for the cross-platform doctrine.

@Composable
fun rememberReduceMotion(): Boolean {
    val context = LocalContext.current
    return remember {
        Settings.Global.getFloat(
            context.contentResolver,
            Settings.Global.ANIMATOR_DURATION_SCALE,
            1f,
        ) == 0f
    }
}

val reduce = rememberReduceMotion()
val alpha by animateFloatAsState(
    targetValue = if (visible) 1f else 0f,
    animationSpec = if (reduce) snap() else spring(),
    label = "alpha",
)

ANIMATOR_DURATION_SCALE reads the animator duration scale directly. The cleaner call is ValueAnimator.areAnimatorsEnabled() (API 26+), which returns false when that scale is 0 - set by the developer-options "Animation off" toggle, Battery Saver, and the user-facing "Remove animations" (Settings -> Accessibility) toggle alike. Deep-dive in ../mobile-principles/references/accessibility-mobile.md.


Quick Reference

NeedLoad
Shared transitions deep-divereferences/shared-transitions.md
Gestures + nestedScroll patternsreferences/gestures-compose.md
Recomposition / jank / Layout Inspectorreferences/recomposition-and-anim.md
Advanced (M3 Expressive, AGSL, Canvas)../compose-graphics/SKILL.md
CMP / KMP patterns../compose-multiplatform/SKILL.md
Mobile UX context../mobile-principles/SKILL.md
Foundation (timing, easing, a11y)../motion-principles/SKILL.md

Sources

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Summon shadow clones: genjutsu's full-scale pipeline for a whole website or web app, from scratch or as a full redesign. Harvests the client's real material, researches in parallel, lets Impeccable lead the direction and genjutsu the motion, builds the pages with parallel agents, then loops fixes, verdicts and a cold-eyes art director until only minor issues remain, the round cap of the chosen tier is reached, or a round fixes nothing. Announces its cost first and asks twice. Web. Needs a host that can spawn subagents.

The English summary is being prepared. Showing the original description.

AThevon/genjutsu555Updated Oct 6, 2026

Algorithmic and generative art with Canvas 2D - particles, flow fields, noise, fractals, L-systems.

The English summary is being prepared. Showing the original description.

AThevon/genjutsu555Updated Oct 6, 2026

cast

Free

Cast genjutsu on a UI - creative coding for motion, micro-interactions, and wow-factor. Scans the stack, proposes an interaction thesis, loads the right sub-skills, implements the illusion. Adapts to Web, Android (Compose), Apple (SwiftUI).

The English summary is being prepared. Showing the original description.

AThevon/genjutsu555Updated Oct 6, 2026

Advanced Compose visuals - Material 3 Expressive motion physics, AGSL shaders (Android 13+), Canvas/DrawScope generative, graphicsLayer effects.

The English summary is being prepared. Showing the original description.

AThevon/genjutsu555Updated Oct 6, 2026

Compose Multiplatform / KMP patterns - expect/actual composables, platform-specific code, density and font handling cross-target, iOS/Android/Desktop interop.

The English summary is being prepared. Showing the original description.

AThevon/genjutsu555Updated Oct 6, 2026

Zero-dependency animations and visual techniques - scroll-driven, View Transitions, @starting-style, modern CSS.

The English summary is being prepared. Showing the original description.

AThevon/genjutsu555Updated Oct 6, 2026

See all skills by AThevon

Report a problem with this skill