Jetpack Compose Animations: 4 Techniques to Make Your App Feel Alive

Published: (March 1, 2026 at 07:18 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Animation is the heartbeat of modern Android apps. It transforms static UIs into fluid, responsive experiences that feel natural and delightful to users. Jetpack Compose, Google’s modern declarative UI framework for Android, provides powerful built‑in APIs to create smooth animations with just a few lines of code.

In this guide, I’ll walk you through four essential animation techniques in Compose that will elevate your app’s feel from ordinary to exceptional. Each technique comes with practical code examples you can use immediately in your projects.

1. animateFloatAsState: The Foundation of Smooth Value Changes

animateFloatAsState is perhaps the most commonly used animation API in Compose. It smoothly animates a float value from its current state to a target value, perfect for opacity fades, scale changes, and rotations.

Use Case: Animated Button Opacity on Tap

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha

@Composable
fun AnimatedOpacityButton() {
    var isPressed by remember { mutableStateOf(false) }

    // Animate opacity from 1.0 to 0.5 when pressed
    val alpha by animateFloatAsState(
        targetValue = if (isPressed) 0.5f else 1.0f,
        label = "button_alpha"
    )

    Button(
        onClick = { isPressed = !isPressed },
        modifier = Modifier.alpha(alpha)
    ) {
        Text("Tap me")
    }
}

Why It Works

  • Smooth Transitions – The animation runs over 300 ms by default, creating a natural fade effect.
  • State‑Driven – The animation automatically triggers whenever the state condition changes.
  • Performance – Uses low‑level graphics APIs for buttery‑smooth 60 fps animations.

Customizing Animation Speed

You can control the animation duration with animationSpec:

val alpha by animateFloatAsState(
    targetValue = if (isPressed) 0.5f else 1.0f,
    animationSpec = tween(durationMillis = 500), // Slower fade
    label = "button_alpha"
)

2. AnimatedVisibility: Show/Hide with Polish

While animateFloatAsState handles value changes, AnimatedVisibility takes composables in and out of the view hierarchy with elegant enter/exit animations.

Use Case: Animated Error Message

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun LoginForm() {
    var showError by remember { mutableStateOf(false) }
    var email by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") }
        )

        // Error message with slide + fade animation
        AnimatedVisibility(
            visible = showError,
            enter = slideInVertically() + fadeIn(),
            exit = slideOutVertically() + fadeOut()
        ) {
            Text(
                "Invalid email format",
                color = Color.Red,
                modifier = Modifier
                    .background(Color(0xFFFFEBEE))
                    .padding(12.dp)
            )
        }

        Button(onClick = {
            showError = email.isEmpty() || !email.contains("@")
        }) {
            Text("Sign In")
        }
    }
}

Animation Combinations

Compose lets you combine multiple animations:

enter = slideInVertically() + fadeIn() + expandVertically()
exit  = slideOutVertically() + fadeOut() + shrinkVertically()

This creates a polished effect where the error message slides in, fades in, and expands all at once.

3. animateContentSize: Smooth Layout Changes

When a composable’s size changes due to content updates, animateContentSize animates the layout change instead of jumping instantly.

Use Case: Expanding Description Text

import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp

@Composable
fun ExpandableText(title: String, fullText: String) {
    var isExpanded by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .clickable { isExpanded = !isExpanded }
            .animateContentSize(animationSpec = tween(durationMillis = 400))
            .padding(16.dp)
    ) {
        Text(
            text = title,
            style = MaterialTheme.typography.headlineSmall
        )

        Text(
            text = fullText,
            maxLines = if (isExpanded) Int.MAX_VALUE else 3,
            overflow = TextOverflow.Ellipsis
        )
    }
}

Feel free to copy these snippets into your own projects and experiment with the parameters to match your app’s design language.

The Magic

When maxLines changes from 3 to MAX_VALUE, the column’s height expands smoothly. No jarring jumps—just elegant growth.

4. updateTransition + Crossfade: Complex State Animations

For more complex animations involving multiple properties changing together, updateTransition orchestrates all animations as a single logical unit.

Use Case: Loading State with Spinner Rotation

import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

enum class LoadingState {
    Idle, Loading, Success, Error
}

@Composable
fun LoadingIndicator(state: LoadingState) {
    val transition = updateTransition(targetState = state, label = "loading_transition")

    val rotation by transition.animateFloat(label = "rotation") {
        if (it == LoadingState.Loading) 360f else 0f
    }

    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Crossfade(targetState = state, label = "content_crossfade") { loadingState ->
            when (loadingState) {
                LoadingState.Idle -> Text("Ready to load")
                LoadingState.Loading -> CircularProgressIndicator(
                    modifier = Modifier
                        .size(40.dp)
                        .rotate(rotation)
                )
                LoadingState.Success -> Text("Loaded!", color = Color.Green)
                LoadingState.Error -> Text("Error occurred", color = Color.Red)
            }
        }
    }
}

Why updateTransition Is Powerful

  • Orchestration – All animations tied to one state change happen in sync.
  • Consistency – Prevents animations from fighting each other.
  • Performance – Recomposes only when animation values change, not on every frame.

Bonus: Crossfade for Elegant Content Switching

Crossfade animates the transition between two different composables by fading the old one out while the new one fades in.

@Composable
fun TabContent(selectedTab: Int) {
    Crossfade(targetState = selectedTab, label = "tab_switch") { tab ->
        when (tab) {
            0 -> HomeScreen()
            1 -> ProfileScreen()
            else -> SettingsScreen()
        }
    }
}

No complex enter/exit logic is needed—Crossfade handles the visual elegance automatically.

Performance Tips

  • Use the label parameter – Helps with debugging in Android Studio’s Animation Inspector.
  • Prefer animateFloatAsState for simple values – It’s optimized and lightweight.
  • Avoid animating during initial composition – State must be stable before the animation starts.
  • Test on real devices – Emulators don’t accurately represent animation performance.

Bringing It All Together

The most polished apps combine these techniques:

TechniqueIdeal Use
animateFloatAsStateQuick state feedback (button presses, toggles)
AnimatedVisibilityEntering/exiting screens and dialogs
animateContentSizeOrganic content growth (expanding lists, text)
updateTransitionComplex, synchronized state changes

Each technique solves a specific animation problem, and mastering all four will make your Compose apps feel genuinely delightful.

Next Steps

All 8 templates use clean Compose UI ready for animations:

Start by implementing animateFloatAsState in your next feature, then gradually explore the other techniques as your app’s complexity grows.

Happy animating!

0 views
Back to Blog

Related posts

Read more »