swup swup Upgrading
GitHub swup on GitHub

Upgrading

Upgrade your project from swup 3 to 4.

If you're upgrading from swup 2, see Upgrading from swup 2 to 3.

New features

Swup 4 introduces new features to become more customizable and enable advanced use cases. Some of the highlights are a new hook system, a visit object available in hook handlers, and built-in scroll support. See the release announcement for a full list of everything that's new.

Breaking changes

There are breaking changes in this release that will require modifications to projects using swup. If you only use swup for simple page transitions, you might not need to touch your code. However, if you make use of events, custom transitions or overwrite methods on the swup instance, you might want to take some more time to review these changes below and modify your site where necessary.

Install the latest version

Install the latest version from npm:

npm install swup@latest
npm install swup@latest

If you're loading swup from a CDN, update the version constraint:

<script src="https://unpkg.com/swup@3"></script> 
<script src="https://unpkg.com/swup@4"></script> 
<script src="https://unpkg.com/swup@3"></script> 
<script src="https://unpkg.com/swup@4"></script> 

Repeat this process for any of the plugins you are using.

Scroll support

Swup 4 will correctly reset the scroll position after each navigation, as well as scroll to #anchor links on the same page. The Scroll Plugin is no longer required for recreating basic browser behavior. If you need animated scrolling, custom offsets, and other customization, keep using the Scroll Plugin.

New hook system

Swup 4 comes with a new hook system that allows more flexibility and replaces the previous events implementation. Among other features, handlers can now pause execution by returning a Promise, or replace the internal default handler completely. See Hooks for details and more examples.

All hook-related functions now live on the hooks instance of swup:

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

Hook names

For easier grouping, hook names are consistently namespaced and in present tense:

  • pageViewpage:view
  • clickLinklink:click
  • contentReplacedcontent:replace
  • serverErrorfetch:error
  • etc.

To clarify the lifecycle, the transition hooks have been renamed to visit:

  • transitionStartvisit:start
  • transitionEndvisit:end

Some hooks were removed entirely:

The old willReplaceContent and contentReplaced events are superseded by a single content:replace hook. Since swup can now register handlers to run before a specific hook, it serves both use cases:

// Run right before the content is replaced
swup.on('willReplaceContent', () => {}) 
swup.hooks.before('content:replace', () => {}) 
// Run right before the content is replaced
swup.on('willReplaceContent', () => {}) 
swup.hooks.before('content:replace', () => {}) 
// Run directly after the content was replaced
swup.on('contentReplaced', () => {}) 
swup.hooks.on('content:replace', () => {}) 
// Run directly after the content was replaced
swup.on('contentReplaced', () => {}) 
swup.hooks.on('content:replace', () => {}) 

The pageRetrievedFromCache event has been removed. There is now only a single page:load hook that fires whenever a page was loaded. Check its boolean cache parameter to know if the page was loaded from cache or not.

swup.on('pageRetrievedFromCache', () => {}); 
swup.hooks.on('page:load', (_, { page, cache }) => { /* cache is true or false */ }); 
swup.on('pageRetrievedFromCache', () => {}); 
swup.hooks.on('page:load', (_, { page, cache }) => { /* cache is true or false */ }); 

Visit object

Along with a new hook system, Swup 4 introduces a visit object that holds information about the current page visit, like the previous and next URL or the element and event that triggered the visit. See Visit for details and more examples.

// Get the next URL and the link element that was clicked
swup.hooks.on('page:view', (visit) => {
  console.log('New page: ', visit.to.url);
  console.log('Triggered by: ', visit.trigger.el);
});

// Disable animations on the upcoming visit
swup.hooks.on('visit:start', (visit) => {
  visit.animation.animate = false;
});
// Get the next URL and the link element that was clicked
swup.hooks.on('page:view', (visit) => {
  console.log('New page: ', visit.to.url);
  console.log('Triggered by: ', visit.trigger.el);
});

// Disable animations on the upcoming visit
swup.hooks.on('visit:start', (visit) => {
  visit.animation.animate = false;
});

The visit object replaces the transition object of swup 3.

