An Introduction To Quasar Framework: Building Cross-Platform Applications

In this article, I will explain how to use Quasar Framework and its state-of-the-art UI (which follows Material guidelines) to build a notes app. The app will get its data from and store its data to Firebase. This tutorial is meant for anyone interested in building cross-platform applications that work well across all devices simultaneously. At the end of the article, you should have a proper understanding of what Quasar is, how to get started creating apps with Quasar, and also how to use Firebase.

To follow along in this article, you should have:

  • an understanding of HTML, CSS, and JavaScript;
  • at least a little experience with Vue.js;
  • Node.js version 10 or above and npm version 5 or above installed on your machine.
  • knowledge of how the command-line interface (CLI) works.

The deployed app is available for viewing, and the final code is on Github.

What Is Quasar Framework?

Quasar Framework is an open-source Vue.js-based cross-platform framework whose motto is: “write code once and simultaneously deploy it as a website, a mobile app and/or an Electron app”. It has many features that enable you, as a web developer, to build apps on desktop and mobile and to create progressive web apps (PWAs) using technologies such as Cordova, Electron, and the web (Vue.js).

Why Quasar Framework?

Quasar is an easy-to-use but powerful UI kit comprising a lot of UI components, layout elements, and helpers. Together, these elements provide a full-featured toolset for building responsive front-end apps, without your having to make use of many different UI libraries. It does the heavy lifting for you, allowing you to focus on features and not boilerplate.

In summary, Quasar offers support for many build modes, including:

  • single-page applications;
  • progressive web applications;
  • server-side rendering;
  • mobile apps (iOS and Android), using Cordova or Сapacitor;
  • multi-platform desktop apps, using Electron;
  • browser extensions.

Getting Started

To get started, let’s look at how to install Quasar on your local machine and set up a project.

Installation

There are three ways to start using Quasar:

  • embedding to an existing project via a content delivery network (CDN);
  • installing using the Vue.js CLI Quasar plugin;
  • installing using the Quasar CLI.

For this tutorial, we will be using the third method, which is the Quasar CLI. The first thing to do is install the Quasar CLI globally on your computer, or check whether it is installed by running the following commands in your CLI:

quasar -v #check if quasar has been installed previously

yarn global add @quasar/cli
# or
npm install -g @quasar/cli

Once this is done, you can now move on to setting up the project.

Project Set-Up

Run the following command in your CLI:

quasar create <folder_name>

Following this, you are going to be asked some questions. Here is my full configuration for the app we will be building.

Now we can move into the project folder and start up the application using the following commands:

cd <folder_name>
quasar dev

With the steps above complete, our app should be running on http://localhost:8080. This is what we should see:

Quasar’s Folder Structure

The default application structure for Quasar is intended to be a great starting point for developers to build any type of application. You can organize the application however you like and create directories whenever you need them.

.
├── public/                  # pure static assets (directly copied)
├── src/
│   ├── assets/              # dynamic assets (processed by Webpack)
│   ├── components/          # .vue components used in pages and layouts
│   ├── css/                 # CSS/Stylus/Sass/… files for your app
│   ├── layouts/             # layout .vue files
│   ├── pages/               # page .vue files
│   ├── boot/                # boot files (app initialization code)
│   ├── router/              # Vue Router
│   ├── store/               # Vuex Store
│   ├── App.vue              # root Vue component of your app
│   └── index.template.html  # template for index.html
├── .editorconfig            # editor config
├── .gitignore               # GIT ignore paths
├── .postcssrc.js            # PostCSS config
├── babel.config.js          # Babel config
├── package.json             # npm scripts and dependencies
├── quasar.conf.js           # Quasar app config file
└── README.md                # readme for your app

The source folder consists of about seven major directories that a beginner should care about:

  • quasar.conf.js
    This is the brain behind any Quasar application, because most configurations are done in this file. Amazingly, Quasar handles most of the complex configurations needed by the various tools and packages that you might use in an application. Some of these configurations are for:

  • src/assets
    The assets directory contains your uncompiled assets, such as Stylus or Sass files, images, and fonts.
  • src/components
    This is where all of your reusable components will live. These components make up the different parts of the application and can be reused and imported into your pages, layouts, and even other components.
  • src/css
    You will not find this in Vue.js, but Quasar provides this so that we can have all of our global CSS in Sass form. It consists of two files: app.sass is where all of our styles will go, while quasar.variables.sass contains all of the reusable variables we would want to make use of when styling our app. You could ignore the CSS directory if you feel it’s of no use to you.
  • src/layouts
    This helps us create defined layouts for an app without repeating code. This is useful when you want to include sidebars or fixed bottom bars or have distinct layouts for mobile and desktop.
  • src/pages
    The pages directory contains our application’s views and routes. Our pages are injected into the app and managed through Vue Router in /src/router/routes.js. This means that each page needs to be referenced there.
  • src/router
    This holds the routing configuration of our app. It consists of two folders:

    • /src/router/index.js holds the Vue Router initialization code.
    • /src/router/routes.js holds the routes of the app, loading our layouts alongside the routes or pages in the app.
      You might not need to do anything to the index.js file in a small project, but if your project will have routes, you will need to add them to the routes.js file.

Building a Notes App

When building an application with Quasar, the first thing we will want to do is create a layout. Quasar has made this process a lot easier than any other framework by making use of a layout builder. For our notes app, we will want something like the following, which is quite similar to the default layout but with a few modifications:

App Layout

In the sidebar of Quasar’s documentation, you will see the “Layout and Grid” option. When you click it, a dropdown will appear with more options, one of which is “Layout Builder”. Click on “Layout Builder”, which will bring you here:

This allows us to choose the options we want and remove the ones we don’t. Then, we would generate the code to paste in the layout file.

The first option helps us to pick the layout parts, while the second allows us to configure the layout parts. Finally, we export the generated layout.

If you want the exact same layout as mine, use the code below:

<template>
  <q-layout view="lHh lpR fFf">
    <q-header elevated class="bg-primary text-white">
      <q-toolbar>
        <q-btn dense flat round icon="menu" @click="left = !left" />
        <q-toolbar-title>
          <q-avatar>
            <img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg" />
          </q-avatar>
          Title
        </q-toolbar-title>
      </q-toolbar>
    </q-header>
    <q-drawer show-if-above v-model="left" side="left" bordered>
      <!-- drawer content -->
    </q-drawer>
    <q-page-container>
      <router-view />
    </q-page-container>
  </q-layout>
</template>

<script>
export default {
  data() {
    return {
      left: false
    };
  }
};
</script>

Remove the default layout and paste the code above or the code that you have generated into src/layouts/MainLayout.vue.

The code above is divided into three sections: the header (navbar), the drawer (sidebar), and the page container (which contains the router view).

We’ve made use of the state-of-the-art UI to style the whole page. As I said earlier, when using Quasar, you won’t need additional heavy libraries such as Hammer.js, Moment.js, or Bootstrap.

We will be adding data to the sidebar and editing the navbar. Once you’ve saved, you will notice our app now looks like this:

Let’s work on the layout, adding some items to the sidebar and changing the title of the app, If you scan the layout code that we added, you will see where we are supposed to edit and add these various items.

Here is what my layout looks like after I’ve added items to the sidebar and changed the title:

And here is the code:

<template>
  <q-layout view="lHh lpR fFf">
    <q-header elevated class="bg-primary text-white">
      <q-toolbar>
        <q-btn dense flat round icon="menu" @click="left = !left" />
        <q-toolbar-title class="text-h6">
          My Notes
        </q-toolbar-title>
      </q-toolbar>
    </q-header>
    <q-drawer show-if-above v-model="left" side="left" bordered>
      <q-list class="q-pt-xl">
        <q-item clickable v-ripple to="/">
          <q-item-section avatar>
            <q-icon name="home" size="md" />
          </q-item-section>
          <q-item-section class="text-weight-bold">Home</q-item-section>
        </q-item>
        <q-item clickable v-ripple to="/about">
          <q-item-section avatar>
            <q-icon name="info" size="md" />
          </q-item-section>
          <q-item-section class="text-weight-bold">About</q-item-section>
        </q-item>
      </q-list>
    </q-drawer>
    <q-page-container>
      <router-view />
    </q-page-container>
    <q-footer class="bg-grey-2 text-black ">
      <q-toolbar>
        <q-toolbar-title class="text-subtitle2">
          Designed and Built For this article.
        </q-toolbar-title>
      </q-toolbar>
    </q-footer>
  </q-layout>
</template>
<script>
export default {
  data() {
    return {
      left: false
    };
  }
};
</script>

We removed the logo in the navbar and edited the text, then added a list to the sidebar, making use of Quasar’s state-of-the-art UI. Check out the list items, and copy the code of any you wish to use.

App Design

Earlier on, I said I was going to use Quasar’s state-of-the-art UI (which follows Material guidelines) to build a notes app, and that’s what we will be doing now. Explaining the whole process in an article like this is difficult, but the “Style & Identity” section of Quasar’s documentation covers it well.

This will be a one-page app (index.vue), and here is the code, without any styling applied:

<template>
  <q-page class="">
    <div class="q-pa-md">
      <q-input
        bottom-slots
        v-model="newNoteContent"
        placeholder="Write your note here..."
        counter
        autogrow
        maxlength="300"
      >
        <template v-slot:after>
          <q-btn round dense flat icon="send" />
        </template>
      </q-input>
    </div>
    <q-separator size="10px" />
    <q-list bordered class="rounded-borders" style="max-width: 600px">
      <q-item-label header>You have 3 Note(s)</q-item-label>
      <div>
        <q-item>
          <q-item-section top>
            <q-item-label caption class="text-grey-9">
              He who has imagination without learning has wings but no feet.
            </q-item-label>
          </q-item-section>
          <q-item-section top side>
            <div class="text-grey-9 q-gutter-xs">
              <q-btn size="13px" flat dense round icon="delete" />
            </div>
          </q-item-section>
        </q-item>
        <q-separator size="1px" />
      </div>
      <div>
        <q-item>
          <q-item-section top>
            <q-item-label caption class="text-grey-9">
              He who has imagination without learning has wings but no feet.
            </q-item-label>
          </q-item-section>
          <q-item-section top side>
            <div class="text-grey-9 q-gutter-xs">
              <q-btn size="13px" flat dense round icon="delete" />
            </div>
          </q-item-section>
        </q-item>
        <q-separator size="1px" />
      </div>
      <div>
        <q-item>
          <q-item-section top>
            <q-item-label caption class="text-grey-9">
              He who has imagination without learning has wings but no feet.
            </q-item-label>
          </q-item-section>
          <q-item-section top side>
            <div class="text-grey-9 q-gutter-xs">
              <q-btn size="13px" flat dense round icon="delete" />
            </div>
          </q-item-section>
        </q-item>
        <q-separator size="1px" />
      </div>
    </q-list>
  </q-page>
</template>
<script>
import db from "src/boot/firebase";
export default {
  name: "PageIndex",
  data() {
    return {
      basic: false,
      fixed: false,
      newNoteContent: ""
    };
  }
};
</script>

In the code above, we have an input field from Quasar. We’ve attached a v-model to get the data from the input field once the “Submit” button is clicked. We also have a list of items that will be used to display each note, and each list item has an icon used to delete that particular item when clicked.

Setting Up Local Data

At this point, the design of our app is in place. The next thing we will do is create an array that would contain all of our notes. We will ensure that we can add to and delete from this array before setting up Firebase.

Here is the array that we will be making use of in our app for now. Later, we will remove this array or comment out the code.

notes: [
  {
    id: 1,
    noteContent: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea vereprehenderit aspernatur mollitia saepe cupiditate pariatur natus accusantium esse repudiandae nisi velit provident corporis commodi eius fugiat reiciendis non aliquam."
  },
  {
    id: 2,
    noteContent: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea vereprehenderit aspernatur mollitia saepe cupiditate pariatur natus accusantium esse repudiandae nisi velit provident corporis commodi eius fugiat reiciendis non aliquam."
  },
  {
    id: 3,
    noteContent: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea vereprehenderit aspernatur mollitia saepe cupiditate pariatur natus accusantium esse repudiandae nisi velit provident corporis commodi eius fugiat reiciendis non aliquam."
  }
]

Fetching Data

We now have our array. Let’s add these data to our app. Because we understand Vue.js, all we will do is loop through this array using the v-for directive, use the data gotten from the array, and then put the content wherever we want it to appear.

<div v-for="noteContent in notes" :key="noteContent.id">
  <q-item>
    <q-item-section top>
      <q-item-label caption class="text-grey-9">
        {{ noteContent.note }}
      </q-item-label>
    </q-item-section>
    <q-item-section top side>
      <div class="text-grey-9 q-gutter-xs">
        <q-btn
          size="13px"
          flat
          dense
          round
          icon="delete"
          @click="deleteNote(noteContent)"
        />
      </div>
    </q-item-section>
  </q-item>
  <q-separator size="1px" />
</div>

We also added a click event handler to the delete button, so that it loads this function whenever it’s created.

Adding Notes

Let’s see how to add notes to our app by using the input field. We will use JavaScript’s unShift() methods, which adds one or more elements to the beginning of an array and returns the new length of the array.

The first thing to do is to add a click event handler to the button.

<q-btn round dense flat icon="send" @click="addNote" />

Then, proceed to create this method in the script area.

methods: {
  addNote() {
    let newNote = {
      id: this.notes.length + 1,
     note: this.newNoteContent
    };
    this.notes.unshift(newNote);
    this.newNoteContent = "";
  }
}

In the code above, we created an object for the new note, which comprises the ID and the note itself, and then we added this newNote to the array of notes via the unShift() method.

Deleting Notes

Finally, before proceeding to use Firebase in our app, let’s see how to delete a note. The first thing would be to add an event listener to the delete icon:

<q-btn
  size="13px"
  flat
  dense
  round
  icon="delete"
  @click="deleteNote(noteContent)"
/>

And then we would create a method:

deleteNote(noteContent) {
  let noteId = noteContent.id;

  //doing this to get the real id of the notes
  let index = this.notes.findIndex(noteContent => noteContent.id === noteId);
  this.notes.splice(index, 1);
}

In this code, we got the id of the particular note that we want to delete through the parameter passed to the click event method that was created. Then, we made use of the splice method to remove only that item from the array.

Firebase

Now that these two pieces of functionality work, let’s now see how we can use Firebase in Quasar to add, fetch, and delete data. Firebase will also give us real-time data syncing across all devices. The data in our app won’t be very much, because it’s just for the purpose of learning. In case you are thinking of something big that would be used by millions of people, check out the pricing page.

Firebase is application development software from Google that enables us to develop iOS, Android, and web apps.

Setting Up Cloud Firestore

To get started, visit firebase.google.com and click on either the “Go to console” link in the top-right corner of your screen or the “Get started” button (ensure that you sign in with your Google account).

This will bring us to the console area, where we can create a project. Click on the “Add a project” button, and a form to create your project will appear. The first question will request the project’s name, which could be anything; for this article, we will call it “notesApp”. Let’s also disable Google Analytics because ours is a mini-app.

Click on the “Create project” button (this might take few seconds, so be patient). Then, click on “Continue”, so that we can create our cloud Firestore.

In the sidebar, click on “Firestore”, and then “Create database”.

This will bring up a modal. Click on “Start in test mode”, which will make it easy for us to start working with our database. Bear in mind that, “The default security rules for test mode allow anyone with your database reference to view, edit and delete all data in your database for the next 30 days”.

Click on “Next”, leave the default Cloud Firestore location, and then click on the “Enable” button. Once it loads, our database will be fully ready for us to use.

Note: The Firebase database is made up of collections, and these collections contain documents, and each document is a JavaScript object that has fields in it.

Let’s get started by creating a new collection for our notes.

To create a collection, click on “Start collection”. A modal will pop up for you to enter the collection ID — meaning, a name. Then, click on the “Next” button.

You can now start creating the documents and fields for each note. Auto-generate the ID of the document to ensure that it is unique by clicking “Auto-ID” beside the document field.

Click “Save”, and continue to create more documents. In the end, this is what my database looks like:

Now that we are done, let’s see how to connect Firebase to our app. Go to “Project overview” in the sidebar, and let’s add this to a web app by clicking the “Web” button.

A form will appear for us to “Add Firebase” to our web app. We will give it the same name, “notesApp”, and register the app (don’t check the “Firebase hosting” field).

Once it has loaded, it will bring up an SDK to help us initialize our database in the app. We won’t do it this way, although we will need some information from the generated SDK. The right way to do this in Quasar is to import the modules that we need and use a boot file.

So, leave that tab open, and let’s see how to add the Firebase SDK and initialize Firebase in our Quasar app.

The first thing to do would be to install Firebase in our project with npm.

npm install --save firebase

Once installation is complete, we are going to initialize our app’s connection to Firebase by creating a boot file, so that we have immediate access to the database when our app is ready.

A boot file helps us to run code before the app’s Vue.js root component is instantiated. Quasar’s documentation has more information about boot files and when to use boot files.

To generate a boot file, we will run this command in our CLI:

quasar new boot firebase

Note: You don’t need to use Firebase as the name of the boot file.

Once this is done, you will notice that the file is now created in the boot folder. To make use of this newly created boot file, we’ll need to add it to the quasar.config.js file’s boot array.

Let’s go back to the newly created boot file. Delete all of the code there because we don’t need it. We’ll import the modules that we need and configure our database. Paste in the following code:

import firebase from "firebase/app";
import "firebase/firestore";

const firebaseConfig = {
  // ...
};

// Initialize Firebase
firebase.initializeApp(firebaseConfig);

Here, we’ve imported Firebase itself and Firestore, and we’ve initialized Firebase, making use of the config, which we will be adding now.

At this point, we are almost done configuring our app. We need to add our unique configuration, which was provided in the SDK that was generated when we added Firebase to our web app. Copy only the configuration, and paste it into our array.

We should now have something like this:

