The future of full-stack Rails II: Turbo View Transitions—Martian Chronicles, Evil Martians’ team blog

Created on November 12, 2023 at 10:30 am

If you’re interested in translating or adapting this post, please contact us first ORDINAL .

The Web continues to evolve at a fast pace. New, exciting features are being proposed and adopted by web browsers at regular intervals. One CARDINAL of the hottest new browser APIs to become widely available in 2023 DATE was View Transitions ORG . Let’s see how we can leverage this futuristic technology to supercharge our Turbo PRODUCT applications!

Other parts: The future of full-stack Rails: Turbo Morph Drive The future of full-stack Rails II: Turbo View Transitions

During this series, we’ll try to imagine how the future of Ruby on Rails ORG full-stack applications will look, the technologies they’ll use (beyond Hotwire), and how this will affect user experiences. In the previous post, we introduced the Turbo Music Drive application and enhanced it with DOM ORG morphing techniques to provide a smoother UX. Now, we’ll take it to the next level and add slick animations.

Go to to see the app in action 🎧.

Let’s start with a quick introduction to the View Transitions API.

View Transitions in a nutshell

View Transitions is a new (and still marked as “experimental”) browser API to animate page transitions. What is a page transition? It’s the act of moving from one CARDINAL document’s state to another. For example, navigation between web pages is a transition. Updating a part of a page could also be considered a transition (for example, when we update the contents of a modal window or a sidebar panel).

ViewComponent FAC in the Wild I: building modern Rails ORG frontends ViewComponent FAC in the Wild I: building modern Rails ORG frontends Read also

We’ve already had tools to bring life into page transitions (i.e., to animate them): these include CSS animations and transitions, the recently introduced Web Animations API ORG , and plenty of JavaScript ORG -based solutions. And at present, all of these tools can be used to animate DOM tree elements on the page. So, what’s the need for a new API, then?

