View Transition API
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.