Skip to main content

SWUP Documentation

Introduction

Swup is a versatile page transition library that transforms multi-page websites into smooth, app-like experiences. Instead of letting the browser perform full page loads, swup intercepts link clicks, fetches new pages in the background, and animates the transition between old and new content.

Key Features

  • Smooth Transitions: Animate between pages using CSS, JavaScript, or native View Transitions
  • Intelligent Caching: Previously visited pages load instantly from memory
  • Browser History Support: Back/forward navigation works as expected with scroll position restoration
  • Modular Architecture: Extend functionality through a rich plugin ecosystem
  • Lightweight Core: Small footprint with optional features added via plugins

Installation

Using a Bundler (Recommended)

npm install swup
import Swup from 'swup';
const swup = new Swup();

Using a CDN

<script src="https://unpkg.com/swup@4"></script>
<script>
  const swup = new Swup();
</script>

ES Modules with Fallback

For modern browsers with a fallback for older ones:

<!-- ES Module for modern browsers -->
<script type="module">
  import Swup from 'https://unpkg.com/swup@4?module';
  const swup = new Swup();
</script>

<!-- UMD fallback for older browsers -->
<script nomodule src="https://unpkg.com/swup@4"></script>
<script nomodule>
  const swup = new Swup();
</script>

Quick Start

Swup requires two things: a content container and transition styles.

1. Mark Your Content Container

Add the #swup id and a transition-* class to your main content element:

<main id="swup" class="transition-fade">
  <h1>Welcome</h1>
  <p>Lorem ipsum dolor sit amet.</p>
</main>

2. Define Transition Styles

/* Base transition styles */
html.is-changing .transition-fade {
  transition: opacity 0.25s;
  opacity: 1;
}

/* Styles for the leaving/entering state */
html.is-animating .transition-fade {
  opacity: 0;
}

3. Initialize Swup

import Swup from 'swup';
const swup = new Swup();

Complete Example

<!DOCTYPE html>
<html>
<head>
  <title>Swup Example</title>
  <style>
    html.is-changing .transition-fade {
      transition: opacity 0.25s;
      opacity: 1;
    }
    html.is-animating .transition-fade {
      opacity: 0;
    }
  </style>
</head>
<body>
  <main id="swup" class="transition-fade">
    <h1>Welcome</h1>
    <p>Lorem ipsum dolor sit amet.</p>
  </main>
  
  <script type="module">
    import Swup from 'https://unpkg.com/swup@4?module';
    const swup = new Swup();
  </script>
</body>
</html>

Core Concepts

Content Containers

Swup replaces only designated content containers, not the entire page. By default, it looks for an element with id="swup". You can configure additional containers for elements like navigation or sidebars that need updating without animation.

const swup = new Swup({
  containers: ['#swup', '#sidebar', '#nav']
});

Note: Only elements inside <body> are supported. Each selector should match a unique element.

Animation Classes

Swup applies classes to the <html> element during transitions:

Class Description
is-changing Present throughout the entire transition
is-animating Present while content is animating (removed after content swap)
is-leaving Present during the leave animation phase
is-rendering Present during the enter animation phase
to-[name] Added when using custom animations via data-swup-animation

Browser History

Swup integrates with the browser's History API. The URL always reflects the current page, and forward/backward navigation works naturally with scroll position restoration.

Scroll Behavior

Swup mimics native browser scrolling:

  • Scroll position resets to top between pages
  • Anchor links scroll to the target element
  • History navigation restores previous scroll positions

Configuration Options

Pass options when initializing swup:

const swup = new Swup({
  // options here
});

Complete Options Reference

Option Type Default Description
animationSelector string '[class*="transition-"]' Selector for elements swup waits to finish animating
animationScope string 'html' Where animation classes are applied ('html' or 'containers')
containers array ['#swup'] Content containers to replace
cache boolean true Enable/disable page caching
hooks object {} Register hook handlers at initialization
ignoreVisit function See below Callback to ignore specific visits
linkSelector string 'a[href]' Selector for links that trigger transitions
linkToSelf string 'scroll' Behavior for links to current page ('scroll' or 'navigate')
native boolean false Enable native View Transitions API
plugins array [] Plugins to enable
resolveUrl function (url) => url Transform URLs before loading
requestHeaders object See below Custom headers for fetch requests
skipPopStateHandling function See below Control history navigation handling
timeout number 0 Fetch timeout in milliseconds (0 = disabled)
animateHistoryBrowsing boolean false Animate back/forward navigation

