Swup Page Transition — Free Hooks-Driven Fade Transition Snippet

Swup Page Transition · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real Swup hooks API
visit:start/content:replace/visit:end match production.
CSS-owned timing
transition-duration on a class, not a JS tween.
In-page adaptation
Two containers stand in for two fetched pages.
Reflow-safe re-trigger
Forces layout so the enter class actually animates.
Active link state
Nav highlights the current container's link.
Console-visible lifecycle
Logs each hook as it fires for learning.
No extra animation library
Fade runs on plain CSS transitions.
Sandbox-safe
Runs standalone without a server or router.

About this UI Snippet

Swup Page Transition — Hooks and CSS-Class Timing on In-Page Containers

Screenshot of the Swup Page Transition snippet rendered live

Swup animates real page-to-page navigation: it intercepts internal link clicks, fetches the destination page's HTML, and swaps its containers into the DOM while firing lifecycle hooks — visit:start, content:replace, visit:end — around the swap. It leans on CSS to own the actual animation: Swup toggles an is-transitioning class and waits for the CSS transition-duration on that class before continuing, rather than animating with JavaScript directly. This snippet is a single HTML document with nothing to fetch, so it adapts that exact hooks-and-class pattern onto two containers already present in the page.

What's real Swup and what's adapted

new Swup({ containers: [...] }), the swup.hooks.on(...) subscriptions, and the transition-fade / is-transitioning class convention are exactly how Swup is used in production — that class-toggle timing model is Swup's actual design, not a simplification. What's adapted is the trigger: since there's no second URL to fetch, a click handler calls swup.hooks.call(...) at the same points Swup's own router would, and toggles hidden/is-transitioning on the two sibling containers instead of Swup replacing fetched content. Drop this onto a real multi-page site and you'd delete the manual toggling — Swup's router calls the hooks and swaps content itself.

CSS owns the timing, not JavaScript

Unlike a JS-driven tween, Swup's convention is to add a class and let the browser's own transition property determine how long the animation takes; Swup (and this adaptation) just waits that same duration before proceeding. That's why .transition-fade defines the opacity/transform transition and .is-transitioning defines the "leaving" state entirely in CSS — change the transition timing function or duration in one place and the whole visit lifecycle respects it automatically.

The hook sequence

visit:start fires as a visit begins (here, on click); content:replace fires the moment the new content is swapped in (here, when the next container is un-hidden); visit:end fires once the entering transition finishes. Subscribing via swup.hooks.on is how you'd hang a loading bar, analytics ping, or scroll-reset off any point in a real Swup-powered navigation.

Where this fits

For the same navigation-transition problem solved with a different library and a transitions-array hook shape instead of an event emitter, see Barba.js page transition. For scroll-triggered rather than navigation-triggered motion, see scroll reveal grid.

Customizing it

Add more containers, change .is-transitioning's transform for a different exit direction, or hang extra behavior off content:replace (like resetting scroll position, which real Swup does by default). In production, Swup's containers option targets CSS selectors present on both the current and fetched page, so naming them consistently across templates is what makes the swap work at all.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to dig through Swup's source to see which parts of this file are real and which are the sandbox workaround. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to point out exactly where swup.hooks.on/call and the is-transitioning class toggle are genuine Swup usage versus where the click handler stands in for Swup's own fetch-based router, and what changes if this ran against two real separate page URLs instead of two in-page containers. The same assistant can help you extend it — asking how to hang a scroll-reset or analytics call off the content:replace hook, or how CSS transition-duration values interact with the wait() timeouts in the JS so they never drift out of sync. It's also useful for comparing approaches: ask it to explain the tradeoffs between Swup's hooks-emitter-plus-CSS-class model and Barba's promise-returning leave/enter functions for the same navigation-transition problem. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a page transition demo using Swup's real hooks API and CSS-class transition convention (load swup.umd.js from a CDN), adapted to run inside a single HTML document with no server-side routing.

Requirements:
- A small nav with two links and two sibling container elements, each sharing a CSS class that defines an opacity/transform transition plus a second "is-transitioning" class representing the leaving state — the actual animation timing must live in CSS transition-duration, not a JavaScript tween, matching Swup's real design.
- Instantiate Swup for real (new Swup({ containers: [...] })) and subscribe to its hooks event emitter for at least visit:start, content:replace, and visit:end, logging each one so the lifecycle is visible.
- Since there is no second page to fetch in a single-document sandbox, implement navigation yourself: intercept clicks on the nav links, and manually call swup.hooks.call() for the same hook names at the same points in the sequence a real Swup visit would fire them, while toggling which container is visible using the hidden attribute and the is-transitioning class — wait for the CSS transition's actual duration (not a mismatched guess) before proceeding to the next step each time.
- After un-hiding the entering container, force a layout reflow before removing its is-transitioning class so the browser actually animates the enter state instead of skipping straight to the resting state.
- Update the active nav link's styling to reflect whichever container is currently showing.
- In code comments, clearly mark which parts are genuine Swup API usage (the hooks emitter, the class-toggle convention) versus the click-interception shim that exists only because this is a single-page sandbox without real page-to-page navigation.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Add the Swup CDNInclude swup.umd.js from the CDN panel.
  2. 2
    Paste HTML, CSS, and JSA nav and two containers render, one hidden.
  3. 3
    Open the console (optional)See visit:start / content:replace / visit:end log.
  4. 4
    Click "Entry Two"The current container fades out via is-transitioning.
  5. 5
    Watch the swapThe next container fades in on the same class toggle.
  6. 6
    Adapt for productionRemove the click shim; let Swup's router own navigation.

Real-world uses

Common Use Cases

Multi-page site transitions
Prototype hook wiring before a real fetch pipeline.
Blogs and journals
Fade between entries like this demo's layout.
Agency sites
Pair with a parallax hero landing page.
Comparing transition libraries
Learning Swup
Read real hook and class-toggle code, no build step.
Design reviews
Demo a transition concept before backend wiring.

Got questions?

Frequently Asked Questions

Swup is genuinely instantiated with new Swup({ containers: [...] }) and its real hooks event emitter is used to subscribe to and fire visit:start, content:replace, and visit:end. What's adapted is only the trigger: since this sandbox has no second URL to fetch, a click handler calls those same hooks and toggles which container is visible, instead of Swup's own fetch-based router doing it — the hook names, timing, and CSS class convention are unchanged from production usage.

You would remove the manual hidden-toggling and the direct swup.hooks.call() invocations. Swup's own router would intercept clicks on internal links, fetch the destination page, replace the content inside the matching container selector, and fire the lifecycle hooks itself around that swap — your CSS transition-fade / is-transitioning classes would carry over completely unchanged, since that part of the pattern was never adapted.

That's Swup's actual design: it adds an is-transitioning (or similarly named) class and then waits for the CSS transition-duration defined on that class before proceeding to the next step, rather than running its own tween engine. This keeps the animation authored entirely in CSS — change the transition property in one place and the whole page's visit timing follows it, no JS animation values to keep in sync.

In production Swup, content:replace fires at the exact moment the fetched page's HTML has been swapped into the container, after the leaving transition finishes and before the entering transition starts. In this adaptation, it fires when the next container is un-hidden and about to animate in — the same position in the sequence, just triggered manually instead of by Swup's own DOM-replacement step.

Both solve the same problem — animated transitions between page loads — with different APIs: Swup exposes a hooks event emitter (swup.hooks.on/call) and lets CSS transition-duration drive timing via class toggles, while Barba.js uses a transitions array of leave/enter functions that each return a promise it awaits. See the companion Barba.js page transition snippet for the same in-page adaptation built on that API instead.