Overlap
Animate the old page and the new page at the same time, with both in the DOM during the animation.
Import it from pagefold/overlap (about 1.4 KB minified and compressed, shared code included).
import PagefoldOverlap from 'pagefold/overlap'
new Pagefold({ plugins: [new PagefoldOverlap()] })
Normally the old content animates out, is replaced, and the new content animates in: one after the other. With Overlap the new container is inserted next to the old one and both animate together. When the visit ends, the old container is removed.
The same visit, with and without the plugin. Both animations run at the same time, so the visit is as long as the longest one, and both pages are visible together while it runs:
Try it. Each page has its own color so you can tell them apart. While the new page slides in, the old one is still there, sliding away, and the counter at the top says there are two pages in the document. Each one is tagged old page and new page while they cross. When the visit ends the old one is removed and the counter goes back to one.
main.js
const pagefold = new Pagefold({
animateHistoryBrowsing: true,
plugins: [new PagefoldOverlap()],
})
// How many pages are in the document? Normally one. While two pages cross, it is two.
const count = document.querySelector('#count')
const update = () => (count.textContent = document.querySelectorAll('#pagefold').length)
new MutationObserver(update).observe(document.querySelector('.stack'), { childList: true }) style.css
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;overflow:hidden;font:16px/1.6 system-ui,sans-serif;background:#0a0a0a;color:#fafaf8}
/* 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}
a:hover{color:#00ff9c;text-decoration-color:currentColor}
a:focus-visible{outline:2px solid #5af0ae;outline-offset:3px;border-radius:2px}
.bar{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;padding:.7rem 1.5rem;border-bottom:1px solid #2c2c2c}
.bar nav{display:flex;gap:1.25rem}
.status{margin:0;font:.8rem ui-monospace,Menlo,monospace;color:#a3a39e}
.status b{display:inline-block;min-width:1.2em;text-align:center;padding:.05rem .4rem;border-radius:6px;background:#1a1a1a;color:#00ff9c}
/* Both containers are in the page at the same time, so they share one cell */
.stack{display:grid;height:calc(100% - 3.1rem);overflow:hidden}
.stack>*{grid-area:1/1;position:relative}
main{padding:2rem 1.75rem}
main h1{margin:0 0 .75rem;font-size:2.4rem;line-height:1.1}
main p{max-width:26rem;margin:0 0 .75rem}
/* The slide: slow enough to see the two pages crossing */
/* The class that starts with transition- is what Pagefold waits for: without it the old page would be removed at once */
.transition-overlap{transition:transform .9s cubic-bezier(.65,0,.25,1),opacity .9s ease}
/* Overlap puts the new container before the old one, so it would be under it: bring it above */
.transition-overlap:not(.is-previous-container){z-index:2}
.transition-overlap.is-next-container{transform:translateX(100%)}
.transition-overlap.is-previous-container{transform:translateX(-35%);opacity:.55}
/* A tag on each page while they cross, so you can tell which is which */
main::after{content:'';position:absolute;bottom:1rem;left:1.75rem;padding:.2rem .6rem;border-radius:999px;font:700 .72rem ui-monospace,Menlo,monospace;letter-spacing:.04em;text-transform:uppercase;opacity:0;transition:opacity .2s}
.stack:has(.is-previous-container) main.is-previous-container::after{content:'old page';left:45%;background:#0a0a0a;color:#fafaf8;opacity:1}
.stack:has(.is-previous-container) main:not(.is-previous-container)::after{content:'new page';background:#0a0a0a;color:#00ff9c;opacity:1} index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Home · Overlap</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="bar">
<nav>
<a href="index.html">Home</a>
<a href="about.html">About</a>
<a href="contact.html">Contact</a>
</nav>
<p class="status">Pages in the document: <b id="count">1</b></p>
</header>
<div class="stack">
<main id="pagefold" class="transition-overlap" style="background:#1b1bff;color:#fafaf8">
<h1>Home</h1>
<p><strong>This is the first page.</strong></p>
<p>Click About. Both pages will be in the document at the same time.</p>
</main>
</div>
<script src="../_lib/pagefold.js"></script>
<script src="../_lib/overlap.js"></script>
<script src="main.js"></script>
</body>
</html> Classes
| Class | On |
|---|---|
is-next-container | the incoming container, briefly, so it can transition into place |
is-previous-container | the outgoing container |
is-kept-container | old containers kept by keep |
is-removing-container | old containers about to be removed |
Every container also gets a custom property, --pagefold-overlap-container, with its position: 0 for the newest.
.transition-overlap {
transition: opacity 0.4s;
}
.transition-overlap.is-next-container,
.transition-overlap.is-previous-container {
opacity: 0;
}
<main id="pagefold" class="transition-overlap">...</main>
The container needs a class that starts with transition-. Pagefold waits for the CSS transitions of the elements that match animationSelector, which by default is [class*="transition-"]. Without that class there is nothing to wait for, and the old page is removed the moment the new one is in: the two never overlap.
The previous container also gets aria-hidden="true", so screen readers do not read two pages.
Turning it off for one visit
pagefold.hooks.on('visit:start', (visit) => {
if (visit.to.url.startsWith('/checkout')) visit.animation.overlap = false
})
Hooks it adds
content:insert and content:remove fire with the list of containers, and you can replace their default behavior to control how containers enter and leave the DOM.
Common problems
- The old page disappears at once, with no overlap. Pagefold is not waiting for your transition. Give the container a class that starts with
transition-, or setanimationSelectorto match it. - The two pages stack one under the other. Both containers are in the document at the same time, so the layout has to allow it. For a crossfade, place them on top of each other with
position: absoluteor a shared grid cell. - An old container stays after the visit.
keepsays how many previous containers to hold on to. Leave it at0to remove them all. - It animates when I do not want it to. Set
visit.animation.overlap = falsefor that visit, as shown above.
Options
| Option | Type | Default | What it does |
|---|---|---|---|
containers | string[] | [] (all containers of the visit) | Containers that overlap. Others are replaced normally. |
keep | number | Record<string, number> | 0 | How many previous containers to keep after the animation, in total or per selector. |