← Back to gallery
CSSFeatured

Shared Element Layout Transition

Progressively-enhanced gallery → detail morph using the View Transition API where supported, with a same-document FLIP fallback that keeps the same hero, title, and metadata anchors moving regardless of browser support. Three rhythms: balanced default, gentle settle, and a sharper power-curve.

view-transitionsshared-elementflip-fallbacklayout-transitionview-transition-nameprogressive-enhancementprefers-reduced-motion

Layout transition / shared element transition

View Transition API showcase

Reimplements same-document view transitions with a dense gallery, a featured destination, and stable fallbacks when the API is unavailable.

Shared snapshot card

Gallery Grid

Keeps the cover, title block, and tags readable while the grid cell grows into a hero-sized card.

  • grid reorder
  • shared title
  • tag chips

Sidebar destination

Inspector Rail

Good for dashboards that pin one item while the surrounding collection continues to flow.

  • sidebar pin
  • preview rail
  • detail focus

Dialog landing

Dialog Landing

Useful when you want a shared card to bloom into a higher-fidelity state instead of navigating away.

  • dialog bloom
  • overlay
  • context retained

Shared element inspector

Gallery Grid

  • grid reorder
  • shared title
  • tag chips

Keeps the cover, title block, and tags readable while the grid cell grows into a hero-sized card.

Helped you ship something? 🐟 Send my cat a churu

.view-transition--gallery {
  view-transition-name: preview-gallery-grid;
}

.pattern-title {
  view-transition-name: title-gallery-grid;
}

@supports (view-transition-name: demo) {
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 5200ms;
    animation-timing-function: ease;
  }
}

.pattern-preview--animated {
  animation: shared-preview-loop 5200ms cubic-bezier(0.2, 0.8, 0.2, 1) infinite alternate both;
}

.pattern-preview--inspector {
  transform: scale(1.00);
}

How to make this

A shared element transition gives the source card and destination hero the same view-transition-name, then styles the browser-generated old/new snapshots.

