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
Link clicked → visit:start → page:load → content:replace → visit:end
↓ ↓
animation:out:start animation:in:start
↓ ↓
animation:out:await animation:in:await
↓ ↓
animation:out:end animation:in:end
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.
Ignoring Links
<!-- 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 */
}
Apply to all links in a container:
<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
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
}
})]
});
Mark specific links for preloading:
<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:
- Use hooks to trigger code after navigation
- Use the Head Plugin for scripts in
<head> - 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
linkSelectorincludes all page links - You're using swup's history helpers for custom entries
- Configure
skipPopStateHandlingto accept all entries if needed
Hard Page Load Instead of Transition
Common causes:
- Missing container on old or new page
- Visit ignored by
ignoreVisitcallback ordata-no-swup - 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
- Install v4:
npm install swup@latest - Hooks replace events:
swup.on()→swup.hooks.on() - Hook names changed:
pageView→page:view - Visit object replaces transition:
swup.transition→visitparameter - Navigate method renamed:
swup.loadPage()→swup.navigate() - Animation attribute renamed:
data-swup-transition→data-swup-animation - Unique container selectors required
- 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>'
});
// 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>