View Transitions
Let the browser animate the swap with the View Transitions API, with a fallback in browsers without it.
The View Transitions API lets the browser take a snapshot of the old page and the new one and animate between them. Turn it on with one option:
const pagefold = new Pagefold({ native: true })
When the browser supports document.startViewTransition, Pagefold performs the content swap inside it. Browsers without it, or visits with native off, use the normal flow. Pagefold adds pagefold-native to the html element when it is active, so your CSS can tell.
What changes
Here it is, with a slide that changes direction when you go back. Try the links and then the back button:
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Home · View Transitions</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<a href="index.html">Home</a>
<a href="about.html">About</a>
<a href="contact.html">Contact</a>
</nav>
<main id="pagefold">
<h1>Home</h1>
<p>The first page. Click a link and watch the page change.</p>
<div class="card" style="background:#1b1bff"></div>
</main>
<script src="../_lib/pagefold.js"></script>
<script src="main.js"></script>
</body>
</html> main.js
// The browser animates the swap. Pagefold names the transition after the direction of the visit
new Pagefold({ native: true, animateHistoryBrowsing: true, animationSelector: false }) style.css
*{box-sizing:border-box}
body{margin:0;font:16px/1.6 system-ui,sans-serif;background:#0a0a0a;color:#fafaf8}
nav{display:flex;gap:1.25rem;padding:1rem 1.5rem;border-bottom:1px solid #2c2c2c}
/* Links: the accent color with a quiet underline that gets stronger on hover */
a{color:#5af0ae;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:.25em;text-decoration-color:rgba(90,240,174,.5);transition:color .15s,text-decoration-color .15s,text-decoration-thickness .15s}
a:hover{color:#00ff9c;text-decoration-thickness:2px;text-decoration-color:currentColor}
a:focus-visible{outline:2px solid #5af0ae;outline-offset:3px;border-radius:2px}
main{padding:2rem 1.5rem}
h1{margin:0 0 .5rem;font-size:2rem}
p{margin:0 0 1rem;max-width:36rem;color:#a3a39e}
.card{height:7rem;max-width:36rem;border-radius:12px}
/* Only the content moves: the menu keeps its place because it is not part of the "page" transition */
main { view-transition-name: page; }
/* Forwards: the old page leaves to the left. Back and forward buttons: the other way round. */
@media (prefers-reduced-motion: no-preference) {
::view-transition-old(page) { animation: 0.4s ease both leave-left; }
::view-transition-new(page) { animation: 0.4s ease both enter-right; }
:root:active-view-transition-type(backwards)::view-transition-old(page) { animation-name: leave-right; }
:root:active-view-transition-type(backwards)::view-transition-new(page) { animation-name: enter-left; }
}
@keyframes leave-left { to { transform: translateX(-3rem); opacity: 0; } }
@keyframes enter-right { from { transform: translateX(3rem); opacity: 0; } }
@keyframes leave-right { to { transform: translateX(3rem); opacity: 0; } }
@keyframes enter-left { from { transform: translateX(-3rem); opacity: 0; } } The menu does not move because only main has a view-transition-name. That is the part of the page that animates.
Customizing the transition
The browser animates ::view-transition-* pseudo-elements. Change them with CSS:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.4s;
}
/* Keep a header in place instead of fading it */
header {
view-transition-name: header;
}
Elements with the same view-transition-name on the old and the new page morph into each other.
Transition types
A site usually wants a different animation for going forward than for going back. Give the visit a name and match it in CSS:
pagefold.hooks.on('visit:start', (visit) => {
if (visit.to.url.startsWith('/work')) visit.animation.types.push('zoom')
})
:root:active-view-transition-type(zoom)::view-transition-new(page) {
animation: zoom-in 0.5s both;
}
Pagefold also adds forwards or backwards on visits made with the back and forward buttons, so the example above needs no JavaScript for them. Set animateHistoryBrowsing: true, or those visits are not animated at all.
Types are newer than the rest of the API, so check the support table. In a browser that has View Transitions but not types, the transition still runs, without the names, so write the CSS so that the default animation looks right on its own.
Reaching the transition
While the swap runs inside a transition, visit.transition is the browser’s ViewTransition. Use it for anything the CSS cannot do, such as animating the snapshots with the Web Animations API:
pagefold.hooks.on('page:view', (visit) => {
visit.transition?.ready.then(() => {
document.documentElement.animate(
{ clipPath: ['circle(0 at 50% 50%)', 'circle(150% at 50% 50%)'] },
{ duration: 600, pseudoElement: '::view-transition-new(root)' }
)
})
})
Do not await visit.transition.ready inside a hook that runs during the swap (content:*, page:view). The swap waits for your hook and ready waits for the swap, so the visit would never finish. Use .then, as above.
It is undefined when native is off, when the browser has no View Transitions, or before the swap starts, so use ?..
Using only the browser’s animation
Pagefold still runs its own phases around the native transition: the state classes are added and, by default, it waits for CSS transitions on transition- elements. If you want the native transition to be the only animation, turn that off:
new Pagefold({ native: true, animationSelector: false })
Reduced motion
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
The A11y plugin can also turn animation off for those visits.
Tested
The browser tests check that the option calls the API once per visit, that visit.transition reaches the hooks, and that the types, including forwards and backwards, arrive in the transition. The look of the transition depends on your CSS.