10 Feb, 2025

view transition api

rails

transitions

JS

Integrating the View Transition API in a Rails application

Standard integration of the View Transition API, as well as a description of how to implement transitions in Rails applications.

View Transition API

Docs

The View Transition API provides a mechanism for easily creating animated transitions between different website views. This includes animating between DOM states in a single-page app (SPA), and animating the navigation between documents in a multi-page app (MPA).

Check browser support in the documentation.

Default setup

Add a meta tag to the <head> of the page:

<meta content="same-origin" name="view-transition" />

After this, a standard fade effect should appear during page transitions.

In css, you can control the transitions using the pseudo-elements ::view-transition-old and ::view-transition-new.

Example:

::view-transition-old(root), ::view-transition-new(root) {
  animation: *your animation name*;
}

Here, root specifies the element to which the transition animation is applied. You can also animate individual page elements separately.

Rails integration

Thanks to Turbo, which controls the app's transitions, the standard setup does not work. As a result, the standard setup is ineffective.

Check out our note on hotwire.

In addition to standard usage, view transitions can be enabled directly via js using document.startViewTransition, by adding a listener to turbo events.

For this, we need to handle two turbo events: before-visit and before-render.

In application.js, create two variables:

// app/javascript/application.js
let current_url = window.location.href,
  loaded_url = '';

current_url - stores the URL of the current page. loaded_url - stores the URL of the loaded page to prevent duplicate triggers.

Next, add an event listener for the before-visit event:

document.addEventListener("turbo:before-visit", function() {
  current_url = window.location.href;
});

Before the transition occurs, we capture the URL of the future page.

Next, add the function:

// app/javascript/application.js
const set_transition_direction = (is_backward) => {
  const html_elem = document.querySelector('html');
  html_elem.dataset.transitionDirection = is_backward ? "left" : "right";
}

Here, we set a data attribute for the transition direction ("forward"/"backward") on the <html> element of the page (the root DOM element). This is necessary for handling the animation.

Add a handler for the second turbo event:

// app/javascript/application.js
document.addEventListener("turbo:before-render", function(event) {
  // add a fallback if the browser does not support the View Transition API
  if (!document.startViewTransition) {
    console.warn("View Transition API is not supported in this browser.");

    event.detail.resume(); // resume rendering
    return;
  }

  const new_url = event.target.baseURI;
  if (loaded_url !== new_url) {
    event.preventDefault();
    // detect the direction of the transition
    // the longer path is "forward"; the shorter one is "backward"
    const is_backward = new URL(new_url) < new URL(current_url);

    document.startViewTransition(() => {
      set_transition_direction(is_backward); // set animation direction
      loaded_url = new_url;

      event.detail.resume(); // resume rendering
    });
  }
})

The if (loaded_url !== new_url) block prevents the event from triggering again during the transition.

In summary, we got this:

// app/javascript/application.js
let current_url = window.location.href,
  loaded_url = '';

const set_transition_direction = (is_backward) => {
  const html_elem = document.querySelector('html');
  html_elem.dataset.transitionDirection = is_backward ? "left" : "right";
}

document.addEventListener("turbo:before-visit", function() {
  current_url = window.location.href;
});

document.addEventListener("turbo:before-render", function(event) {
  if (!document.startViewTransition) {
    console.warn("View Transition API is not supported in this browser.");

    event.detail.resume();
    return;
  }

  const new_url = event.target.baseURI;
  if (loaded_url !== new_url) {
    event.preventDefault();
    const is_backward = new URL(new_url) < new URL(current_url);

    document.startViewTransition(() => {
        set_transition_direction(is_backward);
        loaded_url = new_url;

        event.detail.resume();
    });
  }
})

For convenience, you can extract the functionality into a separate function and call it in application.js. For example, init_transition:

// app/javascript/application.js
import { init_transition } from 'modules/page_transitions'

init_transition()

After this, add styles to a css file (e.g., global.css):

/* Animations for View Transition API */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.3s;
  animation-timing-function: ease-in-out;
}

::view-transition-old(root) {
  animation-name: slide-out;
}

::view-transition-new(root) {
  animation-name: slide-in;
}

@keyframes slide-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

@keyframes slide-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Optional block: */
/* :root {
  set default animation variables ("forward")
}
[data-transition-direction="left"] {
  change variables for "backward" animation
} */

Simple animation case

If you need a simple animation without complex processing of the previous and next pages, remove the set_transition_direction function and the is_backward variable.

In css file, you won't be able to use an optional block.

Similar articles