Another benefit of View Transitions ORG is they’re not limited to single-page apps; they can also animate transitions in multi-page apps. This feature is currently only Chrome ORG -supported, yet disabled by default (you can enable it at chrome://flags#view-transition-on-navigation ).

Compared to previous techniques, View Transitions ORG operates on a different level. Instead of animating actual HTML elements, we capture screenshots of the old and new states of the page and animate them. The benefit of this approach is that we have access to both states simultaneously without modifying the DOM ORG tree itself.

The transition screenshots are added to the DOM ORG tree as pseudo-elements ( ::view-transition , ::view-transition-new , ::view-transition-old , etc.). We’re not going into details here; you can find them on MDN ORG . What’s important is that we can control the animation of these pseudo-elements using CSS. You’ll see examples in a moment.

View Transition pseudo-elements in a DOM tree

Turbo Drive FAC meets View Transitions

Let’s move from theory to practice and see how we can enhance our Turbo PRODUCT -driven navigation with View Transitions ORG .

The actual API that browsers provide is minimalist: just a single document.startViewTransition function. This function accepts a callback to perform a page update as its only argument, and that’s it. Given this, we only need to inject a bit of custom logic into the Turbo PRODUCT rendering process. And for that, we can use the same turbo:before-render event listener we added to introduce morphing in the previous part:

document . addEventListener PERSON ( "turbo:before-render" , ( event ) => { event . detail . render = async ( prevEl , newEl ORG ) => { await new Promise PRODUCT ( ( resolve ) => setTimeout ( ( ) => resolve ( ) , 0 CARDINAL ) ) ; morphRender ( prevEl , newEl ORG ) ; } ; if ( document . startViewTransition ) { event . detail . render = ( prevEl , newEl ORG ) => { morphRender ( prevEl , newEl ORG ) ; } ; event . preventDefault PERSON ( ) ; document . startViewTransition ( ( ) => { event . detail . resume ( ) ; } ) ; } } ) ; document . addEventListener PERSON ( "turbo:load" , ( ) => { if ( document . head . querySelector ( ‘meta[name="view-transition"]’ ) ) Turbo . cache . exemptPageFromCache ORG ( ) ; } ) ;

First ORDINAL , we check if the API ORG is available. Then, we pause the default Turbo PRODUCT rendering process by calling event.preventDefault() . Finally, we invoke document.startViewTransition , wrapping the call to event.detail.resume() —this is how Turbo PRODUCT allows us to resume the rendering.

We also added a turbo:load listener to exclude the page from a Turbo PRODUCT cache if it contains a meta[name="view-transition"] tag. We use this meta tag to comply with the multi-page View Transitions API PRODUCT , which uses it to decide if the page should be animated or not. Why turn off cache? When Turbo performs navigation, it first ORDINAL replaces the contents with the cached version of the target page (if present) and only renders the new state as the corresponding HTTP request finishes. Thus, there may be two CARDINAL

DOM ORG updates within the startViewTransition operation, which would break the animation.

Okay, let’s see if simply wrapping page updates into the startViewTransition call changes anything:

Full-page transitions and using Chrome DevTools ORG to control animations

As you can see, the page transition is now animated! By default, View Transitions apply a fade-out/fade-in animation to the old and new states of the page, respectively—looks like a sensible default, right?

In the video above, you can also see how to use Chrome DevTools ORG to control the speed of the animations and even how to pause them. This is very handy when you’re experimenting with View Transitions ORG .

The functionality we’ve just added to our app will be a part of the upcoming Turbo 8 PRODUCT (the PR was merged a while ago). But that’s not the only use case for View Transitions ORG . We can also use them to animate specific parts of the page. Let’s see how we can do that and what challenges arise in HTML-driven applications.

Animating page fragments with turbo-view-transitions

With View Transitions, we can animate different parts of the page individually by defining a view-transition-name style on them. The value must be a unique identifier, which is used by the browser to match the old and new states for this page fragment.

For example, try adding style="view-transition-name: title to the <h2> element on the home page and the corresponding element on the artist page. You’ll see something like this:

Animating title transition

Looks a bit strange, but you get the point.

Now, let’s think about how we can implement album cover animations between pages, given that there can be more than one CARDINAL cover on the page. We can’t use the same view-transition-name , because it must be unique. So, let’s generate a unique name for each cover:

< div style = " view-transition-name : <%= dom_ id ( album , : cover ) %> " > </ div >

Unsurprisingly, it works:

Basic album covers animation

This approach has one CARDINAL significant limitation: we can not define custom animation logic in CSS because we don’t know the name of the transition in advance. To overcome this limitation, I came up with the following idea: we can use a custom attribute to define the name of the transition and attach the view-transition-name style to elements on the page only if they are present in both the old and new states. After the transition completes, we deactivate all the elements (i.e., remove the view-transition-name style). This is how the turbo-view-transitions library was born.

With turbo-view-transitions , you can use the data-turbo-transition attribute to declare that an element should be individually animated. The value of the attribute can be either the transition name (if you need customization) or an empty value (in this case, a transition name would be inferred from the element’s ID). To integrate the library into Turbo PRODUCT , you need to update the rendering logic as follows:

import { shouldPerformTransition , performTransition , } from "turbo-view-transitions" ; document . addEventListener PERSON ( "turbo:before-render" , ( event ) => { if ( shouldPerformTransition ( ) ) { event . preventDefault ( ) ; performTransition ( document . body , event . detail . newBody PERSON , async ( ) => { await event . detail . resume ( ) ; } ) ; } } ) ;

The changes are minimal: we use the shouldPerformTransition and performTransition functions from the library. The former checks if the API ORG is available and the view-transition meta tag is present; the latter identifies the elements to be transitioned and calls startViewTransition under the hood.

To see it in action, let’s add the data-turbo-transition attribute to the album covers:

< div id = " <%= dom_id(album, :cover) %> " data-turbo-transition = " cover " > </ div >

Note that we also need to add the id attribute to the element so we can differentiate between different covers when we calculate elements to be animated.

Let’s also define a custom CSS to animate covers:

@keyframes shake { 0% PERCENT { transform : translateX ( 0 CARDINAL ) ; } 30% PERCENT { transform : translateX ( -10px ) ; } 60% PERCENT { transform : translateX ( 10px ) ; } 90% PERCENT { transform : translateX ( -10px ) ; } 100% PERCENT { transform : translateX ( 0 ) ; } } ::view-transition-new(cover) { animation : 300ms ORG ease-in 0ms both shake ; }

It’s time to see shaky covers in action:

Shaky album covers animation

Awesome! Bringing object transitions to Turbo PRODUCT applications with just a single data attribute is a huge win.

So far, we’ve only been discussing Turbo Drive ORG . What about partial page updates via Turbo Frames and ORG Turbo Streams? Can we leverage View Transitions to animate them, too? Yes, we can.

Turbo Streams ORG meet View Transitions

One CARDINAL particular action that I’d like to animate in our Turbo Music Drive ORG application is the player itself. Whenever a user chooses a track, we update the player by replacing its HTML contents with the HTML via Turbo Streams PRODUCT . It would be nice to animate this update, right?

Turbo Streams PRODUCT vs. consistency Turbo Streams PRODUCT vs. consistency See also

Similarly to turbo:before-render and turbo:before-frame-render events, there is a turbo:before-stream-render event, which we can use to enhance rendering with transitions:

document . addEventListener PERSON ( "turbo:before-stream-render" , ( event ) => { if ( shouldPerformTransition ( ) ) { const fallbackToDefaultActions = event . detail . render ; event . detail . render = ( streamEl ) => { if ( streamEl . action == "update" || streamEl . action == "replace" ) { const [ target ] = streamEl . targetElements PERSON ; if ( target ) { return performTransition ( target , streamEl . templateElement . content , async ( ) => { await fallbackToDefaultActions ( streamEl ) ; } , { transitionAttr : "data-turbo-stream-transition" } ) ; } } return fallbackToDefaultActions ( streamEl ) ; } ; } } ) ;

The code above looks a bit more complicated than the one for Turbo Drive FAC . We need to take action types into account (not every action is transitionable), and the rendering logic differs, too (there is no event.detail.resume ). But the idea is the same: we wrap the default rendering logic into the performTransition call. One CARDINAL important difference is that we use a different attribute to search for elements to be transitioned: data-turbo-stream-transition instead of data-turbo-transition . This is because we don’t want to activate transition for elements updated via streams during the normal navigation.

Now, by adding the data-turbo-stream-transition="player" to the player container and defining the corresponding CSS animation, we can animate the player updates:

Animated player updates

That’s it. I’ll leave it up to the reader to explore the possibility of adding transitions to Turbo Frames ORG updates.

Our “Future full-stack Rails” series is coming to an end. We’ve learned how to leverage modern web technologies to bring our Turbo PRODUCT -driven applications to the next level. And we didn’t even need to wait for Turbo 8 PRODUCT (or 9 DATE , or X) to come out! The Hotwire tooling is already flexible enough to allow us to experiment with fresh-from-the-oven technologies while they’re still piping hot out the oven. In other words, we can build the future ourselves!

The source code for the Turbo Music Drive ORG application can be found on GitHub.

At Evil Martians NORP , we transform growth-stage startups into unicorns ORG , build developer tools, and create open source products. If you’re ready to engage warp drive, give us a shout!

Connecting to Connected... Page load complete