Mastering Jetpack Compose Shared Element Transitions
Creating smooth UI transitions is key to making an app feel polished and premium. In traditional Android XML development, creating shared element transitions (e.g., animating an image from a list grid item into a full-screen detail hero) was notoriously complex, often leading to flashing screens and broken layouts.
In 2026, Jetpack Compose offers a robust, type-safe API for handling shared elements. This guide walks you through building a shared element transition between a list and a details view using the modern Compose Navigation library.
The Core Concept: SharedTransitionLayout
To animate elements between different screens, you must wrap your navigation host inside a SharedTransitionLayout. This component coordinates layout animations across screen boundaries.
It exposes a SharedTransitionScope that child screens use to mark their shared components:
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.runtime.Composable
@Composable
fun AppNavigation() {
val navController = rememberNavController()
SharedTransitionLayout {
NavHost(navController = navController, startDestination = "list") {
composable("list") {
ListScreen(
onItemClick = { itemId -> navController.navigate("details/$itemId") },
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@composable
)
}
composable("details/{itemId}") { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId") ?: ""
DetailsScreen(
itemId = itemId,
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@composable
)
}
}
}
}
Linking Elements with renderInSharedTransitionScope
Inside the ListScreen and DetailsScreen, use the sharedElement modifier to link the items. Give them a matching rememberSharedContentState key:
The List Item:
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@Composable
fun SharedTransitionScope.ListScreen(
onItemClick: (String) -> Unit,
animatedVisibilityScope: AnimatedVisibilityScope
) {
val itemId = "item_card_5"
Image(
painter = painterResource(id = R.drawable.item_preview),
contentDescription = "Shared Item",
modifier = Modifier
.clickable { onItemClick(itemId) }
// Define the shared element transition configuration
.sharedElement(
state = rememberSharedContentState(key = "image_$itemId"),
animatedVisibilityScope = animatedVisibilityScope
)
)
}
The Detail Screen:
@Composable
fun SharedTransitionScope.DetailsScreen(
itemId: String,
animatedVisibilityScope: AnimatedVisibilityScope
) {
Image(
painter = painterResource(id = R.drawable.item_preview),
contentDescription = "Shared Item Full",
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
// Match the state key exactly to link the transition!
.sharedElement(
state = rememberSharedContentState(key = "image_$itemId"),
animatedVisibilityScope = animatedVisibilityScope
)
)
}
Key Considerations for Smooth Animation
To make the transition look fluid and avoid stuttering:
- Keep Aspect Ratios Identical: If the preview image is a square (1:1) and the detail image is a rectangle (16:9), the image scale content can warp. Animate the container aspect ratio or use matching crop parameters.
- Postpone Transitions: If the detail screen needs to fetch network data before displaying the image, postpone the transition until the state loads to prevent empty elements from animating.
- Animate Text Carefully: Text layout rendering during resizing can cause letters to reflow. Animate the container size and apply cross-fades for text tags rather than stretching them directly.