Default Values

const swup = new Swup({
  animateHistoryBrowsing: false,
  animationSelector: '[class*="transition-"]',
  animationScope: 'html',
  cache: true,
  containers: ['#swup'],
  hooks: {},
  ignoreVisit: (url, { el } = {}) => el?.closest('[data-no-swup]'),
  linkSelector: 'a[href]',
  linkToSelf: 'scroll',
  native: false,
  plugins: [],
  resolveUrl: (url) => url,
  requestHeaders: {
    'X-Requested-With': 'swup',
    'Accept': 'text/html, application/xhtml+xml'
  },
  skipPopStateHandling: (event) => event.state?.source !== 'swup',
  timeout: 0
});

Animations

Swup supports three animation methods: CSS transitions, JavaScript animations, and native View Transitions.

CSS Animations

The default approach. Swup waits for CSS transitions and animations on elements with transition-* classes.

html.is-changing .transition-fade {
  transition: opacity 0.25s;
  opacity: 1;
}

html.is-animating .transition-fade {
  opacity: 0;
}

Different Leave and Enter Animations

Use is-leaving and is-rendering classes for asymmetric animations:

html.is-leaving .transition-slide {
  transform: translateX(-100%);
}

html.is-rendering .transition-slide {
  transform: translateX(100%);
}

JavaScript Animations

Use hooks to manage animations with libraries like GSAP:

import gsap from 'gsap';

swup.hooks.replace('animation:out:await', async () => {
  await gsap.to('.transition-fade', { opacity: 0, duration: 0.25 });
});

swup.hooks.replace('animation:in:await', async () => {
  await gsap.fromTo('.transition-fade', 
    { opacity: 0 }, 
    { opacity: 1, duration: 0.25 }
  );
});

Native View Transitions

Enable the browser's View Transitions API for better performance:

const swup = new Swup({ native: true });
html.is-changing .transition-fade {
  view-transition-name: main;
}

::view-transition-old(main) {
  animation: fade 0.5s ease-in-out both;
}

::view-transition-new(main) {
  animation: fade 0.5s ease-in-out both reverse;
}

@keyframes fade {
  from { opacity: 1; }
  to { opacity: 0; }
}

Fallback for Unsupported Browsers

html.is-changing:not(.swup-native) .transition-fade {
  transition: 0.25s opacity;
  opacity: 1;
}

html.is-animating:not(.swup-native) .transition-fade {
  opacity: 0;
}

Disabling Animations

const swup = new Swup({ animationSelector: false });

Lifecycle & Hooks

Hooks allow you to execute custom code at specific points during page transitions.

Lifecycle Diagram

Registering Handlers

swup.hooks.on('page:view', (visit) => {
  console.log('New page loaded:', visit.to.url);
});

Handler Options

// Execute once then remove
swup.hooks.on('page:view', handler, { once: true });
// or
swup.hooks.once('page:view', handler);

// Execute before internal handler
swup.hooks.on('content:replace', handler, { before: true });
// or
swup.hooks.before('content:replace', handler);

// Control execution order (negative = earlier, positive = later)
swup.hooks.on('visit:start', handler, { priority: -100 });

Async Handlers

Handlers can return Promises to pause execution:

swup.hooks.on('visit:start', async () => {
  await someAsyncOperation();
});

Removing Handlers

const handler = () => console.log('Page loaded');
swup.hooks.on('page:view', handler);
swup.hooks.off('page:view', handler);

Available Hooks

Hook Description
visit:start Transition begins
visit:end Transition complete
visit:abort Visit cancelled (new link clicked)
page:load Page loaded (from cache or network)
page:view New content is visible
content:replace Old content replaced with new
content:scroll Scroll position reset
animation:out:start Leave animation begins
animation:out:await Waiting for leave animation
animation:out:end Leave animation complete
animation:in:start Enter animation begins
animation:in:await Waiting for enter animation
animation:in:end Enter animation complete
animation:skip Animations skipped (history navigation)
link:click Link clicked
link:self Link to current page clicked
link:anchor Anchor link clicked
link:newtab Link opens new tab
history:popstate Back/forward button pressed
fetch:request Fetch request sent
fetch:error Fetch failed
fetch:timeout Fetch timed out
cache:set Page cached
cache:clear Cache cleared
scroll:top Scrolling to top
scroll:anchor Scrolling to anchor
enable Swup initialized
disable Swup destroyed

