Animated Visibility
Enter and exit transitions composed from slideIn, fadeIn, scaleIn. The spec defines the choreography.
Jetpack Compose ships AnimatedVisibility as the default way to animate a composable on or off the screen. Most developers reach for it the first time they need a fade or a slide, pass a single transition, and move on. The interesting part starts when you compose multiple transitions together and shape the perceived motion through timing, direction, and origin. A short slide that arrives with a fade reads very differently from a slow scale that lingers, even though both use the same surface API.
In this article, you'll explore one runnable example that pairs three AnimatedVisibility cells side by side, how the + operator combines slideIn, fadeIn, and scaleIn into a single enter spec, how SLIDE_FROM_RIGHT flips the offset function, and how ANIM_DURATION_MS synchronizes the choreography across every transition.
How the example is structured
The screen renders a Column with a title, a single toggle button, and a Row containing three demo cells. Each cell wraps an AnimatedVisibility block that watches the same visible state, so pressing the button drives all three animations from one source of truth.
val ANIM_DURATION_MS = 2600
val SLIDE_FROM_RIGHT = true
var visible by remember { mutableStateOf(true) }
Button(onClick = { visible = !visible }) {
Text(text = if (visible) "Hide all" else "Show all")
}
The two top level constants act as the only knobs you need. ANIM_DURATION_MS feeds every tween call so the slide, fade, and scale all finish at the same moment. SLIDE_FROM_RIGHT is a boolean that branches the offset lambda inside the slide transition. Keeping these values at the top makes the example friendly to hot reload: you change the number, save the file, and the running animation reflows without restarting the app.
The three cells are labeled SLIDE, FADE, and SCALE. Each one contains a small AnimChip card that appears and disappears according to its own enter and exit spec. Running them next to each other lets you compare the personality of each transition under identical timing.
Composing enter and exit transitions
The SLIDE cell shows the additive pattern that makes AnimatedVisibility flexible. You build an enter spec by combining a positional transition with an opacity transition using the + operator.
AnimatedVisibility(
visible = visible,
enter = slideInHorizontally(
animationSpec = tween(ANIM_DURATION_MS),
initialOffsetX = { fullWidth -> if (SLIDE_FROM_RIGHT) fullWidth else -fullWidth },
) + fadeIn(animationSpec = tween(ANIM_DURATION_MS)),
exit = slideOutHorizontally(
animationSpec = tween(ANIM_DURATION_MS),
targetOffsetX = { fullWidth -> if (SLIDE_FROM_RIGHT) fullWidth else -fullWidth },
) + fadeOut(animationSpec = tween(ANIM_DURATION_MS)),
) {
AnimChip(text = "SLIDE")
}
The + operator on EnterTransition and ExitTransition does not chain animations sequentially. It merges them into a single transition that runs every component in parallel. When you write slideInHorizontally(...) + fadeIn(...), the framework starts both at the same time and stops them at the same time, sharing a single visibility lifecycle. This is why the chip slides in from the right while its alpha climbs from zero, and both finish on the same frame.
The SCALE cell follows the same pattern but swaps the geometric component:
enter = scaleIn(
animationSpec = tween(ANIM_DURATION_MS),
transformOrigin = TransformOrigin(0.5f, 0.5f),
) + fadeIn(animationSpec = tween(ANIM_DURATION_MS)),
exit = scaleOut(
animationSpec = tween(ANIM_DURATION_MS),
transformOrigin = TransformOrigin(0.5f, 0.5f),
) + fadeOut(animationSpec = tween(ANIM_DURATION_MS)),
The transformOrigin of (0.5f, 0.5f) anchors the scale at the center of the chip, so the card grows out of its midpoint instead of expanding from a corner. Pairing the scale with fadeIn softens the appearance: without the fade, the chip would pop into view at full opacity while still being small, which reads as a glitch rather than an entrance.
The FADE cell is the minimal case. It uses a single fadeIn for enter and fadeOut for exit, with no positional component. This is the baseline you compare the other two cells against when you decide how much motion an interface needs.
Direction and timing
Two pieces of the spec control how the motion feels: where the slide starts from, and how long every transition runs. Both live in the offset lambda and the tween spec respectively.
initialOffsetX = { fullWidth -> if (SLIDE_FROM_RIGHT) fullWidth else -fullWidth }
The lambda receives the measured width of the content as fullWidth and returns the starting offset on the X axis. Returning fullWidth places the chip one full width to the right of its final position, so the slide travels leftward into place. Returning -fullWidth mirrors that direction. Because the lambda runs after layout, the offset always matches the actual size of the chip. This avoids hardcoding pixel values that break across screen sizes or font scales.
tween(ANIM_DURATION_MS) produces a time based animation spec with a default easing curve. The 2600 millisecond value is intentionally slow for a demo, which gives you time to watch the slide, fade, and scale unfold side by side. In production, durations between 200 and 400 milliseconds usually feel right for content that appears in response to a tap. The exact value is a perception choice, not a technical one.
Sharing the same duration across the slide and the fade is what makes the combined transition read as one motion. If you gave the slide 400 milliseconds and the fade 1000, you would see the chip arrive at its destination while still translucent, which breaks the illusion of a single object entering the scene. Synchronized timing is the simplest way to keep composed transitions cohesive.
Tweaking the choreography
The example exposes a small set of constants and lambdas you can change to feel out the design space.
ANIM_DURATION_MS: Lower it toward300for snappy UI feedback. Raise it past1500and the same animation starts to feel ceremonial, which suits hero content like an onboarding screen.SLIDE_FROM_RIGHT: Flipping it tofalseswaps the direction so the chip enters from the left. The same toggle drives bothinitialOffsetXandtargetOffsetX, which keeps enter and exit symmetric.- The
+operands: ReplaceslideInHorizontallywithslideInVerticallyto make the chip drop in from above. ReplacescaleInwithexpandInto animate the layout bounds rather than a graphics layer scale, which affects how surrounding content reflows. You can also remove thefadeInentirely to see how a pure slide or pure scale reads on its own. transformOrigin: Move it toTransformOrigin(0f, 0f)and the scale grows out of the top left corner. Setting it to(1f, 1f)makes the chip emerge from the bottom right. The origin lets you tie the animation to a meaningful point in the layout, like the button that triggered it.tweenversusspring: Swappingtweenforspring(dampingRatio = Spring.DampingRatioMediumBouncy)replaces the linear timeline with a physics based curve. The duration constant becomes irrelevant and the motion ends when the spring settles.
Each of these changes leaves the surrounding AnimatedVisibility structure intact. You are tuning the spec, not rewriting the composable, which is the property that makes the API worth composing in the first place.
Conclusion
In this article, you've walked through a single AnimatedVisibility example that runs three composed transitions side by side, how slideIn, fadeIn, and scaleIn combine through the + operator into a unified spec, and how ANIM_DURATION_MS and SLIDE_FROM_RIGHT shape the perceived motion without changing the structure of the composable.
Understanding that + produces parallel composition rather than sequential chaining helps you reason about why synchronized durations matter and why pairing a geometric transition with a fade almost always produces a smoother result than either one alone. The offset lambdas read layout values at runtime, so direction changes stay safe across device sizes. The transformOrigin parameter is the bridge between an animation and the spatial context it lives in.
Whether you are designing a settings panel that slides in from the edge, a chip that scales into place when a filter activates, or a hero card that fades in on first launch, the same combinatorial pattern applies. You pick a geometric transition, pair it with a fade, share a single duration, and tune the origin until the motion feels intentional. The spec is where the choreography lives.
As always, happy coding!
— Jaewoong (skydoves)