Creating a Preloader for Gatsby site
Tags: gatsby, react, javascript
Published: 2021-02-16 20:57:23

## Introduction

Gatsby sites are *fast by design* (as the [official website](https://gatsbyjs.com) claims) and personally I can only agree with that statement. However, you might have encountered a situation before where you wanted to briefly display a page loader (or as I call it, *preloader*) while some parts of the website are still loading, whatever the reason may be.

![Preloader preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qzxvxxhpjze3m3833qtm.gif)

In this post we will go through steps to create a simple customizable preloader that will overlay the page content and fade out once the document is ready. In the end we will also add fallback support for users that have JS disabled in their browsers. The complete code can be found in the [summary section](#summary). Let's get started!

## Implementation

### Creating preloader component
The visual portion of the preloader consists of a parent element and custom inner content. For an inspiration, I will be providing a simple [logo placeholder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1il0jrlv84fv7bd31iip.png) and animation, however feel free to customize or replace it to better fit your own needs. 

Since Gatsby's provides a set of [server rendering APIs](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-ssr/), we will utilize the `gatsby-ssr.js` file and its functions. First we create the JSX component with a parent `div` element and give it an ID of `preloader`. Inside of it, we define our custom content, in this case a logo image and a animation element. 

In order to add this component to the final HTML, we pass it through the `setPreBodyComponents` function on the Gatsby's `onRenderBody` event, which is called during the server side rendering. As the last step we import React for it to be complete:

###### gatsby-ssr.js
```js
const React = require("react")

exports.onRenderBody = ({
  setPreBodyComponents
}) => {
  setPreBodyComponents([
    <div id="preloader">
      {/* Optional: */}
      <img src="/images/logo.png" alt="logo" style={{"height": "calc(3.23625vw + 77.86408px)"}} />
      <div className="preloader_animation"></div>
    </div>
  ])
}
```

Next, we will be adding styles for this component. Again, the inner content styling is up to you and my demo styles can be found at the bottom of the code block:

###### src/styles/preloader.scss
```scss
body {
  #preloader {
    position: fixed;
    display: none;
    top: 0;
    left: -10%;
    right: -10%;
    height: 0;
    margin-left: calc(100vw - 100%);
    overflow-x: hidden;
  }

  &.preloader_active {
    height: 100vh;
    overflow-y: hidden;

    #preloader {
      height: auto;
      bottom: 0;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      background-color: #27135c;
      z-index: 9999;
      display: flex;
      overflow: hidden;
    }
  }

  &.preloader_ready {
    height: auto;
    overflow-y: auto;
    
    #preloader {
      animation: preloaderLeaving 0.5s forwards;

      @keyframes preloaderLeaving {
        0% {
          opacity: 1;
        }
        100% {
          opacity: 0;
        }
      }
    }
  }
}

// Optional:
body {
  #preloader {
    img {
      z-index: 120;
    }

    .preloader_animation {
      position: absolute;
      width: calc(3.23625vw + 77.86408px);
      height: calc(3.23625vw + 77.86408px);
      border: 5px solid #ffffff;
      border-radius: 50%;
      opacity: 0;
      z-index: 110;
      animation: preloaderAnimation 1.5s ease-out infinite 0s;

      @keyframes preloaderAnimation {
        0% {
          transform: scale(.1);
          opacity: 0.0;
        }
        50% {
          opacity: 1;
        }
        100% {
          transform: scale(1.2);
          opacity: 0;
        }
      }
    }
  }
}
```
Let's not forget importing this file into the final HTML through the `gatsby-browser.js` file:

###### gatsby-browser.js
```js
import "./src/styles/preloader.scss"
```

### Adding functionality
As you might have noticed in the stylesheet, we are defining a few currently unused classes such as `preloader_active` and `preloader_ready`. Each of these is bound to a stage in the preloader life cycle:

#### `preloader_active`
This is the starting class/stage. If the `body` element contains this class, the preloader is visible and overlays the page content. To add this class, we will return to the `gatsby-ssr.js` and add it like this:
###### gatsby-ssr.js
```js
// ...
exports.onRenderBody = ({
  // ...
  setBodyAttributes
}) => {
  // ...
  setBodyAttributes({
    className: "preloader_active"
  })
}
```

#### `preloader_ready`
Once the document is ready, we can start fading out the preloader, thanks to the CSS animation in this class. We add the class to the body with a script that is waiting for the document state change. First, we have to create the given script:

###### static/scripts/preloader.js
```js
var body = document.querySelector("body");
document.onreadystatechange = function () {
  if (document.readyState === "complete") {
    body.classList.add("preloader_ready");
    setTimeout(function () {
      body.classList.remove("preloader_active");
      body.classList.remove("preloader_ready");
    }, 500);
  }
};
```
Then we insert the script into the final HTML, using the `gatsby-ssr.js` file and the available API yet again:
###### gatsby-ssr.js
```js
// ...
exports.onRenderBody = ({
  // ...
  setHeadComponents,
  setPostBodyComponents
}) => {
  setHeadComponents([
    <link as="script" rel="preload" href="/scripts/preloader.js" />
  ])
  // ...
  setPostBodyComponents([
    <script src="/scripts/preloader.js" />
  ])
}
```
We want this script to be available as soon as possible, which is the reason for pointing to it twice. You can read more about content preloading in MDN Web Docs' [guide](https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content). 

### Finishing with noscript support
Even though we are building a website based on JavaScript, we still want to support users who prefer to have JS disabled in their browsers. Since the preloader is dependent on a script, it would stay visible forever, preventing the user to see any content. We can simply include a separate stylesheet inside a `noscript` tag in the `head` of the page ([explicitly allowed](https://html.spec.whatwg.org/multipage/scripting.html#the-noscript-element) in HTML5), which hides the preloader:

###### static/styles/noscript.css
```css
body.preloader_active {
  height: auto;
  overflow-y: auto;
}

body.preloader_active #preloader {
  display: none;
}
```
###### gatsby-ssr.js
```js
// ...
exports.onRenderBody = ({
  // ..
}) => {
  setHeadComponents([
    // ...
    <noscript>
      <link rel="stylesheet" href="/styles/noscript.css" />
    </noscript>
  ])
  // ...
}
```

## Summary
We have successfully added a preloader to our Gatsby site. Upon entering the site, the preloader will be overlaying the content until the document is ready, and then it will fade out. If the user has disabled JS in their browser, the preloader will remain hidden through a stylesheet.

Below you can find all files that we have added or modified:

{% collapsible Collapsible %}

###### gatsby-ssr.js
```js
const React = require("react")

exports.onRenderBody = ({
  setHeadComponents,
  setPreBodyComponents,
  setBodyAttributes,
  setPostBodyComponents
}) => {
  setHeadComponents([
    <link as="script" rel="preload" href="/scripts/preloader.js" />,
    <noscript>
      <link rel="stylesheet" href="/styles/noscript.css" />
    </noscript>
  ])
  setPreBodyComponents([
    <div id="preloader">
      {/* Optional: */}
      <img src="/images/logo.png" alt="logo" style={{"height": "calc(3.23625vw + 77.86408px)"}} />
      <div className="preloader_animation"></div>
    </div>
  ])
  setBodyAttributes({
    className: "preloader_active"
  })
  setPostBodyComponents([
    <script src="/scripts/preloader.js" />
  ])
}

```

###### gatsby-browser.js
```js
import "./src/styles/preloader.scss"
```

###### src/styles/preloader.scss
```scss
body {
  #preloader {
    position: fixed;
    display: none;
    top: 0;
    left: -10%;
    right: -10%;
    height: 0;
    margin-left: calc(100vw - 100%);
    overflow-x: hidden;
  }

  &.preloader_active {
    height: 100vh;
    overflow-y: hidden;

    #preloader {
      height: auto;
      bottom: 0;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      background-color: #27135c;
      z-index: 9999;
      display: flex;
      overflow: hidden;
    }
  }

  &.preloader_ready {
    height: auto;
    overflow-y: auto;

    #preloader {
      animation: preloaderLeaving 0.5s forwards;

      @keyframes preloaderLeaving {
        0% {
          opacity: 1;
        }
        100% {
          opacity: 0;
        }
      }
    }
  }
}

// Optional:
body {
  #preloader {
    img {
      z-index: 120;
    }

    .preloader_animation {
      position: absolute;
      width: calc(3.23625vw + 77.86408px);
      height: calc(3.23625vw + 77.86408px);
      border: 5px solid #ffffff;
      border-radius: 50%;
      opacity: 0;
      z-index: 110;
      animation: preloaderAnimation 1.5s ease-out infinite 0s;

      @keyframes preloaderAnimation {
        0% {
          transform: scale(.1);
          opacity: 0.0;
        }
        50% {
          opacity: 1;
        }
        100% {
          transform: scale(1.2);
          opacity: 0;
        }
      }
    }
  }
}
```

###### static/scripts/preloader.js
```js
var body = document.querySelector("body");
document.onreadystatechange = function () {
  if (document.readyState === "complete") {
    body.classList.add("preloader_ready");
    setTimeout(function () {
      body.classList.remove("preloader_active");
      body.classList.remove("preloader_ready");
    }, 500);
  }
};
```

###### static/styles/noscript.css
```css
body.preloader_active {
  height: auto;
  overflow-y: auto;
}

body.preloader_active #preloader {
  display: none;
}
```
###### static/images/logo.png
![Logo placeholder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1il0jrlv84fv7bd31iip.png)

{% endcollapsible %}