Pagefold

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:

Without Overlap
old page outnew page in
Overlap: old
old page out
Overlap: new
new page in

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.

/examples/overlap/ Open in a new tab Source on GitHub
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

ClassOn
is-next-containerthe incoming container, briefly, so it can transition into place
is-previous-containerthe outgoing container
is-kept-containerold containers kept by keep
is-removing-containerold 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 set animationSelector to 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: absolute or a shared grid cell.
  • An old container stays after the visit. keep says how many previous containers to hold on to. Leave it at 0 to remove them all.
  • It animates when I do not want it to. Set visit.animation.overlap = false for 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.