html
<a class="shared-vt-card" href="/detail.html">  <span class="shared-vt-thumb" aria-hidden="true"></span>  <span class="shared-vt-title">Aurora pattern</span></a> <style>7@view-transition {  navigation: auto;}.shared-vt-card {  display: grid;  gap: 0.75rem;  width: 220px;  padding: 0.9rem;  border-radius: 16px;  color: #e5ecff;  background: #101827;}.shared-vt-thumb {  height: 120px;  border-radius: 12px;22  view-transition-name: shared-vt-thumb;23  contain: layout;  background: linear-gradient(135deg, #67e8f9, #a78bfa);}.shared-vt-title {  font: 700 1rem/1.2 sans-serif;28  view-transition-name: shared-vt-title;}30::view-transition-old(shared-vt-thumb),::view-transition-new(shared-vt-thumb) {  animation-duration: 360ms;  animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);}::view-transition-old(shared-vt-title),::view-transition-new(shared-vt-title) {  animation-duration: 240ms;}@media (prefers-reduced-motion: reduce) {40  ::view-transition-old(*),  ::view-transition-new(*) {    animation-duration: 1ms;  }}</style>

Annotated snippet

  1. Line 7@view-transition enables cross-document transitions for regular page navigations. Without it, the names below are harmless CSS but the browser never captures matching snapshots during a link click.
    PitfallWhat browser fallback should I ship for View Transitions?

    Feature-detect the API and design the page so the non-transition path is still correct. Links should still navigate, detail pages should still load, and the same thumbnail/title layout should be understandable without motion. Treat the transition as progressive enhancement, not as the only way to explain where the user went.

  2. Line 22The source thumbnail gets a stable view-transition-name. The destination page must use the exact same name on the hero thumbnail, and only one visible element may own that name at a time.

    Both cards have two thumbnails with the same view-transition-name in the source page. The browser only morphs one element per name — having two visible owners is a silent error: one snapshot ends up unanimated. Unique names per element keep the morph wiring intact.

    PitfallCan two elements use the same view-transition-name?

    Not while both are visible in the same rendered state. Duplicate names make the browser unable to decide which box should become the snapshot, so the shared transition can be skipped. Give every shared piece a unique name, such as card-thumb-42 and card-title-42, and remove or hide the source before the destination with the same name appears.

  3. Line 23contain: layout limits the snapshot boundary so layout changes outside the thumbnail do not distort the captured box. It also makes the shared element less likely to resize from neighboring content.

    Both cards contain the same thumbnail (cyan) with an absolutely-positioned badge anchored to bottom:8 / right:8. Without contain: layout the badge has no containing block on the thumbnail and escapes to the nearest positioned ancestor — it lands at the bottom-right of the whole compare cell, far from the thumbnail. With contain: layout the thumbnail itself becomes the containing block and the badge sits exactly where the snapshot needs it.

    PitfallWhy is my shared element transition not running after I add view-transition-name?

    The name alone is not enough. For cross-document navigation you need @view-transition { navigation: auto; } on both pages, and the source and destination elements must share the exact same view-transition-name. For same-document state changes, wrap the DOM update in document.startViewTransition(). If the API is missing, let the page navigate normally and keep the layout stable.

  4. Line 28The title uses a second transition name instead of riding inside the image snapshot. Text and image usually need different durations because glyphs become blurry when they are scaled as part of one large bitmap.
    PitfallCan two elements use the same view-transition-name?

    Not while both are visible in the same rendered state. Duplicate names make the browser unable to decide which box should become the snapshot, so the shared transition can be skipped. Give every shared piece a unique name, such as card-thumb-42 and card-title-42, and remove or hide the source before the destination with the same name appears.

  5. Line 30The ::view-transition-old/new pseudo-elements are the browser-generated snapshots. Styling these controls the morph timing without adding wrapper divs or measuring element positions in JavaScript.
  6. Line 40Reduced motion should collapse the transition to an almost-instant swap, not remove the navigation. A 1ms duration keeps the browser path consistent while avoiding visible motion.
    PitfallHow do I make View Transitions respect prefers-reduced-motion?

    Keep the navigation path, but collapse the visual transition under @media (prefers-reduced-motion: reduce). Setting the old/new snapshot duration to 1ms avoids animated movement while preserving the same DOM and routing behavior. Do not block the feature entirely in a way that changes where focus lands after navigation.

Other pitfalls

Are shared element transitions expensive?
They can be if the shared snapshot is huge or if you animate filters, shadows, width, height, or layout-affecting properties. Keep the named element bounded, animate transform and opacity on the snapshots, and avoid capturing an entire page section when a thumbnail or title would communicate the relationship.

Notes

Overview

Shared-element transitions morph a thumbnail into a larger detail page, keeping a visual through-line across what would otherwise feel like an abrupt navigation. The View Transitions API turns this into a one-line browser primitive: tag two elements with the same view-transition-name on the old and new pages, and the browser handles the cross-fade, the size morph, and the position translate as part of a single transaction.

When to use it

Reach for shared-element transitions when the user is moving between two views of the same content at different fidelities — a gallery card to a detail page, a search result to its full record, a feed item to its conversation thread. The morph is the visual proof that “this is the same thing, just bigger.” Skip it for navigations between unrelated views (home → settings); a regular page transition reads more honestly there.

How it works

The API snapshots the old DOM, paints the new DOM, then interpolates between the two snapshots over a configurable duration. Elements with a matching view-transition-name are paired and animated together; everything else cross-fades. You can hook into the animation with ::view-transition-old(name) and ::view-transition-new(name) pseudo-elements to customize the timing or curve. The same-document FLIP fallback in this pattern kicks in for engines without API support, measuring the from/to rectangles and faking the morph via transform: translate + scale.

Production gotchas

Two elements with the same name on the page simultaneously will throw — make sure only one is mounted at a time, or scope the name to instance IDs. The fallback FLIP measurement happens on layout, so applying it to elements inside flex containers with min-width: 0 can produce zero-width “from” frames. Pause any other animations on the morphing elements during the transition window or they will fight the interpolation.

Accessibility

The browser respects prefers-reduced-motion: reduce automatically — the morph skips and only the cross-fade plays. For screen reader users the DOM transition is functionally instant. Keep your focus management on the new page; the API does not move focus for you.

References

Implementation depth

Shared element motion should prove identity between two views. The same thumbnail, title, or metadata block needs a stable view-transition-name on both sides, otherwise the browser falls back to a generic crossfade and the user loses the visual through-line.

Treat the FLIP fallback as a compatibility layer, not a second design. Measure from and to rectangles once, animate transform and scale only, then let the destination DOM own layout again so focus, headings, and landmarks remain normal after navigation.