Registering Hooks at Initialization

const swup = new Swup({
  hooks: {
    'visit:start': () => console.log('Starting'),
    'page:view': () => console.log('Loaded'),
    'content:replace.before': () => console.log('Before replace'),
    'visit:end.once': () => console.log('First visit only')
  }
});

DOM Events

Hooks are also dispatched as DOM events:

document.addEventListener('swup:page:view', ({ detail: { visit } }) => {
  console.log('Going to', visit.to.url);
});

The Visit Object

The visit object contains information about the current navigation and is available in all hook handlers.

Accessing the Visit Object

swup.hooks.on('page:view', (visit) => {
  console.log('Navigating to:', visit.to.url);
});

Structure

{
  id: 1042739,                    // Unique visit identifier
  from: {
    url: '/home',
    hash: ''
  },
  to: {
    url: '/about',
    hash: '#anchor',
    html: undefined,              // Populated after fetch
    document: undefined           // Populated after fetch
  },
  containers: ['#swup'],
  animation: {
    animate: true,
    name: 'fade'
  },
  trigger: {
    el: HTMLAnchorElement,        // undefined if via API
    event: MouseEvent             // undefined if via API
  },
  cache: {
    read: true,
    write: true
  },
  history: {
    action: 'push',
    popstate: false,
    direction: undefined          // 'forwards' | 'backwards' | undefined
  },
  scroll: {
    reset: true,
    target: '#anchor'
  },
  meta: {}                        // Custom metadata
}

Modifying Visit Behavior

Best done in the visit:start hook before requests or animations begin:

swup.hooks.on('visit:start', (visit) => {
  // Disable animations
  visit.animation.animate = false;
  
  // Set custom animation
  visit.animation.name = 'slide';
  
  // Keep scroll position
  visit.scroll.reset = false;
  
  // Change containers for this visit
  visit.containers = ['#sidebar'];
  
  // Replace instead of push history
  visit.history.action = 'replace';
  
  // Disable cache
  visit.cache.read = false;
  visit.cache.write = false;
  
  // Add custom metadata
  visit.meta.customData = 'value';
});

Detecting History Navigation

swup.hooks.on('visit:start', (visit) => {
  if (visit.history.popstate) {
    console.log('Direction:', visit.history.direction);
  }
});

Working with the Loaded Document

swup.hooks.on('content:replace', (visit) => {
  const lang = visit.to.document?.documentElement.getAttribute('lang');
  if (lang) {
    document.documentElement.setAttribute('lang', lang);
  }
});

Cache

Swup caches loaded pages in memory for instant repeat visits.

Disabling Cache

const swup = new Swup({ cache: false });

Cache API

// Check cache size
swup.cache.size; // 13

// Check if URL is cached
swup.cache.has('/about'); // true/false

// Get cached page
const page = swup.cache.get('/about');

// Add to cache
swup.cache.set('/about', { 
  url: '/about', 
  html: '<html>...</html>' 
});

// Update cache entry
swup.cache.update('/about', { customData: 'value' });

// Delete specific entry
swup.cache.delete('/about');

// Clear all entries
swup.cache.clear();

// Remove entries matching condition
swup.cache.prune((url, page) => {
  return url.includes('/blog/');
});

Cache Pruning Strategies

Limit by count:

const maxEntries = 20;
swup.hooks.on('cache:set', () => {
  const urls = [...swup.cache.all.keys()].reverse().slice(maxEntries);
  swup.cache.prune((url) => urls.includes(url));
});

Limit by time (TTL):

const ttl = 5 * 60_000; // 5 minutes

swup.hooks.on('cache:set', (visit, { page }) => {
  swup.cache.update(page.url, { created: Date.now(), ttl });
});

swup.hooks.before('page:load', () => {
  swup.cache.prune((url, { created, ttl }) => Date.now() > created + ttl);
});

