CodePen
CodePen Challenge: Multi-Page View Transition CodePen Team Public
Sign Up Log In
index.html Read Only
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>Field Notes — Collection</title> <link rel="stylesheet" href="style.css"></head><body> <main class="grid"> <a class="card" href="article.html?shot=ridgeline" data-shot="ridgeline"> <img class="card__img" src="ridgeline.avif" alt=""> <h2 class="card__title">Ridgeline at dusk</h2> </a> <!-- five more cards, same shape --> </main> <script src="script.js"></script></body>
style.css Read Only
/* 1 — opt this document in to cross-document transitions */@view-transition { navigation: auto;} /* 2 — one curve for every pair that crosses the boundary */::view-transition-group(*) { animation-duration: 340ms; animation-timing-function: cubic-bezier(.2, .8, .2, 1);} /* 3 — the detail page has exactly one hero, so it can be named right here. The grid has six cards, so the clicked one is named in JS — a name must be unique per page. */.detail__img { view-transition-name: shot;}
Preview URL: 019f677a.codepen.dev/ Build Success

Field Notes

Landscape prints, small runs

Ridgeline at dusk Salt flat, noon Estuary, low tide Pine break Scree field Headland, fog

Click a card → the thumbnail flies into the detail page.

script.js Read Only
/* A view-transition-name has to be unique in the document, so the grid names only the card being travelled to — or travelled back from — and nothing else. */const tag = (shot, name) => { const card = document.querySelector(`[data-shot="${shot}"]`); if (!card) return; card.querySelector(".card__img").style.viewTransitionName = name; card.querySelector(".card__title").style.viewTransitionName = name === "none" ? "none" : "shot-title";}; /* Leaving the grid: tag the card the visitor clicked. */addEventListener("pageswap", (event) => { if (!event.viewTransition) return; const url = new URL(event.activation.entry.url); tag(url.searchParams.get("shot"), "shot");}); /* Arriving back on the grid: tag the same card before the first frame is painted, then hand the name back. */addEventListener("pagereveal", (event) => { if (!event.viewTransition) return; const shot = new URL(navigation.activation.from.url) .searchParams.get("shot"); tag(shot, "shot"); event.viewTransition.finished.then(() => tag(shot, "none"));});
article.html Read Only
<body> <a class="back" href="index.html">← Collection</a> <article class="detail"> <img class="detail__img" src="ridgeline.avif" alt=""> <h1 class="detail__title">Ridgeline at dusk</h1> <p>Archival pigment print, 50 × 70 cm. Edition of 25.</p> </article> <script src="script.js"></script></body>
style.css continued
/* 4 — let the title crossfade faster than the image moves */.detail__title { view-transition-name: shot-title;} ::view-transition-old(shot-title),::view-transition-new(shot-title) { animation-duration: 160ms;} /* 5 — never animate for people who asked us not to */@media (prefers-reduced-motion: reduce) { @view-transition { navigation: none; }}
How this works

Two documents, one animation

A same-document view transition wraps a DOM change in document.startViewTransition(). Across two separate HTML files there is no shared JavaScript moment to wrap, so the browser does the wrapping for you: both pages opt in with @view-transition { navigation: auto }, and the outgoing page is held on screen while the incoming one is prepared.

Naming the pair, not the page

The browser only animates elements it can match. Give the grid thumbnail and the detail hero the same view-transition-name and it treats them as one thing that moved — position, size and border radius are interpolated for free. Everything else falls back to the default root crossfade.

Why the name is set in JavaScript

A name has to be unique within a document. Six cards cannot all be shot. So the detail page — which has exactly one hero — claims the name in CSS, and the grid page claims it at the last possible moment, in the pageswap handler, for the single card being navigated to. On the way back, pagereveal does the same job before the first frame is painted, then releases the name once viewTransition.finished settles.

Tuning the motion

Every matched pair becomes a pseudo-element tree you can style like anything else. ::view-transition-group(*) sets one duration and easing for the whole navigation; targeting a single name lets the title crossfade quicker than the image travels, which is what stops the effect feeling syrupy.

Falling back

In a browser without cross-document transitions, every line above is inert and the navigation is an ordinary page load — nothing to polyfill, nothing to feature-detect. The reduced-motion block turns it off for anyone who has asked for less movement.

Details & Comments

Description

A two-file Pen for the challenge: a grid of prints and a detail page, wired together with cross-document view transitions. The clicked thumbnail becomes the hero image on the next page, and comes home again on the back navigation. No framework, no router — one CSS at-rule and about thirty lines of JavaScript.

Tags

  • view-transitions
  • css
  • navigation
  • mpa
  • challenge
  • 1,204
  • 38.6k
  • 0

Comments

Markdown is supported… Log in to comment

More from this Challenge

  • Shared-element list → detail
  • Directional slide on back navigation
  • Transition groups with view-transition-class