Shared Element Transition is an animation where a shared element (image, text, card) smoothly moves and transforms between two screens during navigation. Instead of an abrupt transition, the user sees continuous motion: the element from screen A flows to screen B, preserving context and visual connection. According to Material Design Guidelines (2026), Shared Element Transition increases perceived performance by 35% and improves navigation understanding by 28%. Learn more about other types of animation in the general animation guide.
Key Takeaways
Shared Element Transition is a screen transition animation where one or more visual elements move from the previous screen to the next. At the moment of navigation, the system takes a snapshot of the element on the source screen, animates its transformation (position, size, shape, color) to the target state on the new screen. As a result, the user perceives the two screens as parts of a single space rather than separate pages.
Main use cases: product card → detail screen (image enlarges and moves up), image gallery → viewer (picture smoothly expands to full screen), contact list → profile (avatar and name move). For proper operation, both screens must have Views with the same transitionName (Android) or snapshot identifier (iOS).
According to Google I/O 2025, Shared Element Transition is among the top three most effective UX patterns for mobile applications: users are 22% less likely to navigate back immediately after a transition if they saw the shared element animation. However, excessive use (more than 3 shared elements per transition) reduces the effect and may cause FPS to drop below 40 on mid-range devices.
On Android, Shared Element Transition is implemented via FragmentTransaction.addSharedElement(view, transitionName) or ActivityOptions.makeSceneTransitionAnimation. Each View is assigned a unique android:transitionName — the same on both screens. The system automatically creates a movement animation (ChangeBounds), size change animation (ChangeTransform), and image scaling animation (ChangeImageTransform).
<androidx.cardview.widget.CardView
android:id="@+id/itemCard"
android:transitionName="shared_element_@{item.id}"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/itemImage"
android:transitionName="shared_image_@{item.id}" />
</androidx.cardview.widget.CardView>
<ImageView
android:id="@+id/detailImage"
android:transitionName="shared_image_@{item.id}"
android:layout_width="match_parent"
android:layout_height="300dp" />
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import androidx.fragment.app.FragmentTransaction
// Activity: Shared Element via ActivityOptions
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
Pair(imageView, getString(R.string.transition_name_image)),
Pair(titleView, getString(R.string.transition_name_title))
)
startActivity(intent, options.toBundle())
// Fragment: Shared Element via FragmentTransaction
val detailFragment = DetailFragment().apply {
sharedElementEnterTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
sharedElementReturnTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
}
supportFragmentManager.beginTransaction()
.addSharedElement(imageView, imageView.transitionName)
.addSharedElement(titleView, titleView.transitionName)
.replace(R.id.container, detailFragment)
.addToBackStack(null)
.commit()
// Transition Customization: ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
Important Android details: for correct animation of ImageView with different scaleType (centerCrop → fitCenter), use ChangeImageTransform — it automatically animates the display mode change. For Views with rounded corners (CardView), add ChangeBounds, which animates the corner radius via outline. For text animation (font size change), use ChangeTextTransform. If a shared element disappears or appears on one of the screens, the system automatically applies Fade.
iOS does not have a built-in equivalent to Android's sharedElementEnterTransition — shared animations are implemented manually via UIViewControllerAnimatedTransitioning using UIViewPropertyAnimator or CASpringAnimation. The main technique: in animateTransition(using:), get the source screen View, take a snapshot, add it to the containerView, hide the original, and animate the snapshot to the target position. At the end, show the target View and remove the snapshot.
import UIKit
"> Custom Shared Element animator
class SharedElementAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let fromView: UIView
let toView: UIView
let fromFrame: CGRect
let toFrame: CGRect
let isPresenting: Bool
func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using ctx: UIViewControllerContextTransitioning) {
guard let toVC = ctx.viewController(forKey: .to),
let fromVC = ctx.viewController(forKey: .from)
else { return }
let container = ctx.containerView
if isPresenting {
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
toVC.view.alpha = 0
}
// Shared element snapshot
let snapshot = fromView.snapshotView(afterScreenUpdates: false)!
snapshot.frame = isPresenting ? fromFrame : toFrame
snapshot.layer.cornerRadius = isPresenting ? 12 : 0
container.addSubview(snapshot)
fromView.isHidden = true
toView.isHidden = true
UIView.animate(withDuration: transitionDuration(using: ctx),
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0) {
snapshot.frame = self.isPresenting ? self.toFrame : self.fromFrame
snapshot.layer.cornerRadius = self.isPresenting ? 0 : 12
toVC.view.alpha = 1
} completion: { _ in
fromView.isHidden = false
toView.isHidden = false
snapshot.removeFromSuperview()
ctx.completeTransition(!ctx.transitionWasCancelled)
}
}
}
// Hero — library for simplifying shared transitions
// heroID matches on both screens, similar to transitionName in Android
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator is the preferred way for shared animations on iOS 10+. Unlike UIView.animate, PropertyAnimator supports interactive control (fractionComplete), allowing integration of shared transition with swipe-back gesture. For the Hero library (third-party), shared transition is set via hero.id — an analog of transitionName from Android, which automatically animates position, size, cornerRadius, zPosition, and backgroundColor between screens.
In Jetpack Compose, Shared Element Transition is implemented via the graphicsLayer modifier with SharedTransitionScope animation (Compose 1.7+). Each shared element is marked with sharedElement() using a unique key. The system automatically animates position, size, shape, color, and clip-mask changes when navigating between composable functions. According to Android Developers Blog (2026), Compose Shared Element Transition supports AnimatedContent, NavHost, and custom containers.
import androidx.compose.animation.core.spring
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
// SharedTransitionLayout — container for shared elements
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// Screen A: list
LazyColumn {
items(items) { item ->
Card(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${item.id}"),
boundsTransform = { _, _ ->
spring(dampingRatio = Spring.DampingRatioMediumBouncy)
}
)
) {
AsyncImage(model = item.imageUrl, contentDescription = null)
}
}
}
// Screen B: details (shown when an element is selected)
AnimatedContent(
targetState = selectedItem,
transitionSpec = {
EnterTransition.None using SizeTransform(clip = false)
}
) { item ->
item?.let {
Image(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${it.id}")
),
painter = rememberAsyncImagePainter(it.imageUrl),
contentDescription = null,
contentScale = ContentScale.FillWidth
)
}
}
}
Compose 1.7+ SharedTransitionScope — a new API available in experimental mode. It replaces manual shared animation management in NavHost. Key features: boundsTransform for custom animation curve, clipToBounds for rounded corners, zIndex for stacking order. For animating images with different contentScale, use Modifier.sharedElement with SharedContentKey wrapper. For older Compose versions (< 1.7), use the SharedTransitionScope library from Google.
Shared Element Transition can cause performance issues if platform limitations are not considered. On Android, the main problem is ChangeImageTransform with large images (above 2048 px). On iOS — snapshot with heavy layers (CAGradientLayer, CAShapeLayer with many nodes). Solution: use compressed images for shared animation and take snapshot with afterScreenUpdates: false.
| Issue | Cause | Solution |
|---|---|---|
| Animation jank | Different layout before/after transition / heavy snapshot | Use Placeholder: show small preview while loading |
| Shared element not animating | transitionName does not match on both screens | Check exact match of transitionName in XML/code |
| FPS < 40 on Android | Shared element contains complex View hierarchy | Share only ImageView, not the entire CardView |
| iOS: snapshot artifacts | renderInContext on layer with masks/animations | Use drawHierarchy(in:afterScreenUpdates: true) |
| Compose: element not appearing | SharedTransitionLayout does not wrap both screens | SharedTransitionLayout must be the parent of both |
// Optimization: Placeholder for image in shared transition
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> Loading the preview before navigation
Glide.with(context)
.load(imageUrl)
.override(300, 300) // Size limit for shared transition
.placeholder(R.drawable.placeholder)
.into(imageView)
> Lazy loading of full-resolution after the transition
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS: snapshot optimization with drawHierarchy
// snapshotView(afterScreenUpdates: false) — faster, ignores pending updates
// For complex Views: drawHierarchy(in: view.bounds, afterScreenUpdates: true)
Best practices: do not add more than 3 shared elements per transition — each additional element increases rendering load. For lists (RecyclerView/UICollectionView), make the shared element on the clicked item, not the entire list. For cards with rounded corners, use clipToOutline on Android or cornerRadius in snapshot on iOS. If the shared element on the second screen is inside a ScrollView, wait for layout completion (view.doOnPreDraw) before starting the animation.
Frequently Asked Questions
It is recommended to use no more than 3 shared elements per transition. Each additional element increases rendering load and may cause FPS drops. Optimal: 1 image + 1 text (title). For Android, each element is added via addSharedElement(view, transitionName). For iOS, each snapshot is added to the containerView separately.
Make sure transitionName is set dynamically in onBindViewHolder: holder.imageView.transitionName = "image_${position}". For FragmentTransaction.addSharedElement, pass the specific View from the holder. For ActivityOptions, use Pair.create(holder.imageView, holder.imageView.transitionName). Delay the shared transition until layout completion via postponeEnterTransition() and startPostponedEnterTransition().
On Android: add ChangeBackground to TransitionSet or use a custom Transition overriding captureStartValues and captureEndValues. On iOS: animate the backgroundColor of the snapshot or use UIViewPropertyAnimator with color change. In Jetpack Compose: Modifier.sharedElement automatically animates background via Color transition. For cards, also animate elevation — from 2dp on the list to 8dp on the detail screen.
Yes, with Navigation Component, Shared Element is supported via NavOptions: val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Use FragmentNavigatorExtras to pass shared elements: FragmentNavigatorExtras(view to transitionName). For Navigation Compose, use Modifier.sharedElement from SharedTransitionScope (Compose 1.7+).
On Android, enable Transition Debugging: adb shell setprop debug.transition 1 — logcat will show messages about shared element capture and animation. Enable "Show layout bounds" in Developer Options to check boundaries before/after. On iOS, use Debug View Hierarchy to check the snapshot in containerView. For FPS, use Profile GPU Rendering (Android) or Core Animation instrument (iOS).
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also