Markup Attributes

Control swup behavior with data attributes.

<!-- Ignore single link -->
<a href="/" data-no-swup>Ignored</a>

<!-- Ignore all links in container -->
<nav data-no-swup>
  <a href="/page1">Ignored</a>
  <a href="/page2">Ignored</a>
</nav>

Custom Animations

<a href="/about" data-swup-animation="slide">Slide Animation</a>

Adds to-slide class to HTML during transition:

html.is-changing.to-slide .transition-page {
  /* slide-specific styles */
}
<nav data-swup-animation="slide">
  <a href="/page1">Slides</a>
  <a href="/page2">Slides</a>
  <a href="/page3" data-swup-animation="fade">Fades (override)</a>
</nav>

Persisting Elements

Keep element state (playback, scroll, event listeners) between pages:

<video src="/video.mp4" autoplay data-swup-persist="video-player"></video>

Elements are matched by their persist attribute value.

History Mode

Replace instead of push history entry:

<a href="/wizard-step-2" data-swup-history="replace">Next Step</a>

Methods & Helpers

Instance Methods

Navigate programmatically:

swup.navigate('/about');

// With options
swup.navigate('/about', { 
  animate: false,
  animation: 'slide',
  history: 'replace',
  method: 'POST',
  data: new FormData(),
  cache: { read: false, write: true },
  meta: { customData: 'value' }
});

Destroy swup:

swup.destroy();

Manage plugins:

swup.use(new SwupScrollPlugin());
swup.unuse('SwupScrollPlugin');
const plugin = swup.findPlugin('SwupScrollPlugin');

Debug logging:

swup.log('Message', { data: 'object' });

Instance Properties

swup.options;   // Current configuration
swup.plugins;   // Active plugin instances
swup.location;  // Current location object

// Location details
swup.location.href;     // https://example.com/path?query#hash
swup.location.url;      // /path?query
swup.location.pathname; // /path
swup.location.search;   // ?query
swup.location.hash;     // #hash

Helper Utilities

import { 
  Location,
  classify,
  createHistoryRecord,
  updateHistoryRecord,
  delegateEvent,
  getCurrentUrl
} from 'swup';

// Parse URLs
const { url, hash } = Location.fromUrl('http://example.com/about#section');
const { url, hash } = Location.fromElement(linkElement);

// Sanitize strings for CSS/slugs
classify('Hello World'); // 'hello-world'

// Manage history
createHistoryRecord('/new-page', { custom: 'data' });
updateHistoryRecord(null, { custom: 'data' });

// Delegate events
const delegation = delegateEvent('form', 'submit', (event) => {
  console.log('Form submitted');
});
delegation.destroy(); // Clean up

Plugins

Swup's modular architecture allows extending functionality through plugins.

Installing Plugins

npm install @swup/scroll-plugin
import Swup from 'swup';
import SwupScrollPlugin from '@swup/scroll-plugin';

const swup = new Swup({
  plugins: [new SwupScrollPlugin()]
});

Or add/remove plugins dynamically:

swup.use(new SwupScrollPlugin());
swup.unuse('SwupScrollPlugin');

Official Plugins

Plugin Description
Accessibility Plugin Screen reader announcements, focus management, reduced motion support
Body Class Plugin Update body classname on navigation
Debug Plugin Development logging and warnings
Forms Plugin Handle form submissions with transitions
Fragment Plugin Dynamic container replacement rules
Head Plugin Update <head> contents
JS Plugin Advanced JavaScript animation management
Morph Plugin Morph elements instead of replacing
Parallel Plugin Animate old and new content simultaneously
Preload Plugin Preload pages on hover/visibility
Progress Bar Plugin Loading progress indicator
Route Name Plugin Named routes and route-based animations
Scripts Plugin Re-run scripts after navigation
Scroll Plugin Smooth scrolling with offsets

Official Themes

Theme Description
Fade Theme Fade transitions
Slide Theme Slide transitions
Overlay Theme Overlay slide transitions

Plugin Examples

Accessibility Plugin

import SwupA11yPlugin from '@swup/a11y-plugin';

