Transition
By default, Swup uses CSS transitions for animations, but there is a plugin that allows us to override this behavior with custom JavaScript-based animations. The SwupJsPlugin accepts an array of animation objects that define the transition behavior between pages.
The default setup includes two key functions: Out
, which executes before the content is replaced, and In
, which runs after the content is loaded. Here’s a simple example:
{ animations: [ { from: '(.*)', // matches any route to: '(.*)', // matches any route out: done => done(), // proceeds immediately in: done => done() // proceeds immediately } ]}
Our Custom GSAP Transition
In our case we take advantage and we also add on top of it.
-
Preloading Assets / Lottie / Videos : We check if images have already been preloaded. If not, we use a helper function to ensure they are loaded before the page transitions, which prevents cumulative layout shifts (CLS) and ensures smoother visual transitions.
-
Scroll Handling: We implemented a check to ensure that if the page is scrolled, it scrolls to the top before proceeding with the next animation. This prevents animations from running while the page is scrolling, which could cause visual issues.
-
In and Out Animations: The In and Out classes each return a GSAP timeline. The In animation runs after the new content is loaded, while the Out animation runs before content is replaced.
-
Additional Buffer: The u_take_your_time(150) function introduces a small delay after the page scrolls to the top, ensuring the transition is smooth and the new page has enough time to fully load.
This custom setup ensures that transitions are seamless, all assets are loaded properly, and the user experience remains fluid throughout the navigation process.
import gsap from "gsap";import In from "./In";import Out from "./Out";import { smoothScrollToTop } from "./utilities";
export const createTransitionOptions = (payload) => { const { boostify, forceScroll } = payload;
return [ { from: "(.*)", // Matches any route to: "(.*)", // Matches any route
// 'In' transition – executed after content is replaced in: async (next, infos) => { // Preload images if not already loaded var images = document.querySelector("img"); if(images){ if (!window["lib"]["preloadImages"]) { const { preloadImages } = await import("@terrahq/helpers/preloadImages"); window["lib"]["preloadImages"] = preloadImages; } await window["lib"]["preloadImages"]("img"); }
// Preload Loties if not already loaded const lottieElements = document.querySelectorAll(".js--lottie-element"); if(lottieElements.length){ if (!window["lib"]["preloadLotties"]) { const { preloadLotties } = await import("@terrahq/helpers/preloadLotties"); window["lib"]["preloadLotties"] = preloadLotties; } await window["lib"]["preloadLotties"]({ selector: lottieElements, }); }
// Create GSAP timeline for 'In' transition const tl = gsap.timeline({ onComplete: next, // Proceed to the next step once the timeline is complete });
// Add 'In' animation and other custom animations to the timeline tl.add(new In()).add("finishTransition"); },
// 'Out' transition – executed before content is replaced out: (next, infos) => { // Create GSAP timeline for 'Out' transition const tl = gsap.timeline({ onComplete: async () => { // Scroll to top before proceeding to the next page, if needed if (forceScroll && window.scrollY !== 0) { await smoothScrollToTop(); await u_take_your_time(150); // Introduce a delay to ensure smooth transition } next(); // Proceed to the next step }, });
// Add 'Out' animation to the timeline tl.add(new Out()); }, }, ];};