Shared Element Transition — What It Is, Shared Elements in Transitions

Author: IT Sectr Published: 2026-03-02 Reading time: 10 min

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 — animation of a shared element between two screens during navigation.
  • sharedElementEnterTransition — Android XML attribute for configuring shared element entry.
  • UIViewPropertyAnimator — foundation for custom shared animations between UIViewControllers in iOS.
  • The shared element is defined via transitionName in Android and tag or snapshot in iOS.
  • Animation works with ImageView, TextView, CardView, Button and any View with a unique identifier.

What is Shared Element Transition?

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.

Shared Element Transition in Android (Kotlin)

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).

xml

<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" />
kotlin
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.

Shared Element Transition in iOS (Swift)

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.

swift
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.

Shared Element in Jetpack Compose

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.

kotlin
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.

Optimization and Common Issues

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.

IssueCauseSolution
Animation jankDifferent layout before/after transition / heavy snapshotUse Placeholder: show small preview while loading
Shared element not animatingtransitionName does not match on both screensCheck exact match of transitionName in XML/code
FPS < 40 on AndroidShared element contains complex View hierarchyShare only ImageView, not the entire CardView
iOS: snapshot artifactsrenderInContext on layer with masks/animationsUse drawHierarchy(in:afterScreenUpdates: true)
Compose: element not appearingSharedTransitionLayout does not wrap both screensSharedTransitionLayout must be the parent of both
kotlin
// 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

How many shared elements can be animated in one transition?

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.

Shared Element Transition does not work with RecyclerView — what to do?

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().

How to do Shared Element Transition with background color change?

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.

Does Shared Element work on Android with Jetpack Navigation?

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+).

How to debug Shared Element Transition?

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

  • Shared Element Transition — animation of a shared element between screens while preserving visual context.
  • Android: FragmentTransaction.addSharedElement or ActivityOptions.makeSceneTransitionAnimation.
  • iOS: custom implementation via UIViewControllerAnimatedTransitioning + snapshot + PropertyAnimator.
  • Jetpack Compose: Modifier.sharedElement from SharedTransitionScope (Compose 1.7+).
  • Maximum 3 shared elements per transition to avoid FPS drops.
  • For ImageView with different scaleType, use ChangeImageTransform (Android) or snapshot (iOS).
  • Hero library (iOS) — third-party solution with hero.id, similar to transitionName in Android.

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.

Discuss the project

Read also