Jetpack Compose animation foundations - animate*AsState, AnimatedVisibility, Crossfade, updateTransition, SharedTransitionLayout, gestures.
Scanned 9/10/2026
Install to Claude Code
npx -y skills add nexuslinkproductions/yuri-os --skill genjutsu-compose-motion --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Genjutsu Compose Motion?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/nexuslinkproductions-genjutsu-compose-motion-yuri-os)More formats (shields.io, HTML) on the badges page.
---
name: genjutsu-compose-motion
description: "Jetpack Compose animation foundations - animate*AsState, AnimatedVisibility, Crossfade, updateTransition, SharedTransitionLayout, gestures."
---
# Compose Motion - Sub-skill
> Jetpack Compose animation core. Loaded for Android Compose and Compose Multiplatform projects.
> Concise rules here. Deep-dive in `references/`.
> Baseline: Compose 1.7+ (April 2026 stable). Modern stable APIs only - no `swipeable`, no `animateContentSize` hacks where `AnimatedContent` is correct.
---
## API Decision Tree
| Need | API |
|---|---|
| Single value over time | `animateFloatAsState`, `animateDpAsState`, `animateColorAsState`, etc. |
| Visibility / mount-unmount | `AnimatedVisibility(visible) { ... }` |
| Crossfade between states | `Crossfade(target) { state -> ... }` |
| Multi-state coordinated | `updateTransition(target).animateFloat { ... }` |
| Manual control / interruption | `Animatable(initialValue)` + `animateTo(...)` |
| Looping / infinite | `rememberInfiniteTransition().animateFloat(...)` |
| Shared elements | `SharedTransitionLayout` + `Modifier.sharedElement(...)` (Compose 1.7+) |
| Layout swap with anim | `AnimatedContent(target) { ... }` |
| Drag / swipe | `Modifier.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)
| Use | Spec |
|---|---|
| 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: `VeryLow` 200, `Low` 400, `MediumLow` 700, `Medium` 1500, `High` 10000. Higher = faster settle. Damping constants: `HighBouncy` 0.2, `MediumBouncy` 0.5, `LowBouncy` 0.75, `NoBouncy` 1.0. Below 1.0 overshoots. Springs ignore `durationMillis`; if you need a deterministic duration, use `tween(...)` instead.
---
## `animate*AsState` - The Bread and Butter
```kotlin
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
```kotlin
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
```kotlin
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
```kotlin
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
```kotlin
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
```kotlin
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
```kotlin
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(
state = 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
```kotlin
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)
| Need | Modifier |
|---|---|
| Tap | `Modifier.clickable(onClick = ...)` |
| Tap + long-press | `Modifier.combinedClickable(onClick = ..., onLongClick = ...)` |
| Drag (single axis) | `Modifier.draggable(state, orientation)` |
| Multi-touch / custom | `Modifier.pointerInput { detectDragGestures { ... } }` |
| Scroll offset reading | `LazyListState` or `ScrollState.value` |
| Swipe-to-dismiss / snap points | `Modifier.anchoredDraggable(state)` (Compose 1.6+, replaces `swipeable`) |
| Pinch + pan + rotate | `Modifier.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
```kotlin
// 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))
```
```kotlin
// 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
```kotlin
// 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)
}
```
```kotlin
// 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
```kotlin
// BAD - on insert/delete/reorder, items animate to wrong slots
LazyColumn {
items(list) { item ->
AnimatedVisibility(visible = item.expanded) { ItemRow(item) }
}
}
```
```kotlin
// 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
```kotlin
// BAD - every item runs its own transition graph; scroll = jank
LazyColumn {
items(list, key = { it.id }) { item ->
AnimatedContent(targetState = item.state) { state -> Row(state) }
}
}
```
```kotlin
// 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.
```kotlin
@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` covers the developer-options "Animations off" toggle. For the user-facing accessibility setting on Android 14+ ("Remove animations" in Settings -> Accessibility), use `AccessibilityManager.areTransitionsEnabled()` (API 34+). Deep-dive in `../mobile-principles/references/accessibility-mobile.md`.
---
## Quick Reference
| Need | Load |
|---|---|
| Shared transitions deep-dive | `references/shared-transitions.md` |
| Gestures + nestedScroll patterns | `references/gestures-compose.md` |
| Recomposition / jank / Layout Inspector | `references/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
- [aldefy/compose-skill](https://github.com/aldefy/compose-skill) - animation reference
- [skydoves/Orbital](https://github.com/skydoves/Orbital) - shared element transitions
- [mutualmobile/compose-animation-examples](https://github.com/mutualmobile/compose-animation-examples)
- [fornewid/material-motion-compose](https://github.com/fornewid/material-motion-compose)
- [Android Compose animations docs](https://developer.android.com/develop/ui/compose/animation/introduction)
- [Android SharedTransitionLayout](https://developer.android.com/develop/ui/compose/animation/shared-elements)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!