Jetpack Compose animation foundations - animate*AsState, AnimatedVisibility, Crossfade, updateTransition, SharedTransitionLayout, gestures.
npx skills add https://github.com/AThevon/genjutsu --skill compose-motion
> Jetpack Compose animation core. Loaded for Android Compose and Compose Multiplatform projects.
> Concise rules here. Deep-dive in references/.
> Baseline: current stable Jetpack Compose (1.7+). Modern stable APIs only - no swipeable, no animateContentSize hacks where AnimatedContent is correct.
| 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.
| 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 Butterval 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 AnimAnimatedVisibility(
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 SwapAnimatedContent(
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 StatesCrossfade(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 Coordinatedval 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 Controlval 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 AnimationsSharedTransitionLayout {
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 - Loopingval 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.
| 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).
// 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.
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.
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.
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.
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.
| 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 |
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Improves the quality of images, especially screenshots, by enhancing resolution, sharpness, and clarity. Perfect for preparing images for presentations, documentation, or social media posts.
Downloads videos from YouTube and other platforms for offline viewing, editing, or archival. Handles various formats and quality options.
Lightweight WSI tile extraction and preprocessing. Use for basic slide processing tissue detection, tile extraction, stain normalization for H&E images. Best for simple pipelines, dataset preparation, quick tile-based analysis. For advanced spatial proteomics, multiplexed imaging, or deep learning pipelines use pathml.
Microscopy data management platform. Access images via Python, retrieve datasets, analyze pixels, manage ROIs/annotations, batch processing, for high-content screening and microscopy workflows.
Python library for working with DICOM (Digital Imaging and Communications in Medicine) files. Use this skill when reading, writing, or modifying medical imaging data in DICOM format, extracting pixel data from medical images (CT, MRI, X-ray, ultrasound), anonymizing DICOM files, working with DICOM metadata and tags, converting DICOM images to other formats, handling compressed DICOM data, or processing medical imaging datasets. Applies to tasks involving medical image analysis, PACS systems, radiology workflows, and healthcare imaging applications.
This skill should be used when working with pre-trained transformer models for natural language processing, computer vision, audio, or multimodal tasks. Use for text generation, classification, question answering, translation, summarization, image classification, object detection, speech recognition, and fine-tuning models on custom datasets.
Take athevon/compose-motion from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.