swup.on('transitionStart', () => { 
  console.log('Visit to', swup.transition.to); 
  console.log('Animation name', swup.transition.custom); 
}); 
swup.hooks.on('visit:start', (visit) => { 
  console.log('Visit to', visit.to.url); 
  console.log('Animation name', visit.animation.name); 
}); 
swup.on('transitionStart', () => { 
  console.log('Visit to', swup.transition.to); 
  console.log('Animation name', swup.transition.custom); 
}); 
swup.hooks.on('visit:start', (visit) => { 
  console.log('Visit to', visit.to.url); 
  console.log('Animation name', visit.animation.name); 
}); 

Cache API

The cache has been simplified. It no longer requires passing in the title, containers, or body class of the page. Only the URL and HTML response are required. Please review the Cache docs if you access it directly in your code.

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

The method swup.loadPage({ url }) has been renamed to swup.navigate(url) for clarity.

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

Custom animation attribute

To improve clarity around naming, the attribute for choosing a custom animation is now properly called data-swup-animation.

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

Unique container selectors

Swup 4 will only match and replace a single element for each container selector. Previously, each selector would match as many elements as found on the page. We recommend only using id attributes or other unique identifiers for container selectors.

<div class="section">Navigation</div> 
<div class="section">Content</div> 
<div id="nav" class="section">Navigation</div> 
<div id="content" class="section">Content</div> 
<div class="section">Navigation</div> 
<div class="section">Content</div> 
<div id="nav" class="section">Navigation</div> 
<div id="content" class="section">Content</div> 
const swup = new Swup({
  containers: ['.section'] 
  containers: ['#nav', '#content'] 
})
const swup = new Swup({
  containers: ['.section'] 
  containers: ['#nav', '#content'] 
})

Container attributes

Swup 4 will no longer add [data-swup] attributes to containers.

<div id="swup" class="transition-page" data-swup="0"></div> 
<div id="swup" class="transition-page"></div> 
<div id="swup" class="transition-page" data-swup="0"></div> 
<div id="swup" class="transition-page"></div> 

Custom payloads

Going forward, only complete HTML responses are allowed from the server. Previously, swup supported sending custom payloads by using the Custom Payload Plugin or overloading the getPageData method directly. This change was done to drastically simplify library complexity and allow more flexibility for other more common use cases like dynamically setting content containers. If you require custom payloads, we recommend sticking with swup 3.

const swup = new Swup({
  plugins: [new SwupCustomPayloadPlugin()] 
  // no longer supported 
});
const swup = new Swup({
  plugins: [new SwupCustomPayloadPlugin()] 
  // no longer supported 
});
swup.getPageData = (req) => JSON.parse(req.textContent); 
// no longer supported 
swup.getPageData = (req) => JSON.parse(req.textContent); 
// no longer supported 

Browser support

Swup 4 removes support for CSS vendor prefixes on animation and transition properties. In practical terms, this won't reduce browser support, but it's probably a good idea to check the compatibility tables for transitions and animations. In case you need to support Safari 8 or lower, you might want to stick with swup 3.

.transition-page {
  -webkit-transition: opacity 200ms; 
  transition: opacity 200ms; 
}
.transition-page {
  -webkit-transition: opacity 200ms; 
  transition: opacity 200ms; 
}

Plugin authors

Hooks

As mentioned above, switch from events to hooks:

this.swup.on('contentReplaced', () => {}); 
this.swup.hooks.on('content:replace', () => {}); 
this.swup.on('contentReplaced', () => {}); 
this.swup.hooks.on('content:replace', () => {}); 

Creating custom hooks has changed:

this.swup._handlers.formSubmit = []; 
this.swup.hooks.create('form:submit'); 
this.swup._handlers.formSubmit = []; 
this.swup.hooks.create('form:submit'); 

As has triggering a hook:

this.swup.triggerEvent('formSubmit'); 
this.swup.hooks.call('form:submit'); 
this.swup.triggerEvent('formSubmit'); 
this.swup.hooks.call('form:submit'); 

If you need wait for all handlers to finish before continuing, await the call:

this.swup.triggerEvent('formSubmit'); 
await this.swup.hooks.call('form:submit'); 
this.swup.triggerEvent('formSubmit'); 
await this.swup.hooks.call('form:submit'); 

If you need to replace swup's internal handler for a custom implementation, don't replace the instance method. Instead, specify that your hook handler should replace the internal one.

this.swup.replaceContent = () => { /* custom implementation */ }; 
this.swup.hooks.replace('content:replace', () => { /* custom implementation */ }); 
this.swup.replaceContent = () => { /* custom implementation */ }; 
this.swup.hooks.replace('content:replace', () => { /* custom implementation */ });