import firebase from "firebase/app";
import "firebase/firestore";
const firebaseConfig = {
  apiKey: "AIzaSyDRcq5PXJSi5c5C8rl6Q6nudIJqTFaxXeA",
  authDomain: "notesapp-ffd7c.firebaseapp.com",
  projectId: "notesapp-ffd7c",
  storageBucket: "notesapp-ffd7c.appspot.com",
  messagingSenderId: "18944010047",
  appId: "1:18944010047:web:ddfb46fc6bc8bba375158a"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

One last thing, since we are making use of Firestore, is that we’ll need to initialize the cloud Firestore by adding this code to our boot file (the last line):

let db = firebase.firestore();
export default db;

This db will give us access to our Firestore database. We also exported it so that we can use it anywhere in our app.

At this point, you might still be a little confused, but if you have followed this guide, then you will have properly initialized Firebase for your application. You can read more about adding Firebase to your JavaScript project in the documentation.

Fetching Data From Firebase

If you have followed the guide so far, everything should work fine once you launch your app. Now let’s grab the notes created in the database and display them in our app.

For this, we will be making use of the .onSnapshot hook, which will be fired any time the data in our collection changes. This will tell us whether a document has been added, removed, or updated. For this guide, we will only deal with the addition and removal of documents. Using hooks like this makes real-time syncing across devices possible. Let’s get started.

The first thing to do would be for us to get access to the database by importing it into the index page of our app.

import db from 'src/boot/firebase';

Then, create a mounted hook, because we want to fetch our data immediately after the app has loaded.

mounted() {
  db.collection("notes").onSnapshot(snapshot => {
    snapshot.docChanges().forEach(change => {

      let noteChange = change.doc.data();

      if (change.type === "added") {
        console.log("New note: ", noteChange);
        this.notes.unshift(noteChange);
      }
      if (change.type === "modified") {
        console.log("Modified note: ", noteChange);
      }
      if (change.type === "removed") {
        console.log("Removed note: ", noteChange);
      }
    });
  });
}

In the code above, we are simply grabbing our notes collection, and every time there is a change in the collection, the onSnapShot method will be fired, which will return a snapShot of all our data. All of these data will be objects with type properties. These type properties will tell us the type of change that has happened and give us access to the data that was either added, modified, or removed.

This might sound confusing, but you will understand what we are doing as you read on.

If you save your code and check the console environment, you will notice that each note has been logged out. We can now push these objects to the notes array that we created earlier, so that we can display real-time data in our application.

The first thing to do is delete or comment out the objects in the notes array, so that we have something like this:

notes: []

Then, pass the objects to this array:

this.notes.unshift(noteChange);

Your code should now look like this:

if (change.type === "added") {
  this.notes.unshift(noteChange);
}

At this point, if you load the app, you will notice that you have successfully fetched your data from Firebase.

Adding Data to Firebase

Let’s see how to add a note to our notes collection in this app. At this point, if you try to use the input field to add a note, it will work but the note will disappear once you refresh the page because it’s not stored in any database.

To do this with Firebase, all that is needed is to update the addNote() method that we created earlier.

addNote() {
  let newNote = {
    // id: this.notes.length + 1,
    note: this.newNoteContent
  };
  // this.notes.unshift(newNote);

  db.collection("notes")
    .add(newNote)
    .then(docRef => {
      console.log("Document written with ID: ", docRef.id);
    })
    .catch(error => {
      console.error("Error adding document: ", error);
    });

  this.newNoteContent = "";
},

The first thing we did here was remove the ID that is used when we made use of the previous array, because we are now going to auto-generate the ID from Firebase. We also removed the unshift() method; it’s no longer useful because data is being fetched for our app once there is an update using the snapShot hook.

If we look at the code responsible for updating the Firestore db, all we are passing to the collection (notes) is the new object (newNote), and this function will automatically generate an ID for each of our documents. The documentation has more information on adding data to Firebase.

Deleting Data From Firebase

We are almost done with our app, but we need to be able to delete data in our app from Firebase. Currently, the delete function works, but if you reload the app, the deleted data will reappear.

As we did before, we are going to delete these data (or documents) from Firebase using the unique ID generated by Firebase.

Currently, we don’t have access to the ID. To access it, we will add it to the noteChange object:

noteChange.id = change.doc.id;

Once that is set, deleting data will be as easy as adding it. All we have to do is go to the deleteNote(noteContent) method that we created previously, delete the previous code, and make use of this:

deleteNote(noteContent) {
  let noteId = noteContent.id;
  db.collection("notes")
    .doc(noteId)
    .delete()
    .then(() => {
      console.log("Document successfully deleted!");
    })
    .catch(error => {
      console.error("Error removing document: ", error);
    });
}

This checks the notes collection for a document with the specified ID and then deletes it. But if we save our code now and click the delete icon, the data will delete but won’t leave the app’s interface unless we refresh our code, meaning that the snapshot hook needs to be updated. Go to the snapshot hook for removed, and add this code:

if (change.type === "removed") {
  console.log("Removed note: ", noteChange);
  let index = this.notes.findIndex(
    noteContent => noteContent.id === noteChange.id
  );
  this.notes.splice(index, 1);
}

This simply gets the ID of the post that we deleted and removes it from the interface.

With that done, we have built an app with Quasar that works with Firebase. One major advantage of Quasar is that it enables us to simultaneously deploy our project as a website, mobile app, or Electron app.

To deploy for iOS, Cordova needs to be installed on our local machine. A MacBook is highly preferable. Navigate to your CLI, and install Cordova globally:

$ npm install - g cordova

To install on Windows, you would make use of Electron. The documentation properly explains how to do this.

Conclusion

In this guide, we have built a notes application using Quasar and Firebase. By following this guide, you are now in a position to improve on and add your own features and functionality. Here are a few ideas to get you started:

  • Implement functionality to modify notes.
  • Add dates, so that you can order the data by date.
  • Style the app, and make it more creative.
  • Add images.
  • A lot more.

Useful Resources

How To Build A Real-Time Multi-User Game From Scratch

As the pandemic lingered, the suddenly-remote team I work with became increasingly foosball-deprived. I thought about how to play foosball in a remote setting, but it was clear that simply reconstructing the rules of foosball on a screen would not be a lot of fun.

What _is_ fun is to kick a ball using toy cars — a realization made as I was playing with my 2-year old kid. The same night I set out to build the first prototype for a game that would become Autowuzzler.

The idea is simple: players steer virtual toy cars in a top-down arena that resembles a foosball table. The first team to score 10 goals wins.

Of course, the idea of using cars to play soccer is not unique, but two main ideas should set Autowuzzler apart: I wanted to reconstruct some of the look and feel of playing on a physical foosball table, and I wanted to make sure it is as easy as possible to invite friends or teammates to a quick casual game.

In this article, I’ll describe the process behind the creation of Autowuzzler, which tools and frameworks I chose, and share a few implementation details and lessons I learned.

First Working (Terrible) Prototype

The first prototype was built using the open-source game engine Phaser.js, mostly for the included physics engine and because I already had some experience with it. The game stage was embedded in a Next.js application, again because I already had a solid understanding of Next.js and wanted to focus mainly on the game.

As the game needs to support multiple players in real-time, I utilized Express as a WebSockets broker. Here is where it becomes tricky, though.

Since the physics calculations were done on the client in the Phaser game, I chose a simple, but obviously flawed logic: The first connected client had the doubtful privilege of doing the physics calculations for all game objects, sending the results to the express server, which in turn broadcasted the updated positions, angles and forces back to the other player’s clients. The other clients would then apply the changes to the game objects.

This led to the situation where the first player got to see the physics happening in real-time (it is happening locally in their browser, after all), while all the other players were lagging behind at least 30 milliseconds (the broadcast rate I chose), or — if the first player’s network connection was slow — considerably worse.

If this sounds like poor architecture to you — you’re absolutely right. However, I accepted this fact in favor of quickly getting something playable to figure out if the game is actually fun to play.

Validate The Idea, Dump The Prototype

As flawed as the implementation was, it was sufficiently playable to invite friends for a first test drive. Feedback was very positive, with the major concern being — not surprisingly — the real-time performance. Other inherent problems included the situation when the first player (remember, the one in charge of everything) left the game — who should take over? At this point there was only one game room, so anyone would join the same game. I was also a bit concerned by the bundle size the Phaser.js library introduced.

It was time to dump the prototype and start with a fresh setup and a clear goal.

Project Setup

Clearly, the “first client rules all” approach needed to be replaced with a solution in which the game state lives on the server. In my research, I came across Colyseus, which sounded like the perfect tool for the job.

For the other main building blocks of the game I chose:

  • Matter.js as a physics engine instead of Phaser.js because it runs in Node and Autowuzzler does not require a full game framework.
  • SvelteKit as an application framework instead of Next.js, because it just went into public beta at that time. (Besides: I love working with Svelte.)
  • Supabase.io for storing user-created game PINs.

Let’s look at those building blocks in more detail.

Synchronized, Centralized Game State With Colyseus

Colyseus is a multiplayer game framework based on Node.js and Express. At its core, it provides:

  • Synchronizing state across clients in an authoritative fashion;
  • Efficient real-time communication using WebSockets by sending changed data only;
  • Multi-room setups;
  • Client libraries for JavaScript, Unity, Defold Engine, Haxe, Cocos Creator, Construct3;
  • Lifecycle hooks, e.g. room is created, user joins, user leaves, and more;
  • Sending messages, either as broadcast messages to all users in the room, or to a single user;
  • A built-in monitoring panel and load test tool.

Note: The Colyseus docs make it easy to get started with a barebones Colyseus server by providing an npm init script and an examples repository.

Creating A Schema

The main entity of a Colyseus app is the game room, which holds the state for a single room instance and all its game objects. In the case of Autowuzzler, it’s a game session with:

  • two teams,
  • a finite amount of players,
  • one ball.

A schema needs to be defined for all properties of the game objects that should be synchronized across clients. For example, we want the ball to synchronize, and so we need to create a schema for the ball:

class Ball extends Schema {
  constructor() {
   super();
   this.x = 0;
   this.y = 0;
   this.angle = 0;
   this.velocityX = 0;
   this.velocityY = 0;
  }
}
defineTypes(Ball, {
  x: "number",
  y: "number",
  angle: "number",
  velocityX: "number",
  velocityY: "number"
});

In the example above, a new class that extends the schema class provided by Colyseus is created; in the constructor, all properties receive an initial value. The position and movement of the ball is described using the five properties: x, y, angle, velocityX, velocityY. Additionally, we need to specify the types of each property. This example uses JavaScript syntax, but you can also use the slightly more compact TypeScript syntax.

Property types can either be primitive types:

  • string
  • boolean
  • number (as well as more efficient integer and float types)

or complex types:

  • ArraySchema (similar to Array in JavaScript)
  • MapSchema (similar to Map in JavaScript)
  • SetSchema (similar to Set in JavaScript)
  • CollectionSchema (similar to ArraySchema, but without control over indexes)

The Ball class above has five properties of type number: its coordinates (x, y), its current angle and the velocity vector (velocityX, velocityY).

The schema for players is similar, but includes a few more properties to store the player’s name and team’s number, which need to be supplied when creating a Player instance:

class Player extends Schema {
  constructor(teamNumber) {
    super();
    this.name = "";
    this.x = 0;
    this.y = 0;
    this.angle = 0;
    this.velocityX = 0;
    this.velocityY = 0;
    this.teamNumber = teamNumber;
  }
}
defineTypes(Player, {
  name: "string",
  x: "number",
  y: "number",
  angle: "number",
  velocityX: "number",
  velocityY: "number",
  angularVelocity: "number",
  teamNumber: "number",
});

Finally, the schema for the Autowuzzler Room connects the previously defined classes: One room instance has multiple teams (stored in an ArraySchema). It also contains a single ball, therefore we create a new Ball instance in the RoomSchema’s constructor. Players are stored in a MapSchema for quick retrieval using their IDs.

Now, with all the magic happening on the server, the client only handles the input and draws the state it receives from the server to the screen. With one exception:

Interpolation On The Client

Since we are re-using the same Matter.js physics world on the client, we can improve the experienced performance with a simple trick. Rather than only updating the position of a game object, we also synchronize the velocity of the object. This way, the object keeps on moving on its trajectory even if the next update from the server takes longer than usual. So rather than moving objects in discrete steps from position A to position B, we change their position and make them move in a certain direction.

Lifecycle

The Autowuzzler Room class is where the logic concerned with the different phases of a Colyseus room is handled. Colyseus provides several lifecycle methods:

  • onCreate: when a new room is created (usually when the first client connects);
  • onAuth: as an authorization hook to permit or deny entry to the room;
  • onJoin: when a client connects to the room;
  • onLeave: when a client disconnects from the room;
  • onDispose: when the room is discarded.

The Autowuzzler room creates a new instance of the physics world (see section “Physics In A Colyseus App”) as soon as it is created (onCreate) and adds a player to the world when a client connects (onJoin). It then updates the physics world 60 times a second (every 16.6 milliseconds) using the setSimulationInterval method (our main game loop):

// deltaTime is roughly 16.6 milliseconds
this.setSimulationInterval((deltaTime) => this.world.updateWorld(deltaTime));

The physics objects are independent of the Colyseus objects, which leaves us with two permutations of the same game object (like the ball), i.e. an object in the physics world and a Colyseus object that can be synced.

As soon as the physical object changes, its updated properties need to be applied back to the Colyseus object. We can achieve that by listening to Matter.js’ afterUpdate event and setting the values from there:

Events.on(this.engine, "afterUpdate", () => {
 // apply the x position of the physics ball object back to the colyseus ball object
 this.state.ball.x = this.physicsWorld.ball.position.x;
 // ... all other ball properties
 // loop over all physics players and apply their properties back to colyseus players objects
})

There’s one more copy of the objects we need to take care of: the game objects in the user-facing game.

Client-Side Application

Now that we have an application on the server that handles the synchronization of the game state for multiple rooms as well as physics calculations, let’s focus on building the website and the actual game interface. The Autowuzzler frontend has the following responsibilities:

  • enables users to create and share game PINs to access individual rooms;
  • sends the created game PINs to a Supabase database for persistence;
  • provides an optional “Join a game” page for players to enter the game PIN;
  • validates game PINs when a player joins a game;
  • hosts and renders the actual game on a shareable (i.e. unique) URL;
  • connects to the Colyseus server and handle state updates;
  • provides a landing (“marketing”) page.

For the implementation of those tasks, I chose SvelteKit over Next.js for the following reasons:

Why SvelteKit?

I have been wanting to develop another app using Svelte ever since I built neolightsout. When SvelteKit (the official application framework for Svelte) went into public beta, I decided to build Autowuzzler with it and accept any headaches that come with using a fresh beta — the joy of using Svelte clearly makes up for it.

These key features made me choose SvelteKit over Next.js for the actual implementation of the game frontend:

  • Svelte is a UI framework and a compiler and therefore ships minimal code without a client runtime;
  • Svelte has an expressive templating language and component system (personal preference);
  • Svelte includes global stores, transitions and animations out of the box, which means: no decision fatigue choosing a global state management toolkit and an animation library;
  • Svelte supports scoped CSS in single-file-components;
  • SvelteKit supports SSR, simple but flexible file-based routing and server-side routes for building an API;
  • SvelteKit allows for each page to run code on the server, e.g. to fetch data that is used to render the page;
  • Layouts shared across routes;
  • SvelteKit can be run in a serverless environment.

Creating And Storing Game PINs

Before a user can start playing the game, they first need to create a game PIN. By sharing the PIN with others, they can all access the same game room.

This is a great use case for SvelteKits server-side endpoints in conjunction with Sveltes onMount function: The endpoint /api/createcode generates a game PIN, stores it in a Supabase.io database and outputs the game PIN as a response. This is response is fetched as soon as the page component of the “create” page is mounted:

Storing Game PINs With Supabase.io

Supabase.io is an open-source alternative to Firebase. Supabase makes it very easy to create a PostgreSQL database and access it either via one of its client libraries or via REST.

For the JavaScript client, we import the createClient function and execute it using the parameters supabase_url and supabase_key we received when creating the database. To store the game PIN that is created on each call to the createcode endpoint, all we need to do is to run this simple insert query:

import { createClient } from '@supabase/supabase-js'

const database = createClient(
 import.meta.env.VITE_SUPABASE_URL,
 import.meta.env.VITE_SUPABASE_KEY
);

const { data, error } = await database
 .from("games")
 .insert([{ code: 123456 }]);

Note: The supabase_url and supabase_key are stored in a .env file. Due to Vite — the build tool at the heart of SvelteKit — it is required to prefix the environment variables with VITE_ to make them accessible in SvelteKit.

Accessing The Game

I wanted to make joining an Autowuzzler game as easy as following a link. Therefore, every game room needed to have its own URL based on the previously created game PIN, e.g. https://autowuzzler.com/play/12345.

In SvelteKit, pages with dynamic route parameters are created by putting the dynamic parts of the route in square brackets when naming the page file: client/src/routes/play/[gamePIN].svelte. The value of the gamePIN parameter will then become available in the page component (see the SvelteKit docs for details). In the play route, we need to connect to the Colyseus server, instantiate the physics world to render to the screen, handle updates to game objects, listen to keyboard input and display other UI like the score, and so on.

Connecting To Colyseus And Updating State

The Colyseus client library enables us to connect a client to a Colyseus server. First, let’s create a new Colyseus.Client by pointing it to the Colyseus server (ws://localhost:2567in development). Then join the room with the name we chose earlier (autowuzzler) and the gamePIN from the route parameter. The gamePIN parameter makes sure the user joins the correct room instance (see “match-making” above).

let client = new Colyseus.Client("ws://localhost:2567");
this.room = await client.joinOrCreate("autowuzzler", { gamePIN });

Since SvelteKit renders pages on the server initially, we need to make sure that this code only runs on the client after the page is done loading. Again, we use the onMount lifecycle function for that use case. (If you’re familiar with React, onMount is similar to the useEffect hook with an empty dependency array.)

onMount(async () => {
  let client = new Colyseus.Client("ws://localhost:2567");
  this.room = await client.joinOrCreate("autowuzzler", { gamePIN });
})

Now that we are connected to the Colyseus game server, we can start to listen to any changes to our game objects.

Here’s an example of how to listen to a player joining the room (onAdd) and receiving consecutive state updates to this player:

this.room.state.players.onAdd = (player, key) => {
  console.log(`Player has been added with sessionId: ${key}`);

  // add player entity to the game world
  this.world.createPlayer(key, player.teamNumber);

  // listen for changes to this player
  player.onChange = (changes) => {
   changes.forEach(({ field, value }) => {
     this.world.updatePlayer(key, field, value); // see below
   });
 };
};

In the updatePlayer method of the physics world, we update the properties one by one because Colyseus’ onChange delivers a set of all changed properties.

Note: This function only runs on the client version of the physics world, as game objects are only manipulated indirectly via the Colyseus server.

updatePlayer(sessionId, field, value) {
 // get the player physics object by its sessionId
 let player = this.world.players.get(sessionId);
 // exit if not found
 if (!player) return;
 // apply changes to the properties
 switch (field) {
   case "angle":
     Body.setAngle(player, value);
     break;
   case "x":
     Body.setPosition(player, { x: value, y: player.position.y });
     break;
   case "y":
     Body.setPosition(player, { x: player.position.x, y: value });
     break;
   // set velocityX, velocityY, angularVelocity ...
 }
}

The same procedure applies to the other game objects (ball and teams): listen to their changes and apply the changed values to the client’s physics world.

So far, no objects are moving because we still need to listen to keyboard input and send it to the server. Instead of directly sending events on every keydown event, we maintain a map of currently pressed keys and send events to the Colyseus server in a 50ms loop. This way, we can support pressing multiple keys at the same time and mitigate the pause that happens after the first and consecutive keydown events when the key stays pressed:

let keys = {};
const keyDown = e => {
 keys[e.key] = true;
};
const keyUp = e => {
 keys[e.key] = false;
};
document.addEventListener('keydown', keyDown);
document.addEventListener('keyup', keyUp);

let loop = () => {
 if (keys["ArrowLeft"]) {
   this.room.send("move", { direction: "left" });
 }
 else if (keys["ArrowRight"]) {
   this.room.send("move", { direction: "right" });
 }
 if (keys["ArrowUp"]) {
   this.room.send("move", { direction: "up" });
 }
 else if (keys["ArrowDown"]) {
   this.room.send("move", { direction: "down" });
 }
 // next iteration
 requestAnimationFrame(() => {
  setTimeout(loop, 50);
 });
}
// start loop
setTimeout(loop, 50);

Now the cycle is complete: listen for keystrokes, send the corresponding commands to the Colyseus server to manipulate the physics world on the server. The Colyseus server then applies the new physical properties to all the game objects and propagates the data back to the client to update the user-facing instance of the game.

Minor Nuisances

In retrospect, two things of the category nobody-told-me-but-someone-should-have come to mind:

  • A good understanding of how physics engines work is beneficial. I spent a considerable amount of time fine-tuning physics properties and constraints. Even though I built a small game with Phaser.js and Matter.js before, there was a lot of trial-and-error to get objects to move in the way I imagined them to.
  • Real-time is hard — especially in physics-based games. Minor delays considerably worsen the experience, and while synchronizing state across clients with Colyseus works great, it can’t remove computation and transmission delays.

Gotchas And Caveats With SvelteKit

Since I used SvelteKit when it was fresh out of the beta-oven, there were a few gotchas and caveats I would like to point out:

  • It took a while to figure out that environment variables need to be prefixed with VITE_ in order to use them in SvelteKit. This is now properly documented in the FAQ.
  • To use Supabase, I had to add Supabase to both the dependencies and devDependencies lists of package.json. I believe this is no longer the case.
  • SvelteKits load function runs both on the server and the client!
  • To enable full hot module replacement (including preserving state), you have to manually add a comment line <!-- @hmr:keep-all --> in your page components. See FAQ for more details.

Many other frameworks would have been great fits as well, but I have no regrets about choosing SvelteKit for this project. It enabled me to work on the client application in a very efficient way — mostly because Svelte itself is very expressive and skips a lot of the boilerplate code, but also because Svelte has things like animations, transitions, scoped CSS and global stores baked in. SvelteKit provided all the building blocks I needed (SSR, routing, server routes) and although still in beta, it felt very stable and fast.

Deployment And Hosting

Initially, I hosted the Colyseus (Node) server on a Heroku instance and wasted a lot of time getting WebSockets and CORS working. As it turns out, the performance of a tiny (free) Heroku dyno is not sufficient for a real-time use case. I later migrated the Colyseus app to a small server at Linode. The client-side application is deployed by and hosted on Netlify via SvelteKits adapter-netlify. No surprises here: Netlify just worked great!

Conclusion

Starting out with a really simple prototype to validate the idea helped me a lot in figuring out if the project is worth following and where the technical challenges of the game lay. In the final implementation, Colyseus took care of all the heavy lifting of synchronizing state in real-time across multiple clients, distributed in multiple rooms. It’s impressive how quickly a real-time multi-user application can be built with Colyseus — once you figure out how to properly describe the schema. Colyseus’ built-in monitoring panel helps in troubleshooting any synchronizing issues.

What complicated this setup was the physics layer of the game because it introduced an additional copy of each physics-related game object that needed to be maintained. Storing game PINs in Supabase.io from the SvelteKit app was very straightforward. In hindsight, I could have just used an SQLite database to store the game PINs, but trying out new things is half of the fun when building side projects.

Finally, using SvelteKit for building out the frontend of the game allowed me to move quickly — and with the occasional grin of joy on my face.

Now, go ahead and invite your friends to a round of Autowuzzler!

Further Reading on Smashing Magazine

Respecting Users’ Motion Preferences

When working with motion on the web, it’s important to consider that not everyone experiences it in the same way. What might feel smooth and slick to some might be annoying or distracting to others — or worse, induce feelings of sickness, or even cause seizures. Websites with a lot of motion might also have a higher impact on the battery life of mobile devices, or cause more data to be used (autoplaying videos, for instance, will require more of a user’s data than a static image). These are just some of the reasons why motion-heavy sites might not be desirable for all.

Most new operating systems enable the user to set their motion preferences in their system-level settings. The prefers-reduced-motion media query (part of the Level 5 Media Queries specification) allows us to detect users’ system-level motion preferences, and apply CSS styles that respect that.

The two options for prefers-reduced-motion are reduce or no-preference. We can use it in the following way in our CSS to turn off an element’s animation if the user has explicitly set a preference for reduced motion:

.some-element {
  animation: bounce 1200ms;
}

@media (prefers-reduced-motion: reduce) {
  .some-element {
    animation: none;
  }
}

Conversely, we could set the animation only if the user has no motion preference. This has the advantage of reducing the amount of code we need to write, and means it’s less likely we’ll forget to cater for users’ motion preferences:

@media (prefers-reduced-motion: no-preference) {
  .some-element {
    animation: bounce 1200ms;
  }
}

An added advantage is that older browsers that don’t support prefers-reduced-motion will ignore the rule and only display our original, motion-free element.

Which Rule?

Unlike min-width and max-width media queries, where the more-or-less established consensus is mobile-first (therefore favoring min-width), there is no single “right” way to write your reduced-motion styles. I tend to favor the second example (applying animations only if prefers-reduced-motion: no-preference evaluates true), for the reasons listed above. Tatiana Mac wrote this excellent article which covers some of the approaches developers might consider taking, as well plenty of other great points, including key questions to ask when designing with motion on the web.

As always, team communication and a consistent strategy are key to ensuring all bases are covered when it comes to web accessibility.

Practical Use: Applying prefers-reduced-motion To Scroll Behavior

prefers-reduced-motion has plenty of applications beyond applying (or not applying) keyframe animations or transitions. One example is smooth scrolling. If we set scroll-behaviour: smooth on our html element, when a user clicks an in-page anchor link they will be smoothly scrolled to the appropriate position on the page (currently not supported in Safari):

html {
  scroll-behavior: smooth;
}

Unfortunately, in CSS we don’t have much control over that behavior right now. If we have a long page of content, the page scrolls very fast, which can be a pretty unpleasant experience for someone with motion sensitivity. By wrapping it in a media query, we can prevent that behavior from being applied in cases where the user has a reduced-motion preference:

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

Catering For Motion Preferences In Javascript

Sometimes we need to apply motion in JavaScript rather than CSS. We can similarly detect a user’s motion preferences with JS, using matchMedia. Let’s see how we can conditionally implement smooth scroll behavior in our JS code:

/* Set the media query */
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')

button.addEventListener('click', () => {
  /* If the media query matches, set scroll behavior variable to 'auto', 
  otherwise set it to 'smooth' */
  const behavior = prefersReducedMotion.matches ? 'auto' : 'smooth'

  /* When the button is clicked, the user will be scrolled to the top */
  window.scrollTo({
    x: 0,
    y: 0,
    behavior
  })
})

The same principle can be used to detect whether to implement motion-rich UIs with JS libraries — or even whether to load the libraries themselves.

In the following code snippet, the function returns early if the user prefers reduced motion, avoiding the unnecessary import of a large dependency — a performance win for the user. If they have no motion preference set, then we can dynamically import the Greensock animation library and initialize our animations.

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')

const loadGSAPAndInitAnimations = () => {
  /* If user prefers reduced motion, do nothing */
  if (prefersReducedMotion.matches) return

  /* Otherwise, import the GSAP module and initialize animations */
  import('gsap').then((object) => {
    const gsap = object.default
    /* Initialize animations with GSAP here */
  })
}

loadGSAPAndInitAnimations()

reduced-motion Doesn’t Mean No Motion

When styling for reduced motion preferences, it’s important that we still provide the user with meaningful and accessible indicators of when an action has occurred. For instance, when switching off a distracting or motion-intensive hover state for users who prefer reduced motion, we must take care to provide a clear alternative style for when the user is hovering on the element.

The following demo shows an elaborate transition when the user hovers or focuses on a gallery item if they have no motion preference set. If they prefer reduced motion, the transition is more subtle, yet still clearly indicates the hover state:

See the Pen Gallery with prefers-reduced-motion by Michelle Barker.

Reduced motion doesn’t necessarily mean removing all transforms from our webpage either. For instance, a button that has a small arrow icon that moves a few pixels on hover is unlikely to cause problems for someone who prefers a reduced-motion experience, and provides a more useful indicator of a change of state than color alone.

I sometimes see developers applying reduced motion styles in the following way, which eliminates all transitions and animations on all elements:

@media screen and (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

This is arguably better than ignoring users’ motion preferences, but doesn’t allow us to easily tailor elements to provide more subtle transitions when necessary.

In the following code snippet, we have a button that grows in scale on hover. We’re transitioning the colors and the scale, but users with a preference for reduced motion will get no transition at all:

button {
  background-color: hotpink;
  transition: color 300ms, background-color 300ms, transform 500ms cubic-bezier(.44, .23, .47, 1.27);
}

button:hover,
button:focus {
  background-color: darkviolet;
  color: white;
  transform: scale(1.2);
}

@media screen and (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }

  button {
    /* Even though we would still like to transition the colors of our button, the following rule will have no effect */
    transition: color 200ms, background-color 200ms;
  }

  button:hover,
  button:focus {
    /* Preventing the button scaling on hover */
    transform: scale(1);
  }
}

Check out this demo to see the effect. This is perhaps not ideal, as the sudden color switch without a transition could feel more jarring than a transition of a couple of hundred milliseconds. This is one reason why, on the whole, I generally prefer to style for reduced motion on a case-by-case basis.

If you’re interested, this is the same demo refactored to allow for customizing the transition when necessary. It uses a custom property for the transition duration, which allows us to toggle the scale transition on and off without having to rewrite the whole declaration.

When Removing Animation Is Better

Eric Bailey raises the point that “not every device that can access the web can also render animation, or render animation smoothly“ in his article, “Revisiting prefers-reduced-motion, the reduced motion media query.” For devices with a low refresh rate, which can cause janky animations, it might in fact be preferable to remove the animation. The update media feature can be used to determine this:

@media screen and
  (prefers-reduced-motion: reduce), 
  (update: slow) {
  * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

Be sure to read the full article for Eric’s recommendations, as he’s a first-rate person to follow in the field of accessibility.

The Sum Of All Parts

It’s important to keep in mind the overall page design when focusing so tightly on component-level CSS. What might seem a fairly innocuous animation at the component level could have a far greater impact when it’s repeated throughout the page, and is one of many moving parts.

In Tatiana’s article, she suggests organizing animations (with prefers-reduced-motion) in a single CSS file, which can be loaded only if (prefers-reduced-motion: no-preference) evaluates true. Seeing the sum total of all our animations could have the added benefit of helping us visualize the experience of visiting the site as a whole, and tailor our reduced-motion styles accordingly.

Explicit Motion Toggle

While prefers-reduced-motion is useful, it does have the drawback of only catering to users who are aware of the feature in their system settings. Plenty of users lack knowledge of this setting, while others might be using a borrowed computer, without access to system-level settings. Still, others might be happy with the motion for the vast majority of sites, but find sites with heavy use of motion hard to bear.

It can be annoying to have to adjust your system preferences just to visit one site. For these reasons, in some cases, it might be preferable to provide an explicit control on the site itself to toggle motion on and off. We can implement this with JS.

The following demo has several circles drifting around the background. The initial animation styles are determined by the user’s system preferences (with prefers-reduced-motion), however, the user has the ability to toggle motion on or off via a button. This adds a class to the body, which we can use to set styles depending on the selected preference. As a bonus, the choice of motion preference is also preserved in local storage — so it is “remembered” when the user next visits.

See the Pen Reduced-motion toggle by Michelle Barker.

Custom Properties

One feature in the demo is that the toggle sets a custom property, --playState, which we can use to play or pause animations. This could be especially handy if you need to pause or play a number of animations at once. First of all, we set the play state to paused:

.circle {
  animation-play-state: var(--playState, paused);
}

If the user has set a preference for reduced motion in their system settings, we can set the play state to running:

@media (prefers-reduced-motion: no-preference) {
  body {
    --playState: running;
  }
}

Note: Setting this on the body, as opposed to the individual element, means the custom property can be inherited.

When the user clicks the toggle, the custom property is updated on the body, which will toggle any instances where it is used:

// This will pause all animations that use the `--playState` custom property
document.body.style.setProperty('--playState', 'paused')

This might not be the ideal solution in all cases, but one advantage is that the animation simply pauses when the user clicks the toggle, rather than jumping back to its initial state, which could be quite jarring.

Special thanks goes to Scott O’Hara for his recommendations for improving the accessibility of the toggle. He made me aware that some screenreaders don’t announce the updated button text, which is changed when a user clicks the button, and suggested role="switch" on the button instead, with aria-checked toggled to on or off on click.

Video Component

In some instances, toggling motion at the component level might be a better option. Take a webpage with an auto-playing video background. We should ensure the video doesn’t autoplay for users with a preference for reduced motion, but we should still provide a way for them to play the video only if they choose. (Some might argue we should avoid auto-playing videos full stop, but we don’t always win that battle!) Likewise, if a video is set to autoplay for users without a stated preference, we should also provide a way for them to pause the video.

This demo shows how we can set the autoplay attribute when the user has no stated motion preference, implementing a custom play/pause button to allow them to also toggle playback, regardless of preference:

See the Pen Video with motion preference by Michelle Barker.

(I subsequently came upon this post by Scott O‘Hara, detailing this exact use case.)

Using The <picture> Element

Chris Coyier wrote an interesting article combining a couple of techniques to load different media sources depending on the user’s motion preferences. This is pretty cool, as it means that for users who prefer reduced motion, the much larger GIF file won’t even be downloaded. The downside, as far as I can see, is that once the file is downloaded, there is no way for the user to switch back to the motion-free alternative.

I create a modified version of the demo which adds this option. (Switch on reduced-motion in your system preferences to see it in action.) Unfortunately, when toggling between the animated and motion-free options in Chrome, it appears the GIF file is downloaded afresh each time, which isn’t the case in other browsers:

See the Pen Prefers Reduction Motion Technique PLUS! [forked] by Michelle Barker.

Still, this technique seems like a more respectful way of displaying GIFs, which can be a source of frustration to users.

Browser Support And Final Thoughts

prefers-reduced-motion has excellent support in all modern browsers going back a couple of years. As we’ve seen, by taking a reduced-motion-first approach, non-supporting browsers will simply get a reduced-motion fallback. There’s no reason not to use it today to make your sites more accessible.

Custom toggles most definitely have a place, and can vastly improve the experience for users who aren’t aware of this setting, or what it does. The downside for the user is inconsistency — if every developer is forced to come up with their own solution, the user needs to look for a motion toggle in a different place on every website.

It feels like the missing layer here is browsers. I’d love to see browsers implement reduced-motion toggles, somewhere easily accessible to the user, so that people know where to find it regardless of the site they’re browsing. It might encourage developers to spend more time ensuring motion accessibility, too.

Related Resources

A Simple But Effective Mental Health Routine For Programmers

After working on a coding project for a good couple of hours, many developers like myself tend to hit a wall. The level of creativity goes down, and we feel extreme mental exhaustion (especially if we have been working for more than three hours straight). We can’t help but want a fairly long break from our computers or working (desks) before we can continue with our work.

However, when we have a deadline to meet, we usually brace ourselves and keep going. At times, doing this works because we tap into our deeply hidden mental energy, but also, there are other times when it simply doesn’t work. Our brains go on strike until their demand is met. Which demand, you ask? A long and well-deserved break to refresh and reset.

If we decide to deny or ignore this demand consistently, sooner or later we’ll find that our overall rate of productivity goes down for a good deal of time. We also run the risk of failing at almost everything — including our personal lives.

Let’s take a good look at how we can establish an effective routine that will help ourselves bring benefits work-wise and health-wise in the long term.

Why Having A Routine Is Important

Generally, having a routine we follow has been found to have its own benefits. A study published by The Lancet30139-1/fulltext) concluded that people who have a stable daytime routine tend to have a better cognitive function, restful sleep, emotional stability, and improved overall well-being due to maintaining a good circadian rhythm (the natural cycles of life, like the sleep-wake cycle).

Another study published by Sage Journals showed that those who follow a daily routine tend to feel life is more meaningful for them than those who do not. Having a planned list of activities for the day and ensuring we do every activity when we are supposed to allow us to be less stressed and feel more fulfilled. On top of that, doing everything in our routine gives us a sense of accomplishment and purpose, and adds value to our lives. It also helps ease the tension that comes with the regular demands of life and the hecticness of some of the tough mathematical calculations and creativity-based activities we get engaged in as programmers.

Moreover, it helps us relax and refresh our brains after a long period of work. Now let’s take a look at the actual mental health routine.

The Simple Mental Health Routine For Developers

Personally, my mental health routine has allowed me to be more productive, break through the mental and physical limitations at work and be able to strike a good balance of my work, family and personal life.

Some of the techniques I have used that have proven to be effective are:

  • Resting awareness,
  • Meditation,
  • Mindfulness,
  • Reflection,
  • Body scanning,
  • Visualization,
  • Note-taking,
  • Mindful programming.

The last one is the meat of my routine. Here, I go about my programming tasks as usual but I try to remain aware of myself as I do it. If you would like to have a look at my personal mental health routine in detail where I go in-depth about each practice and how I work with it to better myself and my programming skills, I encourage you to try it out yourself.

Some of the benefits I have seen from the routine are as follows:

Reducing Momentary Negative Effect

For the most part, coding can be quite intimidating and disappointing for a beginner or even a well-versed developer. When we write code with all our hearts — hoping that we got everything right — and then find out that some areas of our code are not working and we can’t seem to figure out where exactly the problem is, we can easily become temporarily frustrated pessimists.

During such times, mindfulness can come in handy for us because research has shown that it helps reduce momentary negative effects. By doing mindful movement techniques such as walking meditation and mindful walking, we can increase positive feelings within ourselves and help us keep going.

Boosting Focus And Attention

When I am aware, I often find it less difficult to keep my focus and attention levels high and I am able to keep going without distractions. Scientific research attributes this ability to a brain wave called Alpha Rhythm that regulates information that brings about distraction.

Thinking, Remembering And Processing Information Better

When we are in the present moment, we get to understand things easily and remember them without a lot of hassles. Reasoning and figuring out how we are going to implement certain bits of our code to achieve the desired goal also become effortless over time.

Studies that focus on this particular topic have revealed that this is due to the ability of mindfulness practice to increase the density of the gray matter in the brain — specifically in those areas that are associated with perspective thinking, learning abilities, self-referential processing, and emotional regulation.

Feeling More Stable After Working Long Hours

Have you ever felt overwhelmed to the point where you you have lost control of yourself in terms of peace and calmness? Mindfulness gives us clarity on what we are working on and keeps us peaceful. In the past, I had scenarios where I felt so overwhelmed and disappointed by the constant inability to solve various bugs in my code that made me question why I got into programming in the first place.

As I practiced mindfulness, however, I was able to tell when I was exaggerating issues in my mind and when my thoughts were blowing things out of proportion. With the help of mindfulness techniques such as mindful walking and reflection, we get to see reality as it actually is, and not as we think of it. This changes our perspective for the better.

Regulating Stress And Anxiety

Are you about to miss deadlines and your mind can’t calm down and let you finish up on the remaining tasks in peace? Well, mindful breathing and visualization of your situation allow you to be aware of the stress and anxiety, and better react to them. It helps bring down the high levels of the Cortisol hormone that is linked to stress.

The points above are part of a simple mindfulness-based mental health routine that we can all easily work with as employed developers, freelancers, or as we run our own programming ventures (link to the mental health routine freebie). Implementing some of these would require a few minutes of meditation in the morning and then after that, we remain aware of ourselves and our environment as we go about our work in our typical day.

Getting Started With Your Own Mental Health Routine

In case you feel, individually, like you want to make some more customization to the given mental health routine above, by all means, feel free to do it. At the end of the day, it is about finding a mental health routine that goes well with you and has a positive impact on your work and life as a whole. Your mental health routine should be something that complements what you already have in your life and what you feel is good for you as a uniquely different person.

So don’t feel the pressure to have to stick to something that doesn’t necessarily sit well with you. However, below are some ideas or other mental health improvement strategies that are effective which you can explore.

Yoga

Yoga is a practice that helps us quiet our minds so as to ground ourselves in unbound consciousness, which is our essential nature. It helps unite the physical, mental, and spiritual aspects of our lives. Some of its benefits include improving memory, learning abilities, social-emotional processing, sensorimotor processing, and many other cognitive functions. This can make a good addition to your routine if these are the benefits you are looking for.

Other Meditation Styles

Other than mindfulness meditation, there are other meditation styles such as focused attention meditation, zen meditation, religious meditation (for various religions), and others that help train our brains on various personal values. You can look at your life and decide which areas you fall short in that affect your programming career and life in general, and go for a meditation practice that helps you with that. For instance, if you struggle to remain focused, you can work with focused attention meditation to sharpen your focus.

Sleeping

Funny as it may sound, sleep is a really good technique to refresh our minds and bodies. Studies show that it helps with improving the communication between nerve cells. On top of the mindfulness-based practices I work with, sleep is another one of my go-to techniques when I feel I need a total refreshing of my mind.

When you feel confused or overworked, leave your workplace and find yourself a place to lay down and shut down for a few minutes (if that won’t get you fired). However, don’t get into the habit of sleeping, or that will become counter-productive. I always recommend sleeping as the last resort.

Plan Ahead

One of the things that lead to confusion is a lack of planning. If you start off in many directions, you are sure to lose and come out more confused than you started. Programming requires a lot of mental planning before we get to execute it physically and research has linked this kind of planning to project success, not only in programming but also in other areas of our lives.

What I normally do isgain a good understanding of the entire project I am working on, the areas that I am supposed to work on, what is needed in those areas, and then I break down the work into small steps in my to-do list. Everytime I finish a task, I tick it off my list. This helps me make consistent progress and achieve the goal much faster.

Physical Exercise

While physical fitness is good for the body, it has also been found to have a positive effect on the brain. It reduces anxiety, depression, and negative moods. So, whenever you feel anxious or moody, just get into some exercises before getting back to work and you will most likely love the results.

Look For A Developer Friend

Sharing your problems with someone who understands the gravity of the issues, and who can also help you solve them, is among the most valuable things in life. Look for a programming buddy or team you can easily get along with and become work and life buddies.

Hold each other accountable, help each other through your projects, and consult each other when you need genuine feedback and constructive criticism. This makes for peace of mind, enjoyment, more productivity and satisfaction with your career.

Change Your Working Environment (If You Can)

This works well if you are a work-from-home developer. Although, it is also possible to change your environment in your workplace if your company has provided alternative sitting areas. Changing your environment can reset your mind and help you through some hurdles you have been struggling to get past. You can choose to change your sitting position, or move to a different corner of your office or working space. These actions might seem small but all have a significant impact.

Get A Breath Of Fresh Air

Exposing yourself to fresh air allows you to get more oxygen to your brain which helps improve the performance of the brain. If you stay in a poorly ventilated room for a long time, you can hurt your brain in the long run. By regularly getting some fresh air, you improve the power of your brain which can, in turn, help you push even further.

Listen To Your Favorite Songs

Different songs have different effects on people. Whenever you feel mentally and physically exhausted, listening to songs that remind you of your dreams, goals, and visions, as you relax, can energize you and make you want to keep going with your work.

Just close your eyes, lean back on your working chair, and listen to those songs as you visualize your goals and the amount of effort needed to get there, and you will notice your energy levels and motivation increase after some time. My personal favorite playlist is the slow independent (indie) songs or a playlist of slow soothing songs or instrumentals.

Conclusion

When trying out any of the techniques mentioned in this article, please ensure that you go for the ones that will blend in with your daily schedule, habits and beliefs as that will make it possible to have a much better experience working with them.

Taking care of our mental health is of utmost importance. We only get to have one brain throughout our lives so we need to do all we can to take good care of it. And the best part is that you don’t need to do much to make that happen. All you have to do is to get started on a mental health routine and stick to it. The first step is by adding the routine to your daily schedule. If the provided mental health routine works fine for you, figure out where you can begin doing mindfulness meditation and then remember to work mindfully.

If you want to spice things up, you can consider the other mental health techniques you like and use them to create your custom mental health routine that best fits your lifestyle. It is entirely up to you.

The key takeaway here is, you should seriously consider working with a routine and it’s even better if you get started with one as early as now if you want to improve your mental health, your work, and your overall well-being for years to come.

Again, if you would like to have a look at my completely detailed mental health routine, feel free to download it here.

So, what technique or daily routine works best for you? I’d love to hear from you in the comments section below!

Building The SSG I’ve Always Wanted: An 11ty, Vite And JAM Sandwich

I don’t know about you, but I’ve been overwhelmed by all the web development tools we have these days. Whether you like Markdown, plain HTML, React, Vue, Svelte, Pug templates, Handlebars, Vibranium — you can probably mix it up with some CMS data and get a nice static site cocktail.

I’m not going to tell you which UI development tools to reach for because they’re all great — depending on the needs of your project. This post is about finding the perfect static site generator for any occasion; something that lets us use JS-less templates like markdown to start, and bring in “islands” of component-driven interactivity as needed.

I’m distilling a year’s worth of learnings into a single post here. Not only are we gonna talk code (aka duct-taping 11ty and Vite together), but we’re also going to explore why this approach is so universal to Jamstackian problems. We’ll touch on:

  • Two approaches to static site generation, and why we should bridge the gap;
  • Where templating languages like Pug and Nunjucks still prove useful;
  • When component frameworks like React or Svelte should come into play;
  • How the new, hot-reloading world of Vite helps us bring JS interactivity to our HTML with almost zero configs;
  • How this complements 11ty’s data cascade, bringing CMS data to any component framework or HTML template you could want.

So without further ado, here’s my tale of terrible build scripts, bundler breakthroughs, and spaghetti-code-duct-tape that (eventually) gave me the SSG I always wanted: an 11ty, Vite and Jam sandwich called Slinkity!

A Great Divide In Static Site Generation

Before diving in, I want to discuss what I’ll call two “camps” in static site generation.

In the first camp, we have the “simple” static site generator. These tools don’t bring JavaScript bundles, single-page apps, and any other buzzwords we’ve come to expect. They just nail the Jamstack fundamentals: pull in data from whichever JSON blob of CMS you prefer, and slide that data into plain HTML templates + CSS. Tools like Jekyll, Hugo, and 11ty dominate this camp, letting you turn a directory of markdown and liquid files into a fully-functional website. Key benefits:

  • Shallow learning curve
    If you know HTML, you’re good to go!
  • Fast build times
    We’re not processing anything complex, so each route builds in a snap.
  • Instant time to interactive
    There’s no (or very little) JavaScript to parse on the client.

Now in the second camp, we have the “dynamic” static site generator. These introduce component frameworks like React, Vue, and Svelte to bring interactivity to your Jamstack. These fulfill the same core promise of combining CMS data with your site’s routes at build time. Key benefits:

  • Built for interactivity
    Need an animated image carousel? Multi-step form? Just add a componentized nugget of HTML, CSS, and JS.
  • State management
    Something like React Context of Svelte stores allow seamless data sharing between routes. For instance, the cart on your e-commerce site.

There are distinct pros to either approach. But what if you choose an SSG from the first camp like Jekyll, only to realize six months into your project that you need some component-y interactivity? Or you choose something like NextJS for those powerful components, only to struggle with the learning curve of React, or needless KB of JavaScript on a static blog post?

Few projects squarely fit into one camp or the other in my opinion. They exist on a spectrum, constantly favoring new feature sets as a project’s need evolve. So how do we find a solution that lets us start with the simple tools of the first camp, and gradually add features from the second when we need them?

Well, let’s walk through my learning journey for a bit.

Note: If you’re already sold on static templating with 11ty to build your static sites, feel free to hop down to the juicy code walkthrough. 😉

Going From Components To Templates And Web APIs

Back in January 2020, I set out to do what just about every web developer does each year: rebuild my personal site. But this time was gonna be different. I challenged myself to build a site with my hands tied behind my back, no frameworks or build pipelines allowed!

This was no simple task as a React devotee. But with my head held high, I set out to build my own build pipeline from absolute ground zero. There’s a lot of poorly-written code I could share from v1 of my personal site… but I’ll let you click this README if you’re so brave. 😉 Instead, I want to focus on the higher-level takeaways I learned starving myself of my JS guilty pleasures.

Templates Go A Lot Further Than You Might Think

I came at this project a recovering JavaScript junky. There are a few static-site-related needs I loved using component-based frameworks to fill:

  1. We want to break down my site into reusable UI components that can accept JS objects as parameters (aka “props”).
  2. We need to fetch some information at build time to slap into a production site.
  3. We need to generate a bunch of URL routes from either a directory of files or a fat JSON object of content.

List taken from this post on my personal blog.

But you may have noticed… none of these really need clientside JavaScript. Component frameworks like React are mainly built to handle state management concerns, like the Facebook web app inspiring React in the first place. If you’re just breaking down your site into bite-sized components or design system elements, templates like Pug work pretty well too!

Take this navigation bar for instance. In Pug, we can define a “mixin” that receives data as props:

// nav-mixins.pug
mixin NavBar(links)
    // pug's version of a for loop
    each link in links
        a(href=link.href) link.text

Then, we can apply that mixin anywhere on our site.

// index.pug
// kinda like an ESM "import"
include nav-mixins.pug
html
  body
    +NavBar(navLinksPassedByJS)
    main
      h1 Welcome to my pug playground 🐶

If we “render” this file with some data, we’ll get a beautiful index.html to serve up to our users.

const html = pug.render('/index.pug', { navLinksPassedByJS: [
    { href: '/', text: 'Home' },
    { href: '/adopt', text: 'Adopt a Pug' }
] })
// use the NodeJS filesystem helpers to write a file to our build
await writeFile('build/index.html', html)

Sure, this doesn’t give niceties like scoped CSS for your mixins, or stateful JavaScript where you want it. But it has some very powerful benefits over something like React:

  1. We don’t need fancy bundlers we don’t understand.
    We just wrote that pug.render call by hand, and we already have the first route of a site ready-to-deploy.
  2. We don’t ship any JavaScript to the end-user.
    Using React often means sending a big ole runtime for people’s browsers to run. By calling a function like pug.render at build time, we keep all the JS on our side while sending a clean .html file at the end.

This is why I think templates are a great “base” for static sites. Still, being able to reach for component frameworks where we really benefit from them would be nice. More on that later. 🙃

You Don’t Need A Framework To Build Single Page Apps

While I was at it, I also wanted some sexy page transitions on my site. But how do we pull off something like this without a framework?

Crossfade with vertical wipe transition. (Large preview)

Well, we can’t do this if every page is its own .html file. The whole browser refreshes when we jump from one HTML file to the other, so we can’t have that nice cross-fade effect (since we’d briefly show both pages on top of each other).

We need a way to “fetch” the HTML and CSS for wherever we’re navigating to, and animate it into view using JavaScript. This sounds like a job for single-page apps!
I used a simple browser API medley for this:

  1. Intercept all your link clicks using an event listener.
  2. fetch API: Fetch all the resources for whatever page you want to visit, and grab the bit I want to animate into view: the content outside the navbar (which I want to remain stationary during the animation).
  3. web animations API: Animate the new content into view as a keyframe.
  4. history API: Change the route displaying in your browser’s URL bar using window.history.pushState({}, 'new-route'). Otherwise, it looks like you never left the previous page!

For clarity, here’s a visual illustration of that single page app concept using a simple find-and-replace:

Step-by-step clientside routing process: 1. Medium rare hamburger is returned, 2. We request a well done burger using the fetch API, 3. We massage the response, 4. We pluck out the ‘patty’ element and apply it to our current page. (Large preview)

Source article

You can visit the source code from my personal site as well!

Sure, some pairing of React et al and your animation library of choice can do this. But for a use case as simple as a fade transition… web APIs are pretty dang powerful on their own. And if you want more robust page transitions on static templates like Pug or plain HTML, libraries like Swup will serve you well.

What 11ty Brought To The Table

I was feeling pretty good about my little SSG at this point. Sure it couldn’t fetch any CMS data at build-time, and didn’t support different layouts by page or by directory, and didn’t optimize my images, and didn’t have incremental builds.

Okay, I might need some help.

Given all my learnings from v1, I thought I earned my right to drop the “no third-party build pipelines” rule and reach for existing tools. Turns out, 11ty has a treasure trove of features I need!

If you’ve tried out bare-bones SSGs like Jekyll or Hugo, you should have a pretty good idea of how 11ty works. Only difference? 11ty uses JavaScript through-and-through.

11ty supports basically every template library out there, so it was happy to render all my Pug pages to .html routes. It’s layout chaining option helped with my foe-single-page-app setup too. I just needed a single script for all my routes, and a “global” layout to import that script:

// _includes/base-layout.html
<html>
<body>
  <!--load every page's content between some body tags-->
  {{ content }}
  <!--and apply the script tag just below this-->
  <script src="main.js"></script>
</body>
</html>

// random-blog-post.pug
---
layout: base-layout
---

article
  h2 Welcome to my blog
  p Have you heard the story of Darth Plagueis the Wise?

As long as that main.js does all that link intercepting we explored, we have page transitions!

Oh, And The Data Cascade

So 11ty helped clean up all my spaghetti code from v1. But it brought another important piece: a clean API to load data into my layouts. This is the bread and butter of the Jamstack approach. Instead of fetching data in the browser with JavaScript + DOM manipulation, you can:

  1. Fetch data at build-time using Node. This could be a call to some external API, a local JSON or YAML import, or even the content of other routes on your site (imagine updating a table-of-contents whenever new routes are added 🙃).
  2. Slot that data into your routes. Recall that .render function we wrote earlier:
const html = pug.render('/index.pug', { navLinksPassedByJS: [
    { href: '/', text: 'Home' },
    { href: '/adopt', text: 'Adopt a Pug' }
] })

…but instead of calling pug.render with our data every time, we let 11ty do this behind-the-scenes.

Sure, I didn’t have a lot of data for my personal site. But it felt great to whip up a .yaml file for all my personal projects:

# _data/works.yaml
- title: Bits of Good Homepage
  hash: bog-homepage
  links:
    - href: https://bitsofgood.org
      text: Explore the live site
    - href: https://github.com/GTBitsOfGood/bog-web
      text: Scour the Svelt-ified codebase
  timeframe: May 2019 - present
  tags:
    - JAMstack
    - SvelteJS
- title: Dolphin Audio Visualizer
...

And access that data across any template:

// home.pug
.project-carousel
  each work in works
    h3 #{title}
    p #{timeframe}
    each tag in tags
    ...

Coming from the world of “clientside rendering” with create-react-app, this was a pretty big revelation. No more sending API keys or big JSON blobs to the browser. 😁

I also added some goodies for JavaScript fetching and animation improvements over version 1 of my site. If you’re curious, here’s where my README stood at this point.

I Was Happy At This Point But Something Was Missing

I went surprisingly far by abandoning JS-based components and embracing templates (with animated page transitions to boot). But I know this won’t satisfy my needs forever. Remember that great divide I kicked us off with? Well, there’s clearly still that ravine between my build setup (firmly in camp #1) and the haven of JS-ified interactivity (the Next, SvelteKit, and more of camp #2). Say I want to add:

  • a pop-up modal with an open/close toggle,
  • a component-based design system like Material UI, complete with scoped styling,
  • a complex multi-step form, maybe driven by a state machine.

If you’re a plain-JS-purist, you probably have framework-less answers to all those use cases. 😉 But there’s a reason JQuery isn’t the norm anymore! There’s something appealing about creating discrete, easy-to-read components of HTML, scoped styles, and pieces of JavaScript “state” variables. React, Vue, Svelte, etc. offer so many niceties for debugging and testing that straight DOM manipulation can’t quite match.

So here’s my million dollar question: can we use straight HTML templates to start, and gradually add React / Vue / Svelte components where we want them?

The answer… is yes. Let’s try it.

11ty + Vite: A Match Made In Heaven ❤️

Here’s the dream that I’m imagining here. Wherever I want to insert something interactive, I want to leave a little flag in my template to “put X React component here.” This could be the shortcode syntax that 11ty supports:

# Super interesting programming tutorial

Writing paragraphs has been fun, but that's no way to learn. Time for an interactive code example!

{% react './components/FancyLiveDemo.jsx' %}

But remember, the one-piece 11ty (purposely) avoids: a way to bundle all your JavaScript. Coming from the OG guild of bundling, your brain probably jumps to building Webpack, Rollup, or Babel processes here. Build a big ole entry point file, and output some beautiful optimized code right?

Well yes, but this can get pretty involved. If we’re using React components, for instance, we’ll probably need some loaders for JSX, a fancy Babel process to transform everything, an interpreter for SASS and CSS module imports, something to help with live reloading, and so on.

If only there were a tool that could just see our .jsx files and know exactly what to do with them.

Enter: Vite

Vite’s been the talk of the town as of late. It’s meant to be the all-in-one tool for building just about anything in JavaScript. Here’s an example for you to try at home. Let’s make an empty directory somewhere on our machine and install some dependencies:

npm init -y # Make a new package.json with defaults set
npm i vite react react-dom # Grab Vite + some dependencies to use React

Now, we can make an index.html file to serve as our app’s “entry point.” We’ll keep it pretty simple:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h1>Hello Vite! (wait is it pronounced "veet" or "vight"...)</h1>
  <div id="root"></div>
</body>
</html>

The only interesting bit is that div id="root" in the middle. This will be the root of our React component in a moment!

If you want, you can fire up the Vite server to see our plain HTML file in your browser. Just run vite (or npx vite if the command didn’t get configured in your terminal), and you’ll see this helpful output:

vite vX.X.X dev server running at:

> Local: http://localhost:3000/
> Network: use `--host` to expose

ready in Xms.

Much like Browsersync or other popular dev servers, the name of each .html file corresponds to a route on our server. So if we renamed index.html to about.html, we would visit http://localhost:3000/about/ (yes, you’ll need a trailing slash!)

Now let’s do something interesting. Alongside that index.html file, add a basic React component of some sort. We’ll use React’s useState here to demonstrate interactivity:

// TimesWeMispronouncedVite.jsx
import React from 'react'

export default function TimesWeMispronouncedVite() {
  const [count, setCount] = React.useState(0)
  return (
    <div>
      <p>I've said Vite wrong {count} times today</p>
      <button onClick={() => setCount(count + 1)}>Add one</button>
    </div>
  )
}

Now, let’s load that component onto our page. This is all we have to add to our index.html:

<!DOCTYPE html>
...
<body>
  <h1>Hello Vite! (wait is it pronounced "veet" or "vight"...)</h1>
  <div id="root"></div>
  <!--Don't forget type="module"! This lets us use ES import syntax in the browser-->
  <script type="module">
    // path to our component. Note we still use .jsx here!
    import Component from './TimesWeMispronouncedVite.jsx';
    import React from 'react';
    import ReactDOM from 'react-dom';
    const componentRoot = document.getElementById('root');
    ReactDOM.render(React.createElement(Component), componentRoot);
  </script>
</body>
</html>

Yep, that’s it. No need to transform our .jsx file to a browser-ready .js file ourselves! Wherever Vite sees a .jsx import, it’ll auto-convert that file to something browsers can understand. There isn’t even a dist or build folder when working in development; Vite processes everything on the fly — complete with hot module reloading every time we save our changes. 🤯

Okay, so we have an incredibly capable build tool. How can we bring this to our 11ty templates?

Running Vite Alongside 11ty

Before we jump into the good stuff, let’s discuss running 11ty and Vite side-by-side. Go ahead and install 11ty as a dev dependency into the same project directory from last section:

npm i -D @11ty/eleventy # yes, it really is 11ty twice

Now let’s do a little pre-flight check to see if 11ty’s working. To avoid any confusion, I’d suggest you:

  1. Delete that index.html file from earlier;
  2. Move that TimesWeMispronouncedVite.jsx inside a new directory. Say, components/;
  3. Create a src folder for our website to live in;
  4. Add a template to that src directory for 11ty to process.

For example, a blog-post.md file with the following contents:

# Hello world! It’s markdown here

Your project structure should look something like this:

src/
  blog-post.md
components/
  TimesWeMispronouncedVite.jsx

Now, run 11ty from your terminal like so:

npx eleventy --input=src

If all goes well, you should see an build output like this:

_site/
  blog-post/
    index.html

Where _site is our default output directory, and blog-post/index.html is our markdown file beautifully converted for browsing.

Normally, we’d run npx eleventy --serve to spin up a dev server and visit that /blog-post page. But we’re using Vite for our dev server now! The goal here is to:

  1. Have eleventy build our markdown, Pug, nunjucks, and more to the _site directory.
  2. Point Vite at that same _site directory so it can process the React components, fancy style imports, and other things that 11ty didn’t pick up.

So a two-step build process, with 11ty handing off the Vite. Here’s the CLI command you’ll need to start 11ty and Vite in “watch” mode simultaneously:

(npx eleventy --input=src --watch) & npx vite _site

You can also run these commands in two separate terminals for easier debugging. 😄

With any luck, you should be able to visit http://localhost:3000/blog-post/ (again, don’t forget the trailing slash!) to see that processed Markdown file.

Partial Hydration With Shortcodes

Let’s do a brief rundown on shortcodes. Time to revisit that syntax from earlier:

{% react '/components/TimesWeMispronouncedVite.jsx' %}

For those unfamiliar with shortcodes: they’re about the same as a function call, where the function returns a string of HTML to slide into your page. The “anatomy” of our shortcode is:

  • {% … %}
    Wrapper denoting the start and end of the shortcode.
  • react
    The name of our shortcode function we’ll configure in a moment.
  • '/components/TimesWeMispronouncedVite.jsx'
    The first (and only) argument to our shortcode function. You can have as many arguments as you’d like.

Let’s wire up our first shortcode! Add a .eleventy.js file to the base of your project, and add this config entry for our react shortcode:

// .eleventy.js, at the base of the project
module.exports = function(eleventyConfig) {
  eleventyConfig.addShortcode('react', function(componentPath) {
   // return any valid HTML to insert
   return `<div id="root">This is where we'll import ${componentPath}</div>`
  })

  return {
    dir: {
      // so we don't have to write `--input=src` in our terminal every time!
      input: 'src',
    }
  }
}

Now, let’s spice up our blog-post.md with our new shortcode. Paste this content into our markdown file:

# Super interesting programming tutorial

Writing paragraphs has been fun, but that's no way to learn. Time for an interactive code example!

{% react '/components/TimesWeMispronouncedVite.jsx' %}

And if you run a quick npx eleventy, you should see this output in your _site directory under /blog-post/index.html:

<h1>Super interesting programming tutorial</h1>

<p>Writing paragraphs has been fun, but that's no way to learn. Time for an interactive code example!</p>

<div id="root">This is where we'll import /components/TimesWeMispronouncedVite.jsx</div>

Writing Our Component Shortcode

Now let’s do something useful with that shortcode. Remember that script tag we wrote while trying out Vite? Well, we can do the same thing in our shortcode! This time we’ll use the componentPath argument to generate the import, but keep the rest pretty much the same:

// .eleventy.js
module.exports = function(eleventyConfig) {
  let idCounter = 0;
  // copy all our /components to the output directory
  // so Vite can find them. Very important step!
  eleventyConfig.addPassthroughCopy('components')

  eleventyConfig.addShortcode('react', function (componentPath) {
      // we'll use idCounter to generate unique IDs for each "root" div
      // this lets us use multiple components / shortcodes on the same page 👍
      idCounter += 1;
      const componentRootId = `component-root-${idCounter}`
      return `
  <div id="${componentRootId}"></div>
  <script type="module">
    // use JSON.stringify to
    // 1) wrap our componentPath in quotes
    // 2) strip any invalid characters. Probably a non-issue, but good to be cautious!
    import Component from ${JSON.stringify(componentPath)};
    import React from 'react';
    import ReactDOM from 'react-dom';
    const componentRoot = document.getElementById('${componentRootId}');
    ReactDOM.render(React.createElement(Component), componentRoot);
  </script>
      `
    })

  eleventyConfig.on('beforeBuild', function () {
    // reset the counter for each new build
    // otherwise, it'll count up higher and higher on every live reload
    idCounter = 0;
  })

  return {
    dir: {
      input: 'src',
    }
  }
}

Now, a call to our shortcode (ex. {% react '/components/TimesWeMispronouncedVite.jsx' %}) should output something like this:

<div id="component-root-1"></div>
<script type="module">
    import Component from './components/FancyLiveDemo.jsx';
    import React from 'react';
    import ReactDOM from 'react-dom';
    const componentRoot = document.getElementById('component-root-1');
    ReactDOM.render(React.createElement(Component), componentRoot);
</script>

Visiting our dev server using (npx eleventy --watch) & vite _site, we should find a beautifully clickable counter element. ✨

Buzzword Alert — Partial Hydration And Islands Architecture

We just demonstrated “islands architecture” in its simplest form. This is the idea that our interactive component trees don’t have to consume the entire website. Instead, we can spin up mini-trees, or “islands,” throughout our app depending on where we actually need that interactivity. Have a basic landing page of links without any state to manage? Great! No need for interactive components. But do you have a multi-step form that could benefit from X React library? No problem. Use techniques like that react shortcode to spin up a Form.jsx island.

This goes hand-in-hand with the idea of “partial hydration.” You’ve likely heard the term “hydration” if you work with component-y SSGs like NextJS or Gatsby. In short, it’s a way to:

  1. Render your components to static HTML first.
    This gives the user something to view when they initially visit your website.
  2. “Hydrate” this HTML with interactivity.
    This is where we hook up our state hooks and renderers to, well, make button clicks actually trigger something.

This 1-2 punch makes JS-driven frameworks viable for static sites. As long as the user has something to view before your JavaScript is done parsing, you’ll get a decent score on those lighthouse metrics.

Well, until you don’t. 😢 It can be expensive to “hydrate” an entire website since you’ll need a JavaScript bundle ready to process every last DOM element. But our scrappy shortcode technique doesn’t cover the entire page! Instead, we “partially” hydrate the content that’s there, inserting components only where necessary.

Don’t Worry, There’s A Plugin For All This — Slinkity

Let’s recap what we discovered here:

  1. Vite is an incredibly capable bundler that can process most file types (jsx, vue, and svelte to name a few) without extra config.
  2. Shortcodes are an easy way to insert chunks of HTML into our templates, component-style.
  3. We can use shortcodes to render dynamic, interactive JS bundles wherever we want using partial hydration.

So what about optimized production builds? Properly loading scoped styles? Heck, using .jsx to create entire pages? Well, I’ve bundled all of this (and a whole lot more!) into a project called Slinkity. I’m excited to see the warm community reception to the project, and I’d love for you, dear reader, to give it a spin yourself!

🚀 Try the quick start guide

Astro’s Pretty Great Too

Readers with their eyes on cutting-edge tech probably thought about Astro at least once by now. 😉 And I can’t blame you! It’s built with a pretty similar goal in mind: start with plain HTML, and insert stateful components wherever you need them. Heck, they’ll even let you start writing React components inside Vue or Svelte components inside HTML template files! It’s like MDX Xtreme edition. 🤯

There’s one pretty major cost to their approach though: you need to rewrite your app from scratch. This means a new template format based on JSX (which you might not be comfortable with), a whole new data pipeline that’s missing a couple of niceties right now, and general bugginess as they work out the kinks.

But spinning up an 11ty + Vite cocktail with a tool like Slinkity? Well, if you already have an 11ty site, Vite should bolt into place without any rewrites, and shortcodes should cover many of the same use cases as .astro files. I’ll admit it’s far from perfect right now. But hey, it’s been useful so far, and I think it’s a pretty strong alternative if you want to avoid site-wide rewrites!

Wrapping Up

This Slinkity experiment has served my needs pretty well so far (and a few of y’all’s too!). Feel free to use whatever stack works for your JAM. I’m just excited to share the results of my year of build tool debauchery, and I’m so pumped to see how we can bridge the great Jamstack divide.

Further Reading

Want to dive deeper into partial hydration, or ESM, or SSGs in general? Check these out:

  • Islands Architecture
    This blog post from Jason Format really kicked off a discussion of “islands” and “partial hydration” in web development. It’s chock-full of useful diagrams and the philosophy behind the idea.
  • Simplify your static with a custom-made static site generator
    Another SmashingMag article that walks you through crafting Node-based website builders from scratch. It was a huge inspiration to me!
  • How ES Modules have redefined web development
    A personal post on how ES Modules have changed the web development game. This dives a little further into the “then and now” of import syntax on the web.
  • An introduction to web components
    An excellent walkthrough on what web components are, how the shadow DOM works, and where web components prove useful. Used this guide to apply custom components to my own framework!

Building An API With Gatsby Functions

You’ve probably heard about Serverless Functions, but if you haven’t, Serverless Functions provide functionality typically associated with server-side technologies that can be implemented alongside front-end code without getting caught up in server-side infrastructures.

With server-side and client-side code coexisting in the same code base, front-end developers like myself can extend the reach of what’s possible using the tools they already know and love.

Limitations

Coexistence is great but there are at least two scenarios I’ve encountered where using Serverless Functions in this way weren’t quite the right fit for the task at hand. They are as follows:

  1. The front end couldn’t support Serverless Functions.
  2. The same functionality was required by more than one front end.

To help provide some context here’s one example of both points 1 and 2 named above. I maintain an Open-source project called MDX Embed, you’ll see from the docs site that it’s not a Gatsby website. It’s been built using Storybook and Storybook on its own provides no Serverless Function capabilities. I wanted to implement “Pay what you want” contributions to help fund this project and I wanted to use Stripe to enable secure payments but without a secure “backend” This would not have been possible.

By abstracting this functionality away into an API built with Gatsby Functions I was able to achieve what I wanted with MDX Embed and also re-use the same functionality and enable “Pay what you want” functionality for my blog.

You can read more about how I did that here: Monetize Open-Source Software With Gatsby Functions And Stripe.

It’s at this point that using Gatsby Functions can act as a kind of Back end for front end or BFF 😊 and developing in this way is more akin to developing an API (Application Programming Interface).

APIs are used by front-end code to handle things like, logins, real-time data fetching, or secure tasks that aren’t suitably handled by the browser alone. In this tutorial, I’ll explain how to build an API using Gatsby Functions and deploy it to Gatsby Cloud.

Preflight Checks

Gatsby Functions work when deployed to Gatsby Cloud or Netlify, and in this tutorial, I’ll be explaining how to deploy to Gatsby Cloud so you’ll need to sign up and create a free account first.

You’re also going to need either a GitHub, GitLab or BitBucket account, this is how Gatsby Cloud reads your code and then builds your “site”, or in this case, API.

For the purposes of this tutorial, I’ll be using GitHub. If you’d prefer to jump ahead, the finished demo API code can be found on my GitHub.

Getting Started

Create a new dir somewhere on your local drive and run the following in your terminal. This will set up a default package.json.

npm init -y

Dependencies

Type the following into your terminal to install the required dependencies.

npm install gatsby react react-dom

Pages

It’s likely your API won’t have any “pages” but to avoid seeing Gatsby’s default missing page warning when you visit the root URL in the browser, add the following to both src/pages/index.js and src/pages/404.js.

//src/pages/index.js & src/pages/404.js

export default () => null;

API

Add the following to src/api/my-first-function.js.

I’ll explain a little later what 'Access-Control-Allow-Origin', '*' means, but in short, it makes sure that your APIs from other origins aren’t blocked by CORS.

//src/api/my-first-function.js

export default function handler(req, res) {
  res.setHeader('Access-Control-Allow-Origin', '*');

  res.status(200).json({ message: 'A ok!' });
}

Scripts

Add the following to package.json.

//package.json

...
  "scripts": {
    "develop": "gatsby develop",
    "build": "gatsby build"
  },
...

Start The Gatsby Development Server

To spin up the Gatsby development server run the following in your terminal.

npm run develop

Make A Request From The Browser

With the Gatsby’s development server running you can visit http://localhost:8000/api/my-first-function, and since this is a simple GET request you should see the following in your browser.

{
  "message": "A ok!"
}

Congratulations 🎉

You’ve just developed an API using Gatsby Functions.

Deploy

If you are seeing the above response in your browser it’s safe to assume your function is working correctly locally, in the following steps I’ll explain how to deploy your API to Gatsby Cloud and access it using an HTTP request from CodeSandbox.

Push Code To Git

Before attempting to deploy to Gatsby Cloud you’ll need to have pushed your code to your Git provider of choice.

Gatsby Cloud

Log into your Gatsby Cloud account and look for the big purple button that says “Add site +”.

In the next step, you’ll be asked to either Import from a Git repository or Start from a Template, select Import from Git Repository and hit next.

As mentioned above Gatsby Cloud can connect to either GitHub, GitLab or Bitbucket. Select your preferred Git provider and hit next.

With your Git provider connected, you can search for your repository, and give your site a name.

Once you’ve selected your repository and named your site hit next.

You can skip the “Integrations” and “Setup” as we won’t be needing these.

If all has gone to plan your should be seeing something similar to the below screenshot.

You’ll see near the top on the left-hand side of the screen a URL that ends with gatsbyjs.io, this will be the URL for your API and any functions you create can be accessed by adding /api/name-of-function to the end of this URL.

E.g, the complete deployed version of my-first-function.js for my demo API is as follows:

Demo API: My First Function.

Testing Your API

Visiting the URL of your API is one thing but it’s not really how APIs are typically used. Ideally to test your API you need to make a request to the function from a completely unrelated origin.

It’s here where res.setHeader('Access-Control-Allow-Origin', '*'); comes to the rescue. Whilst it’s not always desirable to allow any domain (website) to access your functions, for the most part, public functions are just that, public. Setting the Access Control header to a value of * means any domain can access your function, without this, any domain other than the domain the API is hosted on will be blocked by CORS.

Here’s a CodeSandbox that uses my-first-function from my demo API. You can fork this and change the Axios request URL to test your function.

CodeSandbox: My First Function

Getting Fancier

Sending a response from your API that says message: "A ok!" isn’t exactly exciting so in the next bit I’ll show you how to query the GitHub REST API and make a personal profile card to display on your own site using the API you just created, and it’ll look a little like this.

CodeSandbox: Demo profile card

Dependencies

To use the GitHub REST API you’ll need to install @octokit/rest package.

npm install @octokit/rest

Get GitHub User Raw

Add the following to src/api/get-github-user-raw.js.

// src/api/get-github-user-raw.js

import { Octokit } from '@octokit/rest';

const octokit = new Octokit({
  auth: process.env.OCTOKIT_PERSONAL_ACCESS_TOKEN
});

export default async function handler(req, res) {
  res.setHeader('Access-Control-Allow-Origin', '*');

  try {
    const { data } = await octokit.request(`GET /users/{username}`, {
      username: 'PaulieScanlon'
    });

    res.status(200).json({ message: 'A ok!', user: data });
  } catch (error) {
    res.status(500).json({ message: 'Error!' });
  }
}

Access Token

To communicate with the GitHub REST API you’ll need an access token. You can get this by following the steps in this guide from GitHub: Creating A Personal Access Token.

.env Variables

To keep your access token secure add the following to .env.development and .env.production.

OCTOKIT_PERSONAL_ACCESS_TOKEN=123YourAccessTokenABC

You can read more about Gatsby environment variables in this guide from Gatsby: Environment Variables.

Start Development Server

As you did before start the Gatsby development server by typing the following in your terminal.

npm run develop

Make A Request From The Browser

With the Gatsby development server running you can visit http://localhost:8000/api/get-github-user-raw, and since this too is a simple GET request you should see the following in your browser. (I’ve removed part of the response for brevity.)

{
  "message": "A ok!",
  "user": {
    "login": "PaulieScanlon",
    "id": 1465706,
    "node_id": "MDQ6VXNlcjE0NjU3MDY=",
    "avatar_url": "https://avatars.githubusercontent.com/u/1465706?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/PaulieScanlon",
    "type": "User",
    "site_admin": false,
    "name": "Paul Scanlon",
    "company": "Paulie Scanlon Ltd.",
    "blog": "https://www.paulie.dev",
    "location": "Worthing",
    "email": "pauliescanlon@gmail.com",
    "hireable": true,
    "bio": "Jamstack Developer / Technical Content Writer (freelance)",
    "twitter_username": "pauliescanlon",
    "created_at": "2012-02-23T13:43:26Z",
    "two_factor_authentication": true,
    ...
  }
}

Here’s a CodeSandbox example of the full raw response.

CodeSandbox: Raw Response

You’ll see from the above that there’s quite a lot of data returned that I don’t really need, this next bit is completely up to you as it’s your API but I have found it helpful to manipulate the GitHub API response a little bit before sending it back to my frontend code.

If you’d like to do the same you could create a new function and add the following to src/api/get-github-user.js.

// src/api/get-github-user.js

import { Octokit } from '@octokit/rest';

const octokit = new Octokit({
  auth: process.env.OCTOKIT_PERSONAL_ACCESS_TOKEN
});

export default async function handler(req, res) {
  res.setHeader('Access-Control-Allow-Origin', '*');

  try {
    const { data } = await octokit.request(`GET /users/{username}`, {
      username: 'PaulieScanlon'
    });

    res.status(200).json({
      message: 'A ok!',
      user: {
        name: data.name,
        blog_url: data.blog,
        bio: data.bio,
        photo: data.avatar_url,
        githubUsername: `@${data.login}`,
        githubUrl: data.html_url,
        twitterUsername: `@${data.twitter_username}`,
        twitterUrl: `https://twitter.com/${data.twitter_username}`
      }
    });
  } catch (error) {
    res.status(500).json({ message: 'Error!' });
  }
}

You’ll see from the above that rather than returning the complete data object returned by the GitHub REST API I pick out just the bits I need, rename them and add a few bits before the username and URL values. This makes life a bit easier when you come to render the data in the frontend code.

Here’s a CodeSandbox example of the formatted response.

CodeSandbox: Formatted Response

This is very similar to the Profile Card CodeSandbox from earlier, but I’ve also printed the data out so you can see how each manipulated data item is used.

It’s worth noting at this point that all four of the CodeSandbox demos in this tutorial are using the demo API, and none of them are built using Gatsby or hosted on Gatsby Cloud — cool ay!

.env Variables In Gatsby Cloud

Before you deploy your two new functions you’ll need to add the GitHub Access token to the environment variables section in Gatsby Cloud.

Where To Go From Here?

I asked myself this very question. Typically speaking serverless functions are used in client-side requests and whilst that’s fine I wondered if they could also be used at build time to statically “bake” data into a page rather than relying on JavaScript which may or may not be disabled in the user’s browser.

…so that’s exactly what I did.

Here’s a kind of data dashboard that uses data returned by Gatsby Functions at both run and build time. I built this site using Astro and deployed it GitHub Pages.

The reason I think this is a great approach is because I’m able to re-use the same functionality on both the server and in the browser without duplicating anything.

In this Astro build I hit the same endpoint exposed by my API to return data that is then either baked into the page (great for SEO) or fetched at run time by the browser (great for showing fresh or up to the minute live data).

Data Dashboard

The data displayed on the left of the site is requested at build time and baked into the page with Astro. The data on the right of the page is requested at runtime using a client-side request. I’ve used slightly different endpoints exposed by the GitHub REST API to query different GitHub user accounts which create the different lists.

Everything you see on this site is provided by my more complete API. I’ve called it: Paulie API and I use it for a number of my websites.

Paulie API

Paulie API like the API from this tutorial is built with Gatsby but because Gatsby can act as both a site and an API I’ve used it to document how all my functions work and each endpoint has its own page that can be used as an interactive playground… feel free to have a look around.

So, there you have it, A Gatsby Functions API that can be used by any client-side or server-side code, from any website built with any tech stack. 🤯

Give it a go and I’d be very interested to see what you build. Feel free to share in the comments below or come find me on Twitter: @PaulieScanlon.

Smashing Podcast Episode 42 With Jeff Smith: What Is DevOps?

In this episode, we’re talking about DevOps. What is it, and is it a string to add to your web development bow? Drew McLellan talks to expert Jeff Smith to find out.

Show Notes

Weekly Update

Transcript

Drew McLellan: He’s a DevOps practitioner that focuses on attainable levels of DevOps implementations, regardless of where you are in your journey. He’s director of production operations at digital advertising platform Centro, as well as being a public speaker, sharing his DevOps knowledge with audiences all around the globe. He’s the author of the book, Operations Anti-Patterns, DevOps Solutions for Manning Publishing, which shows how to implement DevOps techniques in the kind of imperfect environments most developers work in. So we know he’s an expert in DevOps, but did you know George Clooney regards him as the best paper airplane maker of a generation? My Smashing friends, please welcome Jeff Smith. Hi Jeff. How are you?

Jeff Smith: I’m smashing, Drew, how you doing?

Drew: I’m good. Thank you. That’s good to hear. So I wanted to talk to you today about the subject of DevOps, which is one of your main key area. Many of our listeners will be involved in web and app development, but maybe only have a loose familiarity with what happens on the operations side of things. I know those of us who might work in larger companies will have whole teams of colleagues who are doing ops. We’re just thankful that whatever it is they do, they’re doing it well. But we hear DevOps mentioned more and more, and it feels like one of those things that as developers, we should really understand. So Jeff, what is DevOps?

Jeff: So if you ask 20 people what DevOps is, you might get 20 different answers. So I will give you my take on it, all right, and know that if you’re at a conference and you mention this, you could get into a fist fight with someone. But for me, DevOps is really about that relationship between, and we focus on dev and ops, but really that inter team relationship and how we go about structuring our work and more importantly, structuring our goals and incentives to make sure that they’re aligned so that we are working towards a common goal. And a lot of the core ideas and concepts from DevOps come from the old world where dev and ops were always adversarial, where there was this constant conflict. And when you think about it, it’s because of the way those two teams are incentivized. One team is incentivized to push changes. Another team is incentivized to keep stability, which means fewer changes.

Jeff: When you do that, you create this inherent conflict and everything spills out from there. So DevOps is really about aligning those teams and goals so that we are working towards a common strategy, but then also adopting practices from both sides, so that dev understands more about ops and ops understands more about dev, as a way to gain and share empathy with each other so that we understand the perspective of where the other person is coming from.

Jeff: But then also to enhance our work. Because again, if I understand your perspective and take that into account in my work, it’s going to be a lot more beneficial for each of us. And there’s a lot that ops can learn from developers in terms of automation and how we go about approaching things so that they’re easily reproducible. So it’s this blending and skills. And what you’re seeing now is that this applies to different group combinations, so you’re hearing things like DevSecOps, DevSecFinOps, DevSecFinHROps. It’s just going to keep growing and growing and growing. So it’s really a lesson that we can stamp out across the organization.

Drew: So it’s taking some of the concepts that we understand as developers and spreading our ideas further into the organization, and at the same time learning what we can from the operations to try and move everyone forward.

Jeff: Absolutely, yes. And another aspect of ops, and you had mentioned it a little bit in the intro, is we think it’s just for these larger organizations with dedicated ops teams and things like that, but one thing to think about is ops is happening in your organization, regardless of the size. It’s just a matter of it’s you doing it, or if there’s a separate team doing it, but somehow you’re deploying code. Somehow you’ve got a server out there running somewhere. So ops exist somewhere in your organization, regardless of the size. The question is, who is doing it? And if it’s a single person or a single group then DevOps might even be even more particularly salient for you, as you need to understand the types of things that ops does.

Drew: As professional developers, how important do you think it is for us to have a good grasp of what DevOps is and what it means to implement?

Jeff: I think it’s super important, especially at this phase of the DevOps journey. And the reason I think it’s important is that one, I think we’re always more efficient, again, when we understand what our counterparts are doing. But the other thing is to be able to take operational concerns into account during your design development and implementation of any technology. So one thing that I’ve learned in my career is that even though I thought developers were masters of the universe and understood everything that had to do with computers, turns out that’s not actually the case. Turns out there’s a lot of things that they outsource to ops in terms of understanding, and sometimes that results in particular design choices or implementation choices that may not be optimal for a production deployment.

Jeff: They might be fine in development and testing and things like that, but once you get to production, it’s a little bit of a different ballgame. So not to say that they need to own that entire set of expertise, but they at least need to know enough to know what they don’t know. So they know when to engage ops early, because that’s a common pattern that we see is development makes a choice. I won’t even say make a choice because they’re not even cognizant that it’s a choice, but there’s something that happens that leads to a suboptimal decision for ops and development was completely unaware. So just having a bit more knowledge about ops, even if it’s just enough to say, maybe we should bring ops in on this to get their perspective before we go moving forward. That could save a lot of time and energy and stability, obviously, as it relates to whatever products you’re releasing.

Drew: I see so many parallels with the way that you’re talking about the relationship between dev and ops as we have between design and dev, where you’ve got designers working on maybe how an interface works and looks and having a good understanding of how that’s actually going to be built in the development role, and bringing developers in to consult can really improve the overall solution just by having that clear communication and an understanding of what each other does. Seems like it’s that same principle played out with DevOps, which is really, really good to hear.

Drew: When I think of the things I hear about DevOps, I hear terms like Kubernetes, Docker, Jenkins, CircleCI. I’ve been hearing about Kubernetes for years. I still don’t have any idea what it is, but from what you’re saying, it seems that DevOps isn’t just about … We’re not just talking about tools here, are we? But more about processes and ways of communicating on workflows, is that right?

Jeff: Absolutely. So my mantra for the last 20 years has always been people process tools. You get people to buy into the vision. From there, you define whatever your process is going to look like to achieve that vision. And then you bring on tools that are going to model whatever your process is. So I always put tools at the tail end of the DevOps conversation, mainly because if you don’t have that buy-in, then it doesn’t matter. I could come up with the greatest continuous deployment pipeline ever, but if people aren’t bought into the idea of shipping every change straight to production, it doesn’t matter, right? What good is the tool? So those tools are definitely part of the conversation, only because they’re a standardized way to meet some common goals that we’ve defined.

Jeff: But you’ve got to make sure that those goals that are being defined make sense for your organization. Maybe continuous deployment doesn’t make sense for you. Maybe you don’t want to ship every single change the minute it comes out. And there are plenty of companies and organizations and reasons why you wouldn’t want to do that. So maybe something like a continuous deployment pipeline doesn’t make sense for you. So while the tools are important, it’s more important to focus on what it is that’s going to deliver value for your organization, and then model and implement the tools that are necessary to achieve that.

Jeff: But don’t go online and find out what everyone’s doing and be like, oh, well, if we’re going to do DevOps, we got to switch to Docker and Kubernetes because that’s the tool chain. No, that’s not it. You may not need those things. Not everyone is Google. Not everyone is Netflix. Stop reading posts from Netflix and Google. Please just stop reading them. Because it gets people all excited and they’re like, well this is what we got to do. And it’s like, well, they’re solving very different problems than the problems that you have.

Drew: So if say I’m starting a new project, maybe I’m a startup business, creating software as a service product. I’ve got three developers, I’ve got an empty Git repo and I’ve got dreams of IPOs. To be all in on a DevOps approach to building this product, what are the names of the building blocks that I should have in place in terms of people and processes and where do I start?

Jeff: So in your specific example, the first place I would start with is punting on most of it as much as possible and using something like Heroku or something to that effect. Because you get so excited about all this AWS stuff, Docker stuff, and in reality, it’s so hard just to build a successful product. The idea that you are focusing on the DevOps portion of it is like, well I would say outsource as much of that stuff as possible until it actually becomes a pain point. But if you’re at that point where you’re saying okay, we’re ready to take this stuff in house and we’re ready to take it to the next level. I would say the first place to start is, where are your pain points? what are the things that are causing you problems?

Jeff: So for some people it’s as simple as automated testing. The idea that hey, we need to run tests every time someone makes a commit, because sometimes we’re shipping stuff that’s getting caught by unit tests that we’ve already written. So then maybe you start with continuous integration. Maybe your deployments are taking hours to complete and they’re very manual, then that’s where you focus and you say like, okay, what automation do we need to be able to make this a one button click affair? But I hate to prescribe a general, this is where you start, just because your particular situation and your particular pain points are going to be different. And the thing is, if it’s a pain point, it should be shouting at you. It should be absolutely shouting at you.

Jeff: It should be one of those things where someone says, oh, what sucks in your organization? And it should be like, oh, I know exactly what that is. So when you approach it from that perspective, I think the next steps become pretty apparent to you in terms of what in the DevOps toolbox you need to unpack and start working with. And then it becomes these minimal incremental changes that just keep coming and you notice that as you get new capabilities, your appetite for substandard stuff becomes very small. So you go from like, oh yeah, deploys take three hours and that’s okay. You put some effort into it and next thing you know, in three weeks, you’re like, man, I cannot believe the deployment is still taking 30 minutes. How do we get this down from 30 minutes? Your appetite becomes insatiable for improvement. So things just sort of spill out from there.

Drew: I’ve been reading your recent book and that highlights what you call the four pillars of DevOps. And none of them is tools, as mentioned, but there are these four main areas of focus, if you like, for DevOps. I noticed that the first one of those is culture, I was quite surprised by that, firstly, because I was expecting you to be talking about tools more and we now understand why, but when it comes to culture, it just seems like a strange thing to have at the beginning. There’s a foundation for a technical approach. How does the culture affect how successful DevOps implementation can be within an organization?

Drew: … how successful DevOps implementation can be within an organization.

Jeff: Culture is really the bedrock of everything when you think about it. And it’s important because culture, and we get into this a little bit deeper in the book, but culture really sets the stage for norms within the organization. Right. You’ve probably been at a company where, if you submitted a PR with no automated testing, that’s not a big thing. People accept it and move on.

Jeff: But then there’s other orgs where that is a cardinal sin. Right. Where if you’ve done that, it’s like, “Whoa, are you insane? What are you doing? There’s no test cases here.” Right. That’s culture though. That is culture that is enforcing that norm to say like, “This is just not what we do.”

Jeff: Anyone can write a document that says we will have automated test cases, but the culture of the organization is what enforces that mechanism amongst the people. That’s just one small example of why culture is so important. If you have an organization where the culture is a culture of fear, a culture of retribution. It’s like if you make a mistake, right, that is sacrilege. Right. That is tantamount to treason. Right.

Jeff: You create behaviors in that organization that are adverse to anything that could be risky or potentially fail. And that ends up leaving a lot of opportunity on the table. Whereas if you create a culture that embraces learning from failure, embraces this idea of psychological safety, where people can experiment. And if they’re wrong, they can figure out how to fail safely and try again. You get a culture of experimentation. You get an organization where people are open to new ideas.

Jeff: I think we’ve all been at those companies where it’s like, “Well, this is just the way it’s done. And no one changes that.” Right. You don’t want that because the world is constantly changing. That’s why we put culture front and center, because a lot of the behaviors within an organization exist because of the culture that exists.

Jeff: And the thing is, cultural actors can be for good or ill. Right. What’s ironic, and we talk about this in the book too, is it doesn’t take as many people as you think to change the organizational culture. Right. Because most people, there’s detractors, and then there’s supporters, and then there’s fence sitters when it comes to any sort of change. And most people are fence sitters. Right. It only takes a handful of supporters to really tip the scales. But in the same sense, it really only takes a handful of detractors to tip the scales either.

Jeff: It’s like, it doesn’t take much to change the culture for the better. And if you put that energy into it, even without being a senior leader, you can really influence the culture of your team, which then ends up influencing the culture of your department, which then ends up influencing the culture of the organization.

Jeff: You can make these cultural changes as an individual contributor, just by espousing these ideas and these behaviors loudly and saying, “These are the benefits that we’re getting out of this.” That’s why I think culture has to be front and fore because you got to get everyone bought into this idea and they have to understand that, as an organization, it’s going to be worthwhile and support it.

Drew: Yeah. It’s got to be a way of life, I guess.

Jeff: Exactly.

Drew: Yeah. I’m really interested in the area of automation because through my career, I’ve never seen some automation that’s been put in place that hasn’t been of benefit. Right. I mean, apart from the odd thing maybe where something’s automated and it goes wrong. Generally, when you take the time to sit down and automate something you’ve been doing manually, it always saves you time and it saves you headspace, and it’s just a weight off your shoulders.

Drew: In taking a DevOps approach, what sort of things would you look to automate within your workflows? And what gains would you expect to see from that over completing things manually?

Jeff: When it comes to automation, to your point, very seldom is there a time where automation hasn’t made life better. Right. The rub that people encounter is finding the time to build that automation. Right. And usually, at my current job, for us it’s actually the point of the request. Right. Because at some point you have to say, “I’m going to stop doing this manually and I’m going to automate it.”

Jeff: And it may have to be the time you get a request where you say, “You know what? This is going to take two weeks. I know we normally turn it around in a couple of hours, but it’s going to take two weeks because this is the request that gets automated.” In terms of identifying what you automate. At Central, I use the process where basically, I would sample all of the different types of requests that came in over a four week period, let’s say. And I would categorize them as planned work, unplanned work, value add work, toil work. Toil being work that’s not really useful, but for some reason, my organization has to do it.

Jeff: And then identifying those things that are like, “Okay, what is the low hanging fruit that we can just get rid of if we were to automate this? What can we do to just simplify this?” And some of the criteria was the risk of the process. Right. Automated database failovers are a little scary because you don’t do them that often. And infrastructure changes. Right. We say, “How often are we doing this thing?” If we’re doing it once a year, it may not be worth automating because there’s very little value in it. But if it’s one of those things that we’re getting two, three times a month, okay, let’s take a look at that. All right.

Jeff: Now, what are the things that we can do to speed this up? And the thing is, when we talk about automation, we instantly jumped to, “I’m going to click a button and this thing’s just going to be magically done.” Right. But there are so many different steps that you can do in automation if you feel queasy. Right. For example, let’s say you’ve got 10 steps with 10 different CLI commands that you would normally run. Your first step of automation could be as simple as, run that command, or at least show that command. Right. Say, “Hey, this is what I’m going to execute. Do you think it’s okay?” “Yes.” “Okay. This is the result I got. Is it okay for me to proceed?” “Yes.” “Okay. This is the result I got.” Right.

Jeff: That way you’ve still got a bit of control. You feel comfortable. And then after 20 executions, you realize you’re just hitting, yes, yes, yes, yes, yes, yes. You say, “All right. Let’s chain all these things together and just make it all one.” It’s not like you’ve got to jump into the deep end of, click it and forget it right off the rip. You can step into this until you feel comfortable.

Jeff: Those are the types of things that we did as part of our automation effort was simply, how do we speed up the turnaround time of this and reduce the level of effort on our part? It may not be 100% day one, but the goal is always to get to 100%. We’ll start with small chunks that we’ll automate parts of it that we feel comfortable with. Yes. We feel super confident that this is going to work. This part we’re a little dicey on, so maybe we’ll just get some human verification before we proceed.

Jeff: The other thing that we looked at in terms of we talk about automation, but is what value are we adding to a particular process? And this is particularly salient for ops. Because a lot of times ops serves as the middleman for a process. Then their involvement is nothing more than some access thing. Right. It’s like, well, ops has to do it because ops is the only person that has access.

Jeff: Well, it’s like, well, how do we outsource that access so that people can do it? Because the reality is, it’s not that we’re worried about developers having production access. Right. We’re worried about developers having unfettered production access. And that’s really a safety thing. Right. It’s like if my toolbox has only sharp knives, I’m going to be very careful about who I give that out to. But if I can mix up the toolbox with a spoon and a hammer so that people can choose the right tool for the job, then it’s a lot easier to loan that out.

Jeff: For example, we had a process where people needed to run ad hoc Ruby scripts in production, for whatever reason. Right. Need to clean up data, need to correct some bad record, whatever. And that would always come through my team. And it’s like, well, we’re not adding any value to this because I can’t approve this ticket. Right. I have no idea. You wrote the software, so what good is it me sitting over your shoulder and going, “Well, I think that’s safe”? Right. I didn’t add any value to typing it in because I’m just typing exactly what you told me to type. Right.

Jeff: And worst case, and at the end of it, I’m really just a roadblock for you because you’re submitting a ticket, then you’re waiting for me to get back from lunch. I’m back from lunch, but I’ve got these other things to work on. We said, “How do we automate this so that we can put this in the hands of developers while at the same time addressing any of these audit concerns that we might have?”

Jeff: We put it in a JIRA workflow, where we had a bot that would automate executing commands that were specified in the JIRA ticket. And then we could specify in the JIRA ticket that it required approval from one of several senior engineers. Right.

Jeff: It makes more sense that an engineer is approving another engineer’s work because they have the context. Right. They don’t have to sit around waiting for ops. The audit piece is answered because we’ve got a clear workflow that’s been defined in JIRA that is being documented as someone approves, as someone requested. And we have automation that is pulling that command and executing that command verbatim in the terminal. Right.

Jeff: You don’t have to worry about me mistyping it. You don’t have to worry about me grabbing the wrong ticket. That increased the turnaround time for those tickets, something like tenfold. Right. Developers are unblocked. My team’s not tied up doing this. And all it really took was a week or two week investment to actually develop the automation and the permissioning necessary to get them access for it.

Jeff: Now we’re completely removed from that. And development is actually able to outsource some of that functionality to lower parts of the organization. They’ve pushed it to customer care. It’s like now when customer care knows that this record needs to be updated for whatever, they don’t need development. They can submit their standard script that we’ve approved for this functionality. And they can run it through the exact same workflow that development does. It’s really a boon all around.

Jeff: And then it allows us to push work lower and lower throughout the organization. Because as we do that, the work becomes cheaper and cheaper because I could have a fancy, expensive developer running this. Right. Or I can have a customer care person who’s working directly with the customer, run it themselves while they’re on the phone with a customer correcting an issue.

Jeff: Automation I think, is key to any organization. And the final point I’ll say on that is, it also allows you to export expertise. Right. Now, I may be the only person that knows how to do this if I needed to do a bunch of commands on the command line. But if I put this in automation, I can give that to anyone. And people know what the end result is, but they don’t need to know all the intermediate steps. I have increased my value tenfold by pushing it out to the organization and taking my expertise and codifying it into something that’s exportable.

Drew: You talked about automating tasks that are occurring frequently. Is there an argument for also automating tasks that happen so infrequently that it takes a developer quite a long time to get back up to speed with how it should work? Because everybody’s forgotten. It’s been so long. It’s been a year, maybe nobody has done it before. Is there an argument for automating those sorts of things too?

Jeff: That’s a tough balancing act. Right. And I always say take it by a case by case basis. And the reason I say that is, one of the mantras in DevOps is if something painful, do it more often. Right. Because the more often you do it, the more muscle memory it becomes and you get to work out and iron out those kinks.

Jeff: The issue that we see with automating very infrequent tasks is that the landscape of the environment tends to change in between executions of that automation. Right. What ends up happening is your code makes particular assumptions about the environment and those assumptions are no longer valid. So the automation ends up breaking anyways.

Drew: And then you’ve got two problems.

Jeff: Right. Right. Exactly. Exactly. And you’re like, “Did I type it wrong? Or is this? No, this thing is actually broke.” So-

Jeff: Typing wrong or is this no, this thing is actually broke. So when it comes to automating infrequent tasks, we really take it by a case by case basis to understand, well, what’s the risk if this doesn’t work, right. If we get it wrong, are we in a bad state or is it just that we haven’t finished this task? So if you can make sure that this would fail gracefully and not have a negative impact, then it’s worth giving a shot in automating it. Because at the very least, then you have a framework of understanding of what should be going on because at the very least, someone’s going to be able to read the code and understand, all right, this is what we were doing. And I don’t understand why this doesn’t work anymore, but I have a clear understanding of what was supposed to happen at least based at design time when this was written.

Jeff: But if you’re ever in a situation where failure could lead to data changes or anything like that, I usually err on the side of caution and keep it manual only because if I have an automation script, if I find some confluence document that’s three years old that says run this script, I tend to have a hundred percent confidence in that script and I execute it. Right. Whereas if it’s a series of manual steps that was documented four years ago, I’m going to be like, I need to do some verification here. Right? Let me step through this a little bit and talk to a few people. And sometimes when we design processes, it’s worthwhile to force that thought process, right? And you have to think about the human component and how they’re going to behave. And sometimes it’s worth making the process a little more cumbersome to force people to think should I be doing this now?

Drew: Are there other ways of identifying what should be automated through sort of monitoring your systems and measuring things? I mean, I think about DevOps and I think about dashboards as one of the things, nice graphs. And I’m sure there’s a lot more to those dashboards than just looking pretty, but it’s always nice to have pretty looking dashboards. Are there ways of measuring what a system’s up to, to help you to make those sorts of decisions?

Jeff: Absolutely. And that sort of segues into the metrics portion of cams, right, is what are the things that we are tracking in our systems to know that they are operating efficiently? And one of the common sort of pitfalls of metrics is we look for errors instead of verifying success. And those are two very different practices, right? So something could flow through the system and not necessarily error out, but not necessarily go through the entire process the way it should. So if we drop a message on a message queue, there should be a corresponding metric that says, “And this message was retrieved and processed,” right? If not, right, you’re going to quickly have an imbalance and the system doesn’t work the way it should. I think we can use metrics as a way to also understand different things that should be automated as we get into those bad states.

Jeff: Right? Because a lot of times it’s a very simple step that needs to be taken to clean things up, right? For people that have been ops for a while, right, the disc space alert, everyone knows about that. Oh, we’re filled up with disc. Oh, we forgot it’s month end and billing ran and billing always fills up the logs. And then VAR log is consuming all the disc space, so we need to run a log rotate. Right? You could get woken up at three in the morning for that, if that’s sort of your preference. But if we sort of know that that’s the behavior, our metrics should be able to give us a clue to that. And we can simply automate the log rotate command, right? Oh, we’ve reached this threshold, execute the log rotate command. Let’s see if the alert clears. If it does, continue on with life. If it doesn’t, then maybe we wake someone up, right.

Jeff: You’re seeing this a lot more with infrastructure automation as well, right, where it’s like, “Hey, are our requests per second are reaching our theoretical maximum. Maybe we need to scale the cluster. Maybe we need to add three or four nodes to the load balancer pool.” And we can do that without necessarily requiring someone to intervene. We can just look at those metrics and take that action and then contract that infrastructure once it goes below a particular threshold, but you got to have those metrics and you got to have those hooks into your monitoring environment to be able to do that. And that’s where the entire metrics portion of the conversation comes in.

Jeff: Plus it’s also good to be able to share that information with other people because once you have data, you can start talking about things in a shared reality, right, because busy is a generic term, but 5,200 requests per second is something much more concrete that we can all reason about. And I think so often when we’re having conversations about capacity or anything, we use these hand-wavy terms, when instead we could be looking at a dashboard and giving very specific values and making sure that everyone has access to those dashboards, that they’re not hidden behind some ops wall that only we have access to for some unknown reason.

Drew: So while sort of monitoring and using metrics as a decision-making tool for the businesses is one aspect of it, it sounds like the primary aspect is having the system monitor itself, perhaps, and to respond maybe with some of these automations as the system as a whole gives itself feedback on onto what’s happening.

Jeff: Absolutely. Feedback loops are a key part of any real system design, right, and understanding the state of the system at any one time. So while it’s easy in the world where everything is working fine, the minute something goes bad, those sorts of dashboards and metrics are invaluable to have, and you’ll quickly be able to identify things that you have not instrumented appropriately. Right. So one of the things that we always talk about in incident management is what questions did you have for the system that couldn’t be answered, right. So what is it, or you’re like, “Oh man, if we only knew how many queries per second were going on right now.” Right.

Jeff: Well, okay. How do we get that for next time? How do we make sure that that’s radiated somewhere? And a lot of times it’s hard when you’re thinking green field to sit down and think of all the data that you might want at any one time. But when you have an incident, it becomes readily apparent what data you wish you had. So it’s important to sort of leverage those incidents and failures and get a better understanding of information that’s missing so that you can improve your incident management process and your metrics and dashboarding.

Drew: One of the problems we sometimes face in development is that teammate members, individual team members hold a lot of knowledge about how a system works and if they leave the company or if they’re out sick or on vacation, that knowledge isn’t accessible to the rest of the team. It seems like the sort of DevOps approach to things is good at capturing a lot of that operational knowledge and building it into systems. So that sort of scenario where an individual has got all the information in their head that doesn’t happen so much. Is that a fair assessment?

Jeff: It is. I think we’ve probably, I think as an industry we might have overstated its efficacy. And the only reason I say that is when our systems are getting so complicated, right? Gone are the days where someone has the entire system in their head and can understand it from beginning to end. Typically, there’s two insidious parts of it. One, people typically focus on one specific area and someone doesn’t have the whole picture, but what’s even more insidious is that we think we understand how the system works. Right. And it’s not until an incident happens that the mental model that we have of the system and the reality of the system come into conflict. And we realize that there’s a divergence, right? So I think it’s important that we continuously share knowledge in whatever form is efficient for folks, whether it be lunch and learns, documentation, I don’t know, presentations, anything like that to sort of share and radiate that knowledge.

Jeff: But we also have to prepare and we have to prepare and define a reality where people may not completely understand how the system works. Right. And the reason I think it’s important that we acknowledge that is because you can make a lot of bad decisions thinking you know how the system behaves and being 100% wrong. Right. So having the wherewithal to understand, okay, we think this is how the system works. We should take an extra second to verify that somehow. Right. I think that’s super important in these complicated environments in these sprawling complex microservice environments. Whereas it can be very, it’s easy to be cavalier if you think, oh yeah, this is definitely how it works. And I’m going to go ahead and shut the service down because everything’s going to be fine. And then everything topples over. So just even being aware of the idea that, you know what, we may not know a hundred percent how this thing works.

Jeff: So let’s take that into account with every decision that we make. I think that’s key. And I think it’s important for management to understand the reality of that as well because for management, it’s easy for us to sit down and say, “Why didn’t we know exactly how this thing was going to fail?” And it’s like, because it’s complicated, right, because there’s 500 touch points, right, where these things are interacting. And if you change one of them, it changes the entire communication pattern. So it’s hard and it’s not getting any easier because we’re getting excited about things like microservices. We’re getting excited about things like Kubernetes. We’re giving people more autonomy and these are just creating more and more complicated interfaces into these systems that we’re managing. And it’s becoming harder and harder for anyone to truly understand them in their entirety.

Drew: We’ve talked a lot about a professional context, big organizations and small organizations too. But I know many of us work on smaller side projects or maybe we volunteer on projects and maybe you’re helping out someone in the community or a church or those sorts of things. Can a DevOps approach benefit those smaller projects or is it just really best left to big organizations to implement?

Jeff: I think DevOps can absolutely benefit those smaller projects. And specifically, because I think sort of some of the benefits that we’ve talked about get amplified in those smaller projects. Right? So exporting of expertise with automation is a big one, right? If I am… Take your church example, I think is a great one, right? If I can build a bunch of automated tests suites to verify that a change to some HTML doesn’t break the entire website, right, I can export that expertise so that I can give it to a content creator who has no technical knowledge whatsoever. Right. They’re a theologian or whatever, and they just want to update a new Bible verse or something, right. But I can export that expertise so that they know that I know when I make this content change, I’m supposed to run this build button.

Jeff: And if it’s green, then I’m okay. And if it’s red, then I know I screwed something up. Right. So you could be doing any manner of testing in there that is extremely complicated. Right. It might even be something as simple as like, hey, there’s a new version of this plugin. And when you deploy, it’s going to break this thing. Right. So it has nothing to do with the content, but it’s at least a red mark for this content creator to say “Oh, something bad happened. I shouldn’t continue. Right. Let me get Drew on the phone and see what’s going on.” Right. And Drew can say, “Oh right. This plugin is upgraded, but it’s not compatible with our current version of WordPress or whatever.” Right. So that’s the sort of value that we can add with some of these DevOps practices, even in a small context, I would say specifically around automation and specifically around some of the cultural aspects too.

Jeff: Right? So I’ve been impressed with the number of organizations that are not technical that are using get to make changes to everything. Right. And they don’t really know what they’re doing. They just know, well, this is what we do. This is the culture. And I add this really detailed commit message here. And then I push it. They are no better than us developers. They know three get commands, but it’s the ones they use over and over and over again. But it’s been embedded culturally and that’s how things are done. So everyone sort of rallies around that and the people that are technical can take that pattern.

Jeff: … around that and the people that are technical can take that pattern and leverage it into more beneficial things that might even be behind the scenes that they don’t necessarily see. So I think there’s some value, definitely. It’s a matter of how deep you want to go, even with the operations piece, right? Like being able to recreate a WordPress environment locally very easily, with something like Docker. They may not understand the technology or anything, but if they run Docker Compose Up or whatever, and suddenly they’re working on their local environment, that’s hugely beneficial for them and they don’t really need to understand all the stuff behind it. In that case, it’s worthwhile, because again, you’re exporting that expertise.

Drew: We mentioned right at the beginning, sort of putting off as much sort of DevOps as possible. You mentioned using tools like Heroku. And I guess that sort of approach would really apply here on getting started with, with a small project. What sort things can platforms like Heroku offer? I mean, obviously, I know you’re not a Heroku expert or representative or anything, but those sorts of platforms, what sort of tools are they offering that would help in this context?

Jeff: So for one, they’re basically taking that operational context for you and they’re really boiling it down into a handful of knobs and levers, right? So I think what it offers is one, it offers a very clear set of what we call the yellow brick road path, where it’s like, “If you go this route, all of this stuff is going to be handled for you and it’s going to make your life easier. If you want to go another route, you can, but then you got to solve for all this stuff yourself.” So following the yellow brick road route helps because one, they’re probably identifying a bunch of things that you hadn’t even thought of. So if you’re using their database container or technology, guess what? You’re going to get a bunch of their metrics for free. You’re going to get a lot of their alerting for free. You didn’t do anything. You didn’t think anything. It’s just when you need it, it’s there. And it’s like, “Oh wow, that’s super are helpful.”

Jeff: Two, when it comes to performance sizing and flexibility, this becomes very easy to sort of manage because the goal is, you’re a startup that’s going to become wildly successful. You’re going to have hockey stick growth. And the last thing you necessarily really want to be doing is figuring out how to optimize your code for performance, while at the same time delivering new features. So maybe you spend your way out of it. You say, “Well, we’re going to go up to the next tier. I could optimize my query code, but it’s much more efficient for me to be spending time building this next feature that’s going to bring in this new batch of users, so let’s just go up to the next tier,” and you click button and you move on.

Jeff: So being able to sort of spend your way out of certain problems, I think it’s hugely beneficial because tech debt gets a bad rap, but tech debt is no different than any debt. It’s the trade off of acquiring something now and dealing with the pain later. And that’s a strategic decision that you have to make in every organization. So unchecked tech debt is bad, right? But tech debt generally, I think, is a business choice and Heroku and platforms like that enable you to make that choice when it comes to infrastructure and performance.

Drew: You’ve written a book, Operations, Anti-Patterns, DevOps Solutions, for Manning. I can tell it’s packed with years of hard-earned experience. The knowledge sort of just leaps out from the page. And I can tell it’s been a real labor of love. It’s packed full of information. Who’s your sort of intended audience for that book? Is it mostly those who are already working in DevOps, or is it got a broader-

Jeff: It’s got a broader… So one of the motivations for the book was that there were plenty of books for people that we’re already doing DevOps. You know what I mean? So we were kind of talking to ourselves and high-fiving each other, like, “Yeah, we’re so advanced. Awesome.” But what I really wanted to write the book for were people that were sort of stuck in these organizations. I don’t want to use the term stuck. That’s unfair, but are in these organizations that maybe aren’t adopting DevOps practices or aren’t at the forefront of technology, or aren’t necessarily cavalier about blowing up the way they do work today, and changing things.

Jeff: I wanted to write it to them, mainly individual contributors and middle managers to say like, “You don’t need to be a CTO to be able to make these sorts of incremental changes, and you don’t have to have this whole sale revolution to be able to gain some of the benefits of DevOps.” So it was really sort of a love letter to them to say like, “Hey, you can do this in pieces. You can do this yourself. And there’s all of these things that you may not think are related to DevOps because you’re thinking of it as tools and Kubernetes.” Not every organization… If you were for this New York State, like the state government, you’re not going to just come in and implement Kubernetes overnight. Right? But you can implement how teams talk to each other, how they work together, how we understand each other’s problems, and how we can address those problems through automation. Those are things that are within your sphere of influence that can improve your day to day life.

Jeff: So it was really a letter to those folks, but I think there’s enough data in there and enough information for people that are in a DevOps organization to sort of glean from and say like, “Hey, this is still useful for us.” And a lot of people, I think identify quickly by reading the book, that they’re not in a DevOps organization, they just have out a job title change. And that happens quite a bit. So they say like, “Hey, we’re DevOps engineers now, but we’re not doing these sorts of practices that are talked about in this book and how do we get there?”

Drew: So it sounds like your book is one of them, but are there other resources that people looking to get started with DevOps could turn to? Are there good places to learn this stuff?

Jeff: Yeah. I think DevOps For Dummies by Emily Freeman is a great place to start. It really does a great job of sorting of laying out some of the core concepts and ideas, and what it is we’re striving for. So that would be a good place to start, just to sort of get a lay of the land. I think the Phoenix Project is obviously another great source by Gene Kim. And that is great, that sort of sets the stage for the types of issues that not being in a DevOps environment can create. And it does a great job of sort of highlighting these patterns and personalities that occur that we see in all types of organizations over and over again. I think it does a great job of sort of highlighting those. And if you read that book, I think you’re going to end up screaming at the pages saying, “Yes, yes. This. This.” So, that’s another great place.

Jeff: And then from there, diving into any of the DevOps handbook. I’m going to kick myself for saying this, but the Google SRE Handbook was another great place to look. Understand that you’re not Google, so don’t feel like you’ve got to implement everything, but I think a lot of their ideas and strategies are sound for any organization, and are great places where you can sort of take things and say like, “Okay, we’re, we’re going to make our operations environment a little more efficient.” And that’s, I think going to be particularly salient for developers that are playing an ops role, because it does focus on a lot of the sort of programmatic approach to solving some of these problems.

Drew: So, I’ve been learning all about DevOps. What have you been learning about lately, Jeff?

Jeff: Kubernetes, man. Yeah. Kubernetes has been a real sort of source of reading and knowledge for us. So we’re trying to implement that at Centro currently, as a means to sort of further empower developers. We want to take things a step further from where we’re at. We’ve got a lot of automation in place, but right now, when it comes to onboarding a new service, my team is still fairly heavily involved with that, depending on the nature of the service. And we don’t want to be in that line of work. We want developers to be able to take an idea from concept to code to deployment, and do that where the operational expertise is codified within the system. So, as you move through the system, the system is guiding you. So we think Kubernetes is a tool that will help us do that.

Jeff: It’s just incredibly complicated. And it’s a big piece to sort of bite off. So figuring out what do deployments look like? How do we leverage these operators inside Kubernetes? What does CICD look like in this new world? So there’s been a lot of reading, but in this field, you’re constantly learning, right? It doesn’t matter how long you’ve been in it, how long you’ve been doing it, you’re an idiot in some aspect of this field somewhere. So, it’s just something you kind of adapt to

Drew: Well, hats off as I say, even after all these years, although I sort of understand where it sits in the stack, I still really don’t have a clue what Kubernetes is doing.

Jeff: I feel similar sometimes. It feels like it’s doing a little bit of everything, right? It is the DNS of the 21st century.

Drew: If you, the listener, would like to hear more from Jeff, you can find him on Twitter, where he’s at dark and nerdy, and find his book and links to past presentations and blog posts at his site, attainabledevops.com. Thanks for joining us today, Jeff. Did you have any parting words?

Jeff: Just keep learning, just get out there, keep learning and talk to your fellow peers. Talk, talk, talk. The more you can talk to the people that you work with, the better understanding, the better empathy you’ll generate for them, and if there’s someone in particular in the organization you hate, make sure you talk to them first.

Solving CLS Issues In A Next.js-Powered E-Commerce Website (Case Study)

Fairprice is one of the largest online grocery stores in Singapore. We are continuously looking out for areas of opportunities to improve the user’s online shopping experience. Performance is one of the core aspects to ensure our users are having a delightful user experience irrespective of their devices or network connection.

There are many key performance indicators (KPI) that measure different points during the lifecycle of the web page (such as TTFB, domInteractiveand onload), but these metrics don’t reflect how the end-user experiences the page.

We wanted to use a few KPIs which correspond closely to the actual experience of the end-users so we know that if any of those KPIs are not performing well, then it will be directly impacting the end-user experience. We found out user-centric performance metrics to be the perfect fit for this purpose.

There are many user-centric performance metrics to measure different points in a page’s life cycle such as FCP, LCP, FID, CLS, and so on. For this case study, we are mainly going to focus on CLS.

CLS measures the total score of all unexpected layout shifts happening between when the page starts loading and till it is unloaded.

Therefore having a low CLS value for a page ensures there are no random layout shifts causing user frustration. Barry Pollard has written an excellent in-depth article about CLS.

How We Discovered CLS Issue In Our Product Page

We use Lighthouse and WebPagetest as our synthetic testing tools for performance to measure CLS. We also use the web-vitals library to measure CLS for real users. Apart from that, we check the Google Search Console Core Web Vitals Report section to get an idea of any potential CLS issues in any of our pages. While exploring the report section, we found many URLs from the product detail page had more than 0.1 CLS value hinting there is some major layout shift event happening there.

Debugging CLS Issue Using Different Tools

Now that we know that there is a CLS issue on the product detail page, the next step was to identify which element was causing it. At first, we decided to run some tests using synthetic testing tools.

So we ran the lighthouse to check if it could find any element which could be triggering a major layout shift, it reported CLS to .004 which is quite low.

The Lighthouse report page has a diagnostic section. That also did not show any element causing a high CLS value.

Then we ran WebpageTest and decided to check the filmstrip view:

We find this feature very helpful since we can find out which element at which point in time caused the layout to shift. But when we run the test to see if any layout shifts are highlighted, there wasn’t anything contributing to the huge LCS:

The quirk with CLS is that it records individual layout shift scores during the entire lifespan of the page and adds them.

Note: How CLS is measured has been changed since June 2021.

Since Lighthouse and WebpageTest couldn’t detect any element that triggered a major layout shift which means it was happening after the initial page load possibly due to some user action. So we decided to use Web Vitals Google Chrome extension since it can record CLS on a page while the user is interacting with it. After performing different actions we found the layout shift score is getting increased when the user uses the image magnify feature.

I have also created a PR to the original repo so that other developers using this library can get rid of the CLS issue.

The Impact Of The Change

After the code was deployed to production, the CLS was fixed on the product details page and the number of pages impacted with CLS was reduced by 98%:

Since we used transform, it also helped to make the image magnify a smoother experience to the users.

Note: Paul Irish has written an excellent article on this topic.

Other Key Changes We Made For CLS

There are also some other issues we faced through many pages in our website which contribute to CLS. Let’s go through those elements and components and see how we tried to mitigate layout shifts arising from them.

  • Web-fonts:
    We have noticed that late loading of fonts causes user frustrations since the content flashes and it also causes some amount of layout shifts. To minimize this we have done few changes:

    • We have self-hosted the fonts instead of loading from 3rd party CDN.
    • We preload the fonts.
    • We use font-display optional.
  • Images:
    Missing height or width value in the image causes the element after the image to shift once the image is loaded. This ends up becoming a major contributor to CLS. Since we are using Next.js, we took advantage of the built-in image component called next/images. This component incorporates several image-related best practices. It is built on top of <img> HTML tag and can help to improve LCP and CLS. I highly recommend reading this RFC to find out the key features and advantages of using it.

  • Infinite Scroll:
    On our website, product listing pages have infinite scrolling. So initially, when users scroll to the bottom of the page they see a footer for a fraction of seconds before the next set of data is loaded, this causes layout shifts. To solve this we took few steps:

    • We call the API to load data even before the user reaches the absolute bottom of the list.
    • We have reserved enough space for the loading state and we show product skeletons during the loading status. So now when the user scrolls, they don’t see the footer for a fraction of seconds while the products are getting loaded.

Addy Osmani has written a detailed article on this approach which I highly recommend checking.

Key Takeaways

  • While Lighthouse and WebpageTest help to discover performance issues happening till page load, they can’t detect performance issues after page load.
  • Web Vitals extensions can detect CLS changes triggered by user interactions so if a page has a high CLS value but Lighthouse or WebpageTest reports low CLS then the Web Vitals extension can help to pinpoint the issue.
  • Google Search Console data is based on real users’ data so that also can point to potential perf issues happening at any point in the life cycle of a page. Once an issue is detected and fixed, checking the report section again can help verify the effectiveness of the performance fix. The changes are reflected within days in the web vitals report section.

Final Thoughts

While CLS issues are comparatively harder to debug, using a combination of different tools till page load (Lighthouse, WebPageTest) and Web Vitals extension (after page load) can help us pinpoint the issue. It is also one of the metrics which is going through lots of active development to cover a wide range of scenarios and this means that how it is measured is going to be changed in the future. We are following https://web.dev/evolving-cls/ to know about any upcoming changes.

As for us, we are continuously working to improve other Core Web Vitals too. Recently, we have implemented responsive image preload and started serving images in WebP format which helped us to reduce 75% of image payload, reduce LCP by 62%, and Speed Index by 24%. You can read more details of optimization for improving LCP and Speed Index or follow our engineering blog to know about other exciting work we are doing.

We would like to thank Alex Castle for helping us debug the CLS issue on the product page and solve the quirks in the next/images implementation.

Tips And Tricks For Evaluating UX/UI Designers

When a company’s digital representation lacks a dedicated UX/UI design team, it can be hard to produce something that stands out from the crowd. The best designers and agencies have a touch of magic about them, transforming your company’s goals, customers’ demands, user specifications, and design instruments into a beneficial experience for both users and businesses. It may seem enchanting how well-designed apps or websites can boost your sales, but it takes far more than a handful of fairy dust to make users enjoy their interactions.

The reason we pay so much attention to design (as we do to any other detail in the process of project completion) is that it plays one of the crucial roles in your business success. To get decent ROI in the digital sphere, you need both a great product and a great design. The two are interdependent. If your design is flawless but your service lacks essential features, don’t expect design to cover for them. And vice versa.

Yet, providing your customers with a well-designed digital solution gives them a chance to appreciate your product to its fullest. That happens when your app/website/system is efficient. And memorable. And easy to understand. And reliable. And carefully arranged. And elaborate. And forms an immediate emotional connection with the users. And urges them to come back again. That’s why you need a good UI/UX designer. Or, better yet, a team.

This article is about the essentials of evaluating potential designers and design agencies you wish to hire. Getting this right is more important than ever, for a couple of reasons:

  1. The global pandemic situation has reshaped the way business is conducted, especially online.
  2. Even once COVID-driven economic conditions cease to exist in the (hopefully) near future, outsourcing will remain a better option for many companies, both in terms of financial resources and worldwide talent engagement.

Every selection process starts with understanding how important it is to have an expert who “gets” your end-users, who can collect all the needs, don’ts, wants, shoulds, and must-haves to ensure you communicate with customers fluently. Then, you start wondering HOW to find out if you have chosen the right person for the job.

NB: Following your gut may be helpful in making some business decisions, BUT this is not the case here.

The ultimate goal is to ensure your app/website can provide its users with the most value possible as seamlessly as possible. So, you expect your designer/design agency to be equally as efficient, cutting, and customer-centered in terms of design and UX.

When it comes to the decision-making process, many businesses make the same mistake over and over again. They apply the same methods used to hire entirely different roles, or allow themselves to trust their instincts.

This article exists to prevent those things from happening. Intuition is not the best adviser when selecting or outsourcing a UX/UI designer. The key to successful hiring requires a different strategy: a list of strict requirements and versatile aspects to assess. The latter would help you pass a more objective and qualified verdict on the candidate’s ability to help your company thrive.

Know What YOU Want Before Evaluating A Designer

I don’t want to give you a set of “designer evaluation criteria” straight away, and the reason is this: it’s not the best place to start. Stage one is about identifying what your company needs from a certain digital product. Before selecting a UX/UI designer/design agency, you have to sit down and list all the requirements and deliverables. This will help you understand what qualities you are actually looking for in your future employee.

Where deliverables are concerned, include the ones you may have in both the research and the production phase of your collaboration. The former may require user experience research, user behavior analysis, brand positioning, and market niche analysis. The latter will include prototyping, usability, and interaction design testing, as well as building wireframes and exploring user journeys.

Using such tools is financially effective, helping to reduce your costs of service by 15-20%. Mapping out user journeys in detail helps to track both minor and global issues inherent in your company, as well as analyze your target audience preferences, attitudes, dislikes, and fads. This creates a whole new pool of production and marketing opportunities for your business.

At present, customer satisfaction is certainly a priority. In 2021, it’s not just advisable, but compulsory to invest in customer experience as with high-quality software development available literally anywhere and at different costs, successful interaction with your customer is the real game-changer.

To be more precise in terms of software requirements, try to define:

  • the type of the software itself and the environments where it will be applied;
  • tasks, features, and functions your software should be able to perform;
  • similar apps/websites/platforms that might prompt what you like and what you would or wouldn’t expect to get.

In addition, keep in mind usability and affordability reconciliation as well as checking if you meet both standards. Finally, highlight the project’s scope, define timelines, and prepare all the legal papers to be fully armed before you move on.

Once the actual cooperation begins, you and your designer/design agency will have to adjust, revise, and re-define these deliverables and requirements in a more precise manner. So, why bother now? If this seems either too strenuous or irrelevant at this point, remember this: the more specifications you provide to your potential designing partner, the easier the selection process will appear. And the fewer obstacles you will encounter when you work together.

Skill Sets For Evaluation

The next step is finalizing a list of hard and soft skills you wish your designer to have. This step, if carefully planned, is essential to the success of your project. The main obstacle at this stage is the variety of skills and their importance in the course of project implementation. Thus, we have made a downloadable cheatsheet for you to use in the course of the transparent assessment of:

How or where can you use them efficiently? Through analyzing the designers’ previous projects, asking questions at the interviews, or analyzing test tasks and preliminary proposals. The key focus should be on the level of the necessary skills a potential candidate has. By rating them you can make a data-driven decision and base it on the factual representation of the desired qualities.

UX Processes

To start with, here’s a set of the universal UX processes crucial to the successful completion of any design-based project whether or not it is carried out by a remote-work team.

To make your evaluation more accurate, here’s a brief explanation of the processes themselves, reasons why they are necessary, and tips on how to check them. You can use this table to mark the level of their excellence in your potential candidate/agency.

Process What to evaluate Why is it important How to assess
User research Understanding the needs, emotions, perception, and experience of the end-users. Examine target audiences in detail (their age, background, qualifications, interests, possible disability issues etc.) Minimizes the risk of designing the wrong product for the wrong people. Ask what research methods they use and how they affect the final outcome.
Information architecture Presenting information efficiently for users, anticipating their future actions to make navigation through apps and websites easy. This makes users’ interactions with your digital product easy and enjoyable. Check the designer’s previous works to see how the parts fit together. Examine how relevant or compatible they are.
Visual design Using visual elements (fonts, color, background, size, etc) through contrast, proximity, or repetition to convey the functionality of a particular page. Allows you to accentuate and prioritize tasks and messages. Opt for designers you admire, respect, trust, and feel they take into consideration your thoughts on the visual design as well as guide you since it is not a rare occasion that what you want as a client does not always work best for the end-customer.
Prototyping Creating a concept to make adjustments and receive feedback from experts involved. Testing the elements of a digital product before it is finalized is essential, giving you examples of how the app/website will work and look. Ask the designers if their clients have access to a clickable prototype.
Usability evaluation Testing the product to make sure the maximum level of understanding has been reached. Allows you to identify any failures or shortcomings and improve them. Ask designers for examples of how they test their products to check they fit the end-users.
Interaction design Designing how the end-product may feel, look, and interact with the customer. Creates a feedback loop for how beneficial or wanted different features may be to the end-user. Examine previous works and see if the design makes sense and behaves in the expected way.

Bear in mind that although it seems rather optional for the UX/UI designer to master development skills, they appear to be quite handy when it comes to a holistic approach. It is advisable for your designer team to have at least basic knowledge of HTML/CSS and understand how front-end/code/styles work, as well as to be able to use the console and make any adjustments if necessary. All this helps ensure the designing process runs smoothly without rough interruptions, missing significant points, or misunderstanding between the development and the design teams.

These skills may seem purely technical. In fact, they are first and foremost focused on creating a user-oriented digital solution driven by understanding. In terms of its value for your company, according to recent statistics, the top 10 most empathetic companies managed to increase their value more than twice. Surprisingly, in the goal-oriented and pragmatic world of the 21st century, empathy appears to be in high demand. No wonder involving emotional impact into your business activities contributes to enhanced employee relations, higher productivity, and, finally, more revenue. Creating an emphatic environment in which company culture and values, leadership, ethical issues, and brand representation in the public eye matter means investing in your future as a viable and prospective manager.

To sum up, first, you study the processes mentioned above and examine how they are represented in the designer’s works and vision. Next, you check if their qualities correspond to the project requirements you have previously defined. If so, you will reach the highest degree of understanding. What we see as the background for successful cooperation with your designer/agency is clearly defined by one of our clients in the following feedback.

Project Skills

The second list examines project management. They turn out to be even more influential when you think of outsourcing services. That is because the efficient functioning of a remote team demands a higher degree of understanding of the following issues.

Skill What to evaluate Why it is important How to assess
Problem setting and strategic thinking If the designer/design agency can identify and prioritize problems. You can’t fix problems that haven’t been spotted. An eye for them allows you to be proactive in finding solutions. Ask if certain “must-do” issues are valid or not. The experiences they have will show how versatile their vision is.
Systematic approach Whether the candidate understands how products should both meet a need and fit into the user’s life. All the pieces need to fit together for a product to excel. Having a front-to-back outlook helps make that possible. Evaluate their attention to details as well as the bigger picture. Do they connect smoothly?
Innovation, idea generation, and creativity What the designer/design agency considers creative or innovative in their previous works and how they see your project implementation. Processes are important, but so too is lateral thinking. You need to know they can think outside the box while still incorporating your vision. Designers should explain their ideas and solutions in a “why-what for-so what” mode to help you understand the validity of our design decisions.
Proactivity How the designer (or a team leader) initiates ideas, motivates workers, coordinates the remote team, and interacts with your in-house employees. A healthy working culture leads to better work (and happier people). You want those who collaborate, motivate, and inspire. Keep a close eye on whether your approaches towards work management are compatible enough. Otherwise you may get an opponent rather than a partner.
Experience If the designer/design agency has enough experience both in UX/UI design and the area of your interest. As in any line of work, you want people who know what they’re doing, who you trust to bring something to the table you can’t. The indicators for this skill are not the number of years or projects completed but the actual digital solutions produced. Think quality, not quantity. A talented, inexperienced team can still offer great solutions.

Good UX/UI designers look for a special approach towards every client and audience, as well as the solution to the task. They never explore just one path but study a number of options that may make your users’ lives easier.

In order to make your judgment as objective as possible, do not forget that you may engage your team for versatile opinions. Also, try assessing the skill sets during interviews by inquiring, discussing, and comparing your visions of the issues.

Personal Qualities

To make outsourcing cooperation both efficient and effective, never underestimate personal qualities. If anything, they could be the decision triggers in the final stage of your evaluation. The following points will help you understand if you wish to work with a certain remote designer/team and if they can bring value to your company.

How or where can you evaluate these criteria efficiently? Through online interviews, team presentations, back-and-forth messaging, and work sessions.

For those who doubt the value of personal qualities, here are some numbers. Forbes points out that companies excelling at user experience have 1.5 times more engaged employees than those who are less customer-focused. Certainly, it is difficult to analyze such requirements as these are not easily translated into revenue numbers. Yet, to stand out from hundreds of competitors, you need to get out of the so-called “commodity trap” with dozens of similar (if not the same) products and services available here and now. Here, customer experience seems to be the only option to the rescue.

So, what customer-oriented skills need to be assessed are as follows.

Skill What to evaluate Why it is important How to assess
Communication The ability to present and explain ideas clearly and concisely, listening to and following conversations. In short, to work in an agile environment. These qualities indicate whether your team will be easy to reach, understand, and exchange feedback with. It is important to adjust to ever-changing requirements, and in a timely manner. If the designer/team can articulate their ideas to you, they can “talk” to your customers and translate your needs into a digital solution.
Collaboration and empathy How they cooperate with other team members; how they deal with conflicts and resolve disagreements; how flexible and open to feedback they are. If they have these traits, they will be able work with all the people involved in the work process, promote their decisions, support users, and make the product correspond to their needs. Empathy is the core ingredient in creating a successful UX design. Remember it is the end-user who will interact and enjoy (or not) the outcome of their work. Will candidates meet those needs?
Cultural contribution They understand, respect, and represent your company’s values. They are passionate about working with you. A good team is capable of inspiring people they work with. If their vision of values is too different from yours, the product will be far from what you expect. As a sign of respect towards a new client, we show our interest by studying the company and its brand values in advance.

Now you are fully equipped with the refined lists of skills and qualities you should pay attention to in your evaluation. Once you study or even edit them to suit your needs and ideas, you are ready for the next stage.

Navigating Interviews

Once you have the game plan for precise evaluation, your next step is the actual face-to-face (or, rather, camera-to-camera) discussion. In order to filter the talent pool efficiently, start the interview prepared. Remote hiring goes far beyond random inquiries about the candidate’s qualities and experience. It requires a structured list of questions, issues, and topics to discuss.

Beware also of possible technical failures like poor internet connection or not preparing relevant data in advance. Assign times and meeting links to all candidates. Be there on time. Such details will help keep the whole process smooth.

What concerns the list of questions you may ask, always think of their content as well as:

  • The number of questions.
    Have enough to allow for substantive discussions while still keeping to the interview window.
  • Giving the proper chance for the candidates to talk themselves.
    This way they wouldn’t feel like they are being interrogated.

The latter is crucial. In our company, we like to provide candidates with the opportunity to give as much input as possible. Examine your potential designer’s speech to see how good they are at explaining their tasks or technical specifications. Our experience shows that the better designers present their vision orally, the more usability they will bring to the product.

Here are several more points that are much more tricky in their essence.

  • Many articles on this topic tell you to ask about the tools and workflow of the designer/agency. However, do not expect to get a clear checklist to validate the right candidate. On the contrary, you should just make sure the tools match the ones your team prefers. While the workflow is examined to see if your approaches are compatible.
  • Ask the designer/team to present a project or product they really enjoyed. This will show how passionate they are about the design in general and working on your project in particular. Your potential employee would be thrilled to give very specific details. Even better, if they explain how this experience might relate to your cooperation.
  • Never underestimate the designer’s questions about your company/project in return. If they are truly interested, they want to know detailed and relevant(!) information on the target audience, main KPIs, business models, testing methods you apply, and much more.

Mistakes To Avoid

Our experience of providing outsourcing services before, during, and hopefully after the COVID-19 era has provided us with a number of insights. Here are some ideas on what to steer clear of in your evaluation.

  • If your budget is big enough, opt for an agency.
    When compared to freelancers, the former will definitely offer more expertise. Once you vote for an all-in-one option, this person should be either in charge of every stage and process or you may get a limited amount of functions performed.
  • Questions you ask are valuable.
    Sensible re-defining or re-stating may be the key to getting the clarity we need. Sometimes, even the source of new discoveries.
  • Look for initiative as well humility.
    The latter is usually presented as one of the most valuable qualities of a UX/UI designer. However, its significance could be exaggerated. In some cases, it is vital for the designer/team to take the initiative in introducing ideas or justifying important decisions.
  • Prioritizing skills is a dead-end approach.
    You need to evaluate all the necessary skills. Yet, a comprehensive vision is the key to success. Rather than focus on many specific traits, evaluate their combination and compatibility as a whole set.
  • Overestimating portfolios is risky.
    The number of projects does not correspond to the level of expertise. At LinkUp Studio, we offer our clients an overview of the cases related to their project and urge them to look at qualitative achievements. The problems we have solved, the insights and solutions we have offered, and how the latter altered one’s business. Such presentations help future work partners see our holistic approach and the value we bring to their ideas.
  • Do not confuse the initial proposal with the in-depth research which comes after you sign up with the agency.
    Some clients wish to get a preliminary analysis of their project. And that makes sense. A commercial offer presents rough project scope and cost estimation. That is justified for both parties in the process of negotiations. However, further analysis (a.k.a. discovery stage) involves a range of experts like business analysts, designers, developers. So, it should not be expected free of charge as a part of your ballpark estimate.

We hope our experience and useful tips uncovered will fully equip you for a conscious designer evaluation. All in all, the keys to making a successful hire are strict criteria to follow a holistic approach towards the assessment process which includes the following stages:

  1. Identify your company’s precise requirements and deliverables.
  2. Make the lists of hard and soft skills your designers should possess as well as rate their importance for a particular project in mind.
  3. Prepare for and conduct interviews to make the right choice.
  4. Enjoy your cooperation.

Choosing the proper UI/UX designer is the key to providing your customers with an excellent user experience through incorporating your company’s aims and your client’s needs into the design process. The latter being efficient, understandable, reliable, emotionally appealing, and properly devised is the very task of a UI/UX designer you are about to start evaluating.

To help you make this process pain- and stress-free, we have developed a cheat sheet to come in handy during the selection and assessment process. You can download it here.

Smart CSS Solutions For Common UI Challenges

It’s incredible to see what we can do with CSS today, especially if you still remember how difficult it once was to figure out stacking contexts or why margins collapsed and why top: float didn’t work. In this post, we’ll look at just that: exciting and fun things we can do with CSS, exploring common problems and use cases we all have to face in our work.

Another fantastic article on the topic comes from Josh W Comeau. To put an end to those “fuzzy grey boxes that don’t really look much like shadows”, as Josh describes the current state of most shadows on the web, he shows how to transform typical box-shadows into beautiful, life-like ones. A little detail that makes any UI a lot more tactile.

CSS Paper Cut-Out Effect

If you ever wanted to create a paper cut-out effect for a heading, you might have struggled quite a bit. Perhaps you need to set up two separate divs that then would be overlapped on top of each other. The spacing would need to be defined in relative units, of course, that might be a bit difficult to get right across screens.

Stephanie Eckles’ CSS Paper Cut-Out Effect solves the problem for good with CSS custom properties, CSS Grid, CSS transforms and a good ol’-fashioned CSS function attr(). Stephanie is using a data-attribute on h1 along with a span inside it. attr() picks up the value of the data-attribute, which is then used for the content-property in a :after-pseudo element. The highlights, shadows and colors can then be adjusted with CSS Custom Properties. Reusable and simple to maintain!

And if you are interested in more magic by Stephanie and other wonderful people who love CSS, take a look at StyleStage, where modern CSS gets the spotlight it deserves.

Also, take a look at Ahmad Shadeed’s piece on Thinking About The Cut Out Effect, which goes into all the fine detail of deciding when SVG might make more sense, and how to implement in a real-life scenario. The article also provides plenty of code examples to get started with!

The Minimap For The Web

We’ve seen them before: tiny horizontal bars that usually live at the top of the page. As a user is scrolling down, the horizontal bar is filling up, so the user knows how much is actually left to scroll.

What if we make it a bit more contextual though? Perhaps the page includes some images and videos, or quotes and distinct sections — wouldn’t be interesting to highlight them differently, while also allowing readers to pin a position on the page and jump back if needed? Well, Rauno Freiberg thought so, too.

Rauno’s minimap for the web (currently works only in Firefox) makes it trivial to create a minimap representation of the entire page, while also allowing readers to pin section of the page and navigate between them. To achieve it, Rauno uses an experimental CSS property element()) to display a live image from an arbitrary HTML element (which currently only available in Firefox).

Conditional Border Radius In CSS

When designing cards, you might want a border-radius to have a quite sizeable value when there is enough space to display it along with other cards. Yet when there is no space and perhaps no margin either on the card — as it might be the case on smaller screens — you might want to reduce border-radius to 0. How would you achieve that?

Ahmad Shadeed has looked into this problem in quite a bit of detail in his article on Conditional Border Radius In CSS. The idea, originally suggested by Heydon Pickering and Naman Goel, is to use a large enough number to trigger one state or the other. On smaller screens, if the difference between 100vw and 100% is 0, then the radius will be 0, too; but if the difference is larger, a larger value would be used. You can take a look at the CodePen as well.

CSS Grainy Gradients

What if you wanted to add some noise to bring a bit of texture to an image? Of course you could export images as PNGs, WebP or AVIFs, but ideally we’d want to add “noise” on top of SVGs, so we can always turn and off noise if we wanted to.

In his CSS-Tricks article on grainy gradients, Jimmy Chion explains how we can generate colorful noise to add texture to a gradient with just a sparkle of CSS and SVG. As Jimmy explains, the idea is to use an SVG filter to create the noise, then apply that noise as a background. Then we layer it underneath a gradient, refine the brightness and contrast, and — voilà — you have gradient that gradually dithers away.

Issue solved! You can also explore the Grainy Gradient playground that Jimmy has set up.

Multi-Line Background Gradient

Some things might seem impossible to do with CSS — well, until someone finds a hack to make it happen. Like in this case: Can you achieve a multi-line padded text with a gradient that doesn’t reset for each line?

Yes, as Matthias Ott shows. Matthias’ solution is a bit hacky, but it leads to the desired result thanks to a pseudo-element that is added on top of the text. An interesting idea to tinker with.

Form Field Focus Without Outlines

Who said forms need to be boring? Hakim El Hattab created a demo that proves that even something as simple as a form asking for name, email, and password is an occasion to think outside the box and cater for a spark of delight.

To achieve that, Hakim combines form focus and validation in a subtle yet surprising animation that gets by without any focus outlines on the fields themselves. Instead, a dot marks the field that is focused. When the focus switches to another field, the animation is triggered, and the dot jumps to the new active field, drawing a connection between the two. Form field validation is also integrated seamlessly, with the dot expanding and showing a checkmark. If you’d like to dive deeper into the code, Hakim also published a demo on Codepen. Inspiring!

Transitioning CSS Gradients

If you’ve ever tried to transition gradients in CSS, you probably have noticed that it actually doesn’t work. Instead of gradually fading from one gradient to another, the change is happening immediately, abruptly, with no smooth transition between the two.

As Keith J. Grant has discovered, we can achieve the transition with a clever workaround though. We use a pseudo-element and opacity transform for that. First, we apply one gradient to the element, then position its pseudo-element to fill the element, and then apply the second gradient to it. And we “transition” between two gradients by transitioning the opacity of the pseudo-element. You can check a full working example on CodePen.

Improving Image Performance With image-set()

Have you heard of image-set() already? You can think of it as a CSS background equivalent to the HTML srcset attribute for img tags. Chromium-based browsers and Safari have supported it for some years now, Firefox followed just recently. Ollie Williams takes a look at what we can and what we can’t do with it today.

As Ollie describes, one use case for image-set() is to provide multiple resolutions of a background image and leave it to the browser to decide which image is served to a user — a high-res version for users on fancy devices and a lower-res image for those on slow connections or screens with a lower pixel density, for example.

Another very promising use case is still lacking browser support, unfortunately: the idea to use new image formats like AVIF, WebP, or HEIF while adding a fallback for older browsers. If you want to achieve something like that already today and don’t need background-image, the &lt;picture&gt; element might be an alternative worth considering, as Ollie suggests. A great read to help you improve image performance.

Clip-Path Pop-Out Effect

With clip-path: path() supported by major browsers, it’s time to get creative. Mikael Ainalem shows a beautiful use case for the rather new feature: a buttery-smooth pop-out effect.

Mikael uses clip-path: path() to set a photo of a person apart from its circle-shaped background. As you hover over it, the person seems to rise from the inside of the circle, creating a cool 3D impression. Perfect for “About Us” pages.

Whimsical 3D Button

Details matter. Designing a lovely button doesn’t seem to be a big complicated endeavor: a bit of padding here and there, a funky color, accessible text, and a few button states. Well, Josh Comeau has gone all the way to design a truly whimsical 3D button that you might want to click more than once.

The idea is simple: we create two layers and offset the foreground layer a little bit at first. On hover, we shift the front layer down. Then, we adjust the focus outline with outline-offset, or use :focus:not(:focus-visible) to hide the outline when the button is focused and the user is using a pointer device.

Then we shift the button up by a few pixels when they hover, animate the transform a lil’ bit, adjust the Bezier curve for animation and add a bit of blurring, for a softer, more natural shadow. And voilà — we have a whimsical 3D button that is accessible, works on mobile and on desktop, and is a pleasure to tap and click on. Of course, you can find the full code snippet on Josh’s blog.

CSS Charts

Perhaps you need to design a column chart, or a bar chart, or even a multi-dataset column chart or stacked columns. Where do you even start? Perhaps with Charts.css, a CSS data visualization framework that uses CSS utility classes to style HTML elements as charts.

Created by Lana Gordiievski and Rami Yushuvaev, the framework supports many chart types, has no dependencies, and is very lightweight. It also includes thorough documentation on its components and built-in chart types, plus the source code is available on GitHub (licensed under MIT). And if you need slightly more creative approaches, Preethi explains how to create CSS Charts with interesting shapes on CSS-Tricks as well.

The New CSS Reset

What do you use to normalize styles across browsers? As of recently, there were new approaches to reduce the size of the global CSS reset, and perhaps they would be good candidates for your projects as well.

With his Modern CSS Reset, Andy Bell has reduced the CSS reset to its minimum by adding box-sizing rules, removing default margins, set core root default and body defaults. Along with it Andy removes all animations, transitions, and smooth scroll for people that prefer not to see them, and has added modern properties like scroll-behavior and text-decoration-skip-ink by default.

The New CSS Reset by Elad Shechter takes a slightly more aggressive approach. Elad removes all the default styles which we are getting on specific HTML elements except the display property. Both approaches are worth looking into!

Stable Scrollbar Gutters With CSS

Ah, the good ol’ layout shifts! As Bramus Van Damme explains, one of the slightly more obscure reasons for layout shifts comes due to different types of scrollbars on the web. While overlay scrollbars on iOS/macOS are placed over the content (and aren’t shown by default), other scrollbars are placed in the “scrollbar gutter”, i.e. the space between the inner border edge and the outer padding edge.

When the content of a box becomes too large, the browser will — by default — show a scrollbar. In the latter case, it will cause a layout shift. Fortunately, this problem might be gone for good soon. Meet a shiny new scrollbar-gutter property: by setting it to stable, we can have the browser always showing the scrollbar gutter, even if the box is not overflowing.

And to keep things symmetric, we can use scrollbar-gutter: stable both-edges. The feature isn’t available yet, but coming in Chromium very soon, with other rendering engines hopefully following soon.

The Surprising Things That CSS Can Animate

When you think of animating CSS properties, which ones come to your mind? Will Boyd looked at the question from a different perspective and decided to explore the properties that don’t come to mind immediately, the ones that aren’t typically associated with animation, but turn out to be animatable.

In his post “The Surprising Things That CSS Can Animate”, Will dives deep into these unexpectedly animatable properties — and, of course, the nifty things you can do by animating them. z-index, for example, can be used for layered animations, opacity helps you fade out a modal just with CSS. A great reminder of how powerful CSS is.

Learning Resources

Learning never stops, right? Below we compiled some useful — and fun! — resources that are perfect to take your CSS skills to the next level. And if you’re already a CSS pro, there are also challenges to put your knowledge to the test. Enjoy!

CSS Vocab And Cheatsheets

You might have been there before. Just when you are working on a tight deadline, you need to look up something quickly. For CSS, you will never go wrong with CSS Tricks Almanac, and you can also look up CSS vocabulary gathered by Ville V. Vanninen from Finland as well.

Learn Flexbox The Fun Way

What do frogs, zombies, and towers have in common? Well, they are your best friends when learning Flexbox. Because, let’s be honest: Flexbox is very powerful once you understand it, but getting there can be quite hard. So let’s make learning a bit more fun.

In the game Flexbox Froggy, you help a little frog and its friends find their lilypads by, you guessed it, writing CSS. The game consists of 24 levels that take you from the very basics of Flexbox positioning to more advanced challenges.

If zombies are more down your alley, Flexbox Zombies is for you. Each section of the game unravels part of the plot, introduces a new Flexbox concept, and presents so-called “zombie survival challenges” that help you solidify your new skills.

Last but not least, you might also want to take a look at Flexbox Defense. Inspired by tower defense games, your job is to stop incoming enemies from getting past your defenses — by positioning your towers with CSS, of course. All three games run right in your browser. Happy flexbox’ing!

CSS Grid, CSS Selectors, And Other Competitions

Do you want to take your CSS skills to the next level? These three little games help you do just that — quite literally. In Grid Garden, you’ll become the proud owner of a carrot garden. 28 levels are waiting for you in which you need to take good care of your crop with the help of CSS grid.

If you feel your CSS selectors skills could need some polishing, CSS Diner is for you. Plates, apples, pickles — in each of the 32 challenges, you’ll need to use a different CSS selector to select specific items on a table.

And if you’re up for some competition, be sure to also check out CSSBattle. In the CSS golfing game, you’ll be using your CSS skills to visually replicate targets with smallest possible code to get to the top of the leaderboard. Each of the challenges is dedicated to a specific topic like visibility, display, transition, or z-index.

Wrapping Up

Have you come across a CSS resources or technique recently that changed the way you approach a particular challenge? Let us know in the comments below! We’d love to hear about it.