const swup = new Swup({
  plugins: [new SwupA11yPlugin({
    headingSelector: ['main h1', 'h1'],
    respectReducedMotion: true,
    autofocus: false,
    announcements: {
      visit: 'Navigated to: {title}',
      url: 'New page at {url}'
    }
  })]
});

Preload Plugin

import SwupPreloadPlugin from '@swup/preload-plugin';

const swup = new Swup({
  plugins: [new SwupPreloadPlugin({
    throttle: 5,
    preloadHoveredLinks: true,
    preloadVisibleLinks: {
      threshold: 0.2,
      delay: 500
    }
  })]
});
<a href="/about" data-swup-preload>Preloaded on page load</a>

<nav data-swup-preload-all>
  <a href="/page1">All preloaded</a>
  <a href="/page2">All preloaded</a>
</nav>

Troubleshooting

Scripts Not Running

Swup doesn't execute scripts in new content by default. Solutions:

  1. Use hooks to trigger code after navigation
  2. Use the Head Plugin for scripts in <head>
  3. Use the Scripts Plugin to re-run scripts

Stylesheets Not Loading

Swup doesn't update <head> by default. Either:

  • Use a single stylesheet for the entire site
  • Install the Head Plugin with awaitAssets: true

Screen Readers Not Announcing Changes

Install the Accessibility Plugin for proper announcements.

Scroll Anchors Hidden by Fixed Header

[id] {
  scroll-margin-top: 100px; /* Adjust for header height */
}

Or use the Scroll Plugin with offset option.

History Navigation Not Working

Swup only handles history entries it created. Check:

  • Your linkSelector includes all page links
  • You're using swup's history helpers for custom entries
  • Configure skipPopStateHandling to accept all entries if needed

Hard Page Load Instead of Transition

Common causes:

  1. Missing container on old or new page
  2. Visit ignored by ignoreVisit callback or data-no-swup
  3. Click event propagation stopped before reaching swup

Class Name Conflicts

If another library uses transition-* classes:

const swup = new Swup({
  animationSelector: '[class*="swup-transition-"]'
});
<main id="swup" class="swup-transition-fade"></main>

Browser Support

Full Support (CDN)

Chrome Edge Safari Safari iOS Firefox
80+ 80+ 13.1+ 13.4+ 74+

Extended Support

  • Transpiling with Babel: Chrome 66+, Edge 16+, Safari 13+, Firefox 60+
  • Polyfills via polyfill.io: Further extends support
  • Swup v3: For IE 11 support (at cost of newer features)

Upgrading from v3

Breaking Changes Summary

  1. Install v4: npm install swup@latest
  2. Hooks replace events: swup.on()swup.hooks.on()
  3. Hook names changed: pageViewpage:view
  4. Visit object replaces transition: swup.transitionvisit parameter
  5. Navigate method renamed: swup.loadPage()swup.navigate()
  6. Animation attribute renamed: data-swup-transitiondata-swup-animation
  7. Unique container selectors required
  8. Scroll support built-in (Scroll Plugin optional)

Migration Examples

Events to Hooks:

// v3
swup.on('pageView', () => {});
swup.off('pageView', handler);

// v4
swup.hooks.on('page:view', () => {});
swup.hooks.off('page:view', handler);

Before/After Hooks:

// v3
swup.on('willReplaceContent', () => {});
swup.on('contentReplaced', () => {});

// v4
swup.hooks.before('content:replace', () => {});
swup.hooks.on('content:replace', () => {});

Transition Object:

// v3
swup.on('transitionStart', () => {
  console.log(swup.transition.to);
  console.log(swup.transition.custom);
});

// v4
swup.hooks.on('visit:start', (visit) => {
  console.log(visit.to.url);
  console.log(visit.animation.name);
});

Cache API:

// v3
swup.cache.cacheUrl({
  url: '/about',
  title: 'About',
  blocks: ['<div id="swup"></div>'],
  originalContent: '<html>...</html>'
});

// v4
swup.cache.set('/about', { 
  url: '/about', 
  html: '<html>...</html>' 
});

Navigation:

// v3
swup.loadPage({ url: '/about' });

// v4
swup.navigate('/about');

Animation Attribute:

<!-- v3 -->
<a href="/about" data-swup-transition="slide">About</a>

<!-- v4 -->
<a href="/about" data-swup-animation="slide">About</a>