A pjax powered HTML tab selector

posted by Ayush Newatia
19 January, 2021

pjax has got to be one of the most underrated programming techniques on the modern web. You can get a sizeable performance boost and dynamic page interactions by just dropping in a pjax library and not writing any JavaScript yourself.

Turbolinks and its recent successor Turbo Drive both leverage the pjax concept to allow multi-page apps to operate like single-page apps (SPAs) with little to no custom JavaScript.

The number of use cases in which you can use pjax instead of writing custom JavaScript is astounding. I recently revamped my portfolio website. As part of the revamp, I wanted to showcase my projects by category; using a tab selector to switch categories. I had two requirements for this component:

That second one is largely vanity as almost nobody disables JavaScript these days; but even so I strongly believe that static websites shouldn’t completely break without JavaScript.

Initially, I was considering two approaches:


Building a tab selector with pjax

Thinking about the problem a bit further, I figured pjax could fulfil both my requirements. I used an amazing library called Swup.js. It’s a pjax implementation with a focus on animating page or component transitions.

I implemented each tab as it’s own individual web page; which meant I could “deep link” to a tab section using the page’s unique URL. I could also link to a specific project using the element’s id.

To initialise Swup, I used the following code:

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

document.addEventListener('DOMContentLoaded', () => {
  if(document.getElementById("swup")) {
    const swup = new Swup({
      linkSelector: "a[data-swup]",
      plugins: [new SwupPreloadPlugin()]

This initialises Swup only if the page contains an element with the id swup; which also denotes the area of the page to be replaced when the page changes.

It also specifies that only links decorated with data-swup will be given the pjax treatment; and it loads the SwupPreloadPlugin which preloads the content of all Swup links on a given page so the user doesn’t see a delay while navigating. Finally I added a CSS fade animation as per the docs.

This is my HTML markup in Liquid template syntax:

<swup-container id="swup">
  <tab-selector am-margin="vertical-md">
    {% for category in site.data.project_categories %}
      {% assign state = "inactive" %}
      {% if page.project_category == category.id %}
        {% assign state = "active" %}
      {% endif %}

      <tab-item am-state="{{ state }}">
        <a href="/projects/{{ category.id }}" data-swup data-swup-preload>{{ category.name }}</a>
    {% endfor %}

  {{ content }}

And that’s it! We have a tab selector with deep linking and we haven’t written single line of custom JavaScript. Check it out in action here.

With JavaScript disabled, nothing breaks because all the functionality is built using web standards like HTML links. The JavaScript simply enhances the user interaction.


I love the elegance of dropping in a small JavaScript library that progressively enhances a web page.

With web pages getting more and more bloated by the day, the simplicity of a concept like pjax and the power it gives you really is something to behold. The next time you find yourself reaching for some custom JavaScript, maybe try to solve the problem with pjax!