← Writing
5 min read

Shared element transitions, and why they work

A thumbnail that grows into a hero is doing something no fade can: preserving object permanence. How the technique works, what FLIP is, and when it backfires.

  • technique
  • patterns

Tap a card in a grid and the detail view fades in. Fine, common, forgettable.

Tap a card and watch that card grow, move, and become the detail view. Suddenly the interface has told you something: this is the same thing, seen closer. You did not navigate to a new page. You moved through space.

That is a shared element transition, and the reason it feels so much better than a crossfade is not polish. It is that it preserves object permanence.

The mechanism

The insight is that “the same element moving between two screens” is almost never literally true. In a grid-to-detail transition you have two separate DOM elements: a small thumbnail in a grid, and a large image in a detail layout. They may not even exist at the same time.

So the trick is to make it look like one element by animating between the two geometries:

  1. Measure where the element is now: position, size
  2. Render the destination state
  3. Measure where it ended up
  4. Apply a transform that puts it visually back at the start
  5. Animate that transform away to zero

The element is never actually in motion between two places. It is placed at its destination immediately, transformed to look like it is still at the origin, and then released.

This is FLIP (First, Last, Invert, Play), and it is worth knowing by name because it long predates any browser API for it, and because understanding it explains why the native APIs behave the way they do.

Why invert rather than animate directly

You could skip the trick and animate width, height, top and left from the start values to the end values. It would work and it would be slow. Every frame triggers layout, and layout on a page mid-navigation is the most expensive moment to be doing it.

The inversion converts the whole thing into transform and opacity, which skip layout and paint entirely and can run off the main thread. That is why only transform and opacity are cheap, applied to the case where it matters most.

There is a catch, and it is the main source of ugly shared-element transitions: scaling a container scales its contents. Text inside a card being scaled from thumbnail to hero size will stretch and squash. The standard fix is a counter-scale: animate the child by the inverse of the parent’s scale so it stays visually stable. Text-heavy shared elements often look better cross-fading their contents while only the container transforms.

The native API

The View Transitions API does most of this for you. Tag the shared element on both sides with a matching name:

.card-image     { view-transition-name: hero-image; }
.detail-image   { view-transition-name: hero-image; }

Then wrap the DOM change:

document.startViewTransition(() => updateTheDOM());

The browser snapshots before and after, matches elements by view-transition-name, and animates between the two geometries. It is the FLIP dance, handled natively, with pseudo-elements (::view-transition-old(), ::view-transition-new()) you can style.

Two constraints worth internalising. A given view-transition-name must be unique on the page at any moment. Two elements sharing one is the most common failure, and it silently disables the transition. In a grid this means assigning the name only to the clicked card, usually just before the transition starts and removing it after.

And the API is progressive: where it is unsupported, startViewTransition is undefined and you fall back to an unanimated DOM update. Feature-detect and move on rather than shipping a polyfill for a nicety.

When it works

Shared element transitions earn their complexity when there is a genuine parent-child or overview-detail relationship:

  • Grid thumbnail → detail hero
  • List row → expanded record
  • Collapsed player → full-screen player
  • Avatar → profile
  • A card → the modal it opened

The common thread: the user can point at the thing before and after. If they can, the transition confirms a relationship they already suspect. Object permanence is doing the work.

When it backfires

When the elements are not actually the same thing. Morphing a button into an unrelated panel because both happen to be rectangles is visual noise dressed as meaning. The user reads a relationship that does not exist.

When the geometry change is extreme. A 40px avatar becoming a full-bleed header travels so far and scales so much that the motion reads as a zoom effect rather than a movement. Past roughly a 4–5× scale change, a well-timed crossfade often communicates better.

When it delays something frequent. A shared element transition on a list people open forty times a day is forty small taxes. This is exaggeration in the classical sense: delightful once, an obstacle by the fortieth repetition. Reserve it for navigation that is genuinely occasional, or make it fast: 250–300ms, not 500.

When the destination is not ready. If the detail view is waiting on a network request, the shared element lands in an empty shell and the illusion collapses. Either have the data, or transition into a skeleton that occupies the same geometry.

Practical notes

Aspect ratios should match, or be handled. A square thumbnail becoming a 16:9 hero will distort mid-flight unless you crop consistently. object-fit: cover on both ends with a matched focal point is usually enough.

Give it an arc for diagonal movement. Motion that changes both x and y along a straight line looks mechanical; a gentle arc looks carried. This is one of the few places arcs genuinely belong in interface motion.

Do not transition more than one or two elements. Naming five shared elements produces five simultaneous motions, which violates staging. The eye can track one thing. Pick the hero, cut the rest.

Reduced motion should collapse it to a crossfade, not remove it. The user still needs to understand that the view changed; they just should not be flown across the screen to learn it.

Why bother

Most interface animation is decoration that could be removed without loss. Shared element transitions are one of the few that carry information. They answer “where did this come from and where did it go” without a word of copy.

That is the bar worth holding them to. If yours does not answer that question, it is an effect, and it should probably be a crossfade.