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.