New releases

Hotwire Club tooling is now open-source

Explore the agentic skills pack and the MCP server for building assistant workflows.

Turbo Drive - Shared Element View Transitions

Make a gallery thumbnail morph into a full-size hero image across page navigations using the View Transitions API and Turbo Drive.

Turbo Drive - Shared Element View Transitions

Premise

This is the third installment in our View Transitions series. In Challenge 35 we built swiper-like page transitions with Turbo Drive, and in Challenge 38 we animated list appends with Turbo Streams. What we haven’t covered yet is the shared element transition, the one you know from native apps where a thumbnail in a grid smoothly morphs into the hero image on the detail page.

The View Transitions API makes this surprisingly straightforward. When two elements on different pages share the same view-transition-name, the browser automatically interpolates between their position, size, and appearance during a navigation. You don’t need a JavaScript animation library or manual coordinate math - you assign a name, and the browser handles the rest.

There’s one important constraint to keep in mind, though: that name must be unique per page. If two elements share the same view-transition-name at the same time, the entire transition aborts silently. In a gallery grid with dozens of thumbnails, that’s something you’ll need to think about.

Starting Point

We have a simple photo gallery. The index page shows a grid of cards, each with a thumbnail image and a title. Clicking a card navigates to a detail page that shows the same image enlarged as a hero, plus a body of text.

View Transitions are already enabled via the meta tag:

https://stackblitz.com/edit/turbo-drive-shared-element-transitions?file=index.html%3AL6

Right now, navigating between the index and a detail page triggers the default cross-fade, because no view-transition-name is set on any element yet. The stylesheet already contains some basic transition configuration, but it’s waiting for the names to be wired up.

Challenge

Make the clicked card’s image smoothly morph into the hero image on the detail page, and back again when the user navigates with the browser back button.

Two things need to happen:

  1. Assign a unique view-transition-name to each card image on the index page. Each image needs its own name, because names must be unique per page. If two images share the same name, the transition breaks silently. Hint: inline styles are the cleanest way to bind a dynamic name to a DOM node.

  2. Give the detail page’s hero image the same name as the card it came from. When the browser sees the same name disappearing on the old page and appearing on the new one, it creates the morph animation automatically.

Here’s what the result looks like - the thumbnail lifts out of the grid, scales up, and settles into the hero position on the detail page. Navigating back reverses the animation.

Gallery grid where clicking a thumbnail morphs it into the hero image on the detail page, and browser back reverses the animation

Reminder: the Node server simulates a Rails backend. If you make changes to index.js, restart with npm start.

Teaser

What happens when the user navigates backward? The morph plays in reverse by default, which already looks good. But you can go further: Turbo sets a data-turbo-visit-direction attribute on the <html> element during rendering. Try using it to apply a different easing curve or duration for the reverse transition, so that returning to the grid feels distinct from opening a detail page.

Caveat

The uniqueness constraint on view-transition-name is strict. If the same name appears on more than one element at any point during the transition, the browser aborts the entire animation and falls back to a hard cut. This is easy to trip over in a gallery - if your index page renders 20 thumbnails and you give all of them view-transition-name: hero, nothing will animate. Each card image needs its own scoped name.

In a Rails context, this maps naturally to the model ID: style="view-transition-name: photo-<%= photo.id %>". On the detail page only one hero image exists, so there’s no collision risk - just make sure the name matches the one from the index.

One more thing to watch for is Turbo’s page cache. If a cached version of the index page has view-transition-name set on elements that are also present in the incoming page, you might get duplicate names during the snapshot restoration. Test with caching enabled to make sure your transitions survive the round trip.

This is The Hotwire Club

50 hands-on challenges with detailed solutions, published biweekly since 2023. Subscribe to access all solutions and join the Discord community.

Subscribe on Patreon

More from

Turbo Drive - Swiper-like View Transitions
19 November 2024

Use the View Transitions API to drive powerful native-like animations

Turbo Drive - Use ULIDs for Optimistic UI
13 August 2024

Render deterministic optimistic UI elements using client-side ULIDs

Optimistic UI with Turbo 8 Morphs
26 March 2024

Provide Optimistic UI updates using inline Turbo Stream Actions, and reconcile using Turbo 8 Morphs

Cookies
essential