DEV Community

Cover image for Say "No" to loading spinner - How to create a creative loading component with 99.1% pure CSS
Jonatas Sennas
Jonatas Sennas

Posted on

Say "No" to loading spinner - How to create a creative loading component with 99.1% pure CSS

When you're working on the front-end of your app and its routes, you'll likely encounter a UX issue: loading components. Loading components are a screen feature commonly used to display content to users while the requested data has not yet been fully loaded. This feature makes the response time of the request go unnoticed by the user, reducing their frustration when using the application and thus directly impacting the user conversion rate.

Generally, Generic, Gattlestar Balatica

borinngggggThere are several ways to create a loading component, but developers usually choose to venture into familiar territory: spinners. The reason why this happens is clear. Spinners solve the issue with requests, there are thousands of videos on how to make them, and, the main reason, there are already many libraries that provide them ready to use. So, what's the problem? Any problems. As they are quite common, spinners end up becoming a generic solution for a common problem. They do solve the issue, but where's the magic in that?

So, why use spinners when you can use your brand on the loading screen?

Your brand on the loading screen

Apart from being really cool, placing your brand logo on the loading screen reinforces to the user that they are in your application. Additionally, seeing something different from the usual sparks the user's curiosity, and they think, "Wow, that's cool!".

In this article, you'll find a tutorial on creating a creative loading components using CSS.

Requirements

In this tutorial, I used these technologies:

  • Next.js
  • CSS

Next.js was used due to how this framework handle routes. Simple and easy peasy. The focus of this article is on CSS, so you can easily adapt it to any technology you want. Finally, grab your keyboard and let's get started.

Step 1 - Create pages

For this tutorial, three very basic pages were created just to simulate the functioning of the loading component. The idea for the app theme came from the series "Breaking Bad", which, by the way, is my favorite. Additionally, the application was implemented in a gamified manner.

I won't go into much detail about the creation of the screens themselves, but I'll provide the pages JSX code and the corresponding CSS. But, in any case, I'll leave the project repository available at the end of this article.

Home (/)

import Image from "next/image";
import {redirect} from "next/navigation"
import styles from "./page.module.css";


import chibiBerg from "../assets/chibiberg.png";
import Link from "next/link";


export default function Home() {
  return (
    <div className={styles.container}>
      <div className={styles.contentWrapper}>
        <div className={styles.dialogBox}>
          <p className={styles.message}>Say my name!</p>
        </div>
        <div className={styles.imageWrapper}>
          <Image src={chibiBerg} alt="Bad and problematic person" />
        </div>
      </div>
      <div className={styles.buttonWrapper}>
        <Link className={styles.buttonOff} href={"/walter-white"}>Walter White</Link>
        <Link className={styles.buttonOn} href={"/heisenberg"} >Heisenberg</Link>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Walter White (/walter-white) - wrong answer

import Image from "next/image";
import Link from "next/link";
import styles from "@/app/page.module.css";

import chibiBerg from "@/assets/chibiberg.png";

export default function WalterWhite() {
  return (
    <div className={styles.container}>
      <div className={styles.contentWrapper}>
        <div className={styles.dialogBox}>
          <p className={styles.message}>SAY.. MY.. NAME!</p>
        </div>
        <div className={styles.imageWrapper}>
          <Image src={chibiBerg} alt="Bad and problematic person" />
        </div>
      </div>
      <Link className={styles.buttonOn} href={"/heisenberg"} >Heisenberg</Link>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Heisenberg (/heisenberg) - goddamm right answer

import Image from "next/image";
import styles from "@/app/page.module.css";

import chibiBerg from "@/assets/chibiberg.png";

export default function Heisenberg() {
  return (
    <div className={styles.container}>
      <div className={styles.contentWrapper}>
        <div className={styles.dialogBox}>
          <p className={styles.message}>{"You're goddamm right!"}</p>
        </div>
        <div className={styles.imageWrapper}>
          <Image src={chibiBerg} alt="Bad and problematic person" />
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

CSS

.container {
  min-height: 100vh;
  width: 100%;
  background-color: #515151;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 6rem;
}

.contentWrapper {
  display: flex;
  align-items: flex-start;
}

.imageWrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 22rem;
  width: 16rem;
  overflow: hidden;
}

.dialogBox {
  display: flex;
  padding: 2rem 4rem;
  background-color: white;
  border: 1px solid transparent;
  border-radius: 1.5rem 1.5rem 0 1.5rem;
}

.message {
  font-weight: 700;
  font-size: 2rem;
}

.buttonWrapper {
  display: flex;
  gap: 1rem;
}

.buttonOn, .buttonOff {
  display: inline-block;
  color: #000000;
  padding: 1rem 4rem;
  border: 1px solid transparent;
  border-radius: 1rem;
  cursor: pointer;
  font-size: 1.5rem;
  font-weight: 700;
  transition: .2s;

}

.buttonOn {
  background-color: #519bc0;
}

.buttonOff {
  background: transparent;
}

.buttonOn:hover{
  background-color: #81cdff;
}

.buttonOff:hover {
  background-color: #9f0000;
  color: #ffffff;
  opacity: .8;
}
Enter fullscreen mode Exit fullscreen mode

Step 2 - Create loading component

loadingNow, let's get to the good part. I'll divide this step into two parts. First, I'll show you how to create the logo animation, and then I'll demonstrate the text animation. So, let's do this.

Breaking Bad logo

To obtain the Breaking Bad logo as a component, I used the SVGR tool, which converts your SVG code into a component in a very simple way. Just copy the code into the SVGR playground, and you're all set. I chose this approach because the img tag or Image (Next.js) severely limits SVG manipulation. So, if you need to make any color changes to an SVG, for example, this is a good approach.
SVGR playground

Loading component with only logo

import Icon from "../Icon";
import styles from "./index.module.css";

export default function Loading() {
  return (
    <main className={styles.main}>
      <div className={styles.logoWrapper}>
        <Icon /> {/* Back logo */}
        <div className={styles.frontLogoWrapper}>
          <Icon /> {/* Front logo */}
        </div>
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode
.main {
  /* We only want the component to appear*/
  position: fixed;
  top: 0;
  left: 0;
  min-height: 100%;
  width: 100%;
  background-color: #eacea7;

  /* Center the loading component */
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  gap: 5rem;

  z-index: 10; /* High z-index to cover all app */
}

.logoWrapper {
  position: relative; /* Define which element the SVG is relative to.*/
}

.logoWrapper svg {
  width: 30rem; /* Both icons should have the same dimensions. */
}

.logoWrapper > svg {
  fill: #aaaaaa; /* Change back logo color */
}

.frontLogoWrapper > svg {
  fill: #30693e; /* Change front logo color */
}


.frontLogoWrapper {
  /* Position the logo in front of the other one. */
  position: absolute;
  top: 0;
  left: 0;

  /* And here's the magic */
  overflow: hidden;
  animation: slide 3s ease-out forwards; /* "forwards" so that the
  wrapper width stays the same when the animation is over. */
}
@keyframes slide {
  0% {
    width: 0%;
  }

  100% {
    width: 100%;
  }
}
Enter fullscreen mode Exit fullscreen mode

In summary,

  1. Wrap the front logo in a div tag
  2. Position the logo in front of the other one.
  3. Overflow hidden to cover the front logo
  4. Animate the wrapper to increase its width and uncover the front logo

Loading component with logo and text animation

import Icon from "../Icon";
import styles from "./index.module.css";

export default function Loading() {
  return (
    <main className={styles.main}>
      <div className={styles.logoWrapper}>
        <Icon />
        <div className={styles.frontLogoWrapper}>
          <Icon />
        </div>
      </div>
      <div className={styles.textWrapper}>
        <p>Knock knock...</p>
        <p>{"Who's there..?"}</p>
        <p>The DANGER!</p>
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* Same logo CSS file  */

.textWrapper {
  /* Define which element the p tag is relative to.*/
  position: relative;
  width: 30rem;
  height: 3rem;
  overflow: hidden; /* hides texts out of the div */
  margin-top: 2.5rem;
}

.textWrapper p {
  position: absolute;
  text-align: center;
  width: 100%;
  color: #30693e;
  font-weight: 700;
  font-size: 2rem;
}

/*
  showOut - slide from center to top and cover the text
  showIn  - slide from bottom to center and uncover the text
*/

/*Knock-knock - stays 600ms in screen*/
.textWrapper p:nth-child(1) {
  animation: showOut 300ms ease-out 600ms forwards;
}

/*Who's there..? - enter after 600ms and stays 1.4s (2s - 600ms) in screen*/
.textWrapper p:nth-child(2) {
  top: 3rem;
  animation: showIn 300ms ease-out 600ms forwards,
    showOut 300ms ease-out 2s forwards;
}

/*The DANGER! - enter after 2s and stays 1s in screen*/
.textWrapper p:nth-child(3) {
  top: 3rem;
  animation: showIn 300ms ease-out 2s forwards;
}

@keyframes showIn {
  from {
    top: 3rem;
  }

  to {
    top: 0rem;
  }
}

@keyframes showOut {
  from {
    top: 0;
  }

  to {
    top: -3rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

In summary,

  1. Wrap the text in a div tag with relative position
  2. Position as absolute the texts.
  3. Overflow hidden to cover the texts
  4. Animate each text to show and cover subsequently

Step 3 - Let's put the pieces together

In Next.js 13, we have a really cool feature called route groups. One of its many possibilities is that it allows the developer to define a unique layout for pages inside the group. Knowing this, here we'll use this approach to render the loading component before selected pages in just one place. Additionally, we'll use useEffect() and useState(), which are hooks from the React library, with setTimeOut() to simulate request response time. These three together allow us to schedule the loading state change after a while and to make sure it happens only once.

Route group example

route group example

Layout

"use client"

import Loading from "@/components/Loading";
import { useEffect, useState } from "react";

export default function Layout({
    children,
  }: {
    children: React.ReactNode
  }) {
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setTimeout(() => setLoading(false), 3000);
  }, []);

  return loading ? <Loading /> : <>{children}</>;
}
Enter fullscreen mode Exit fullscreen mode

Final result

You can fully access the application here.
app working

Conclusion

So... The idea to create this tutorial came from an encouragement from my tech lead Jessica when I had developed a loading component for the voluntary project known as Lacrei Saúde. Additionally, I would like to extend my thanks to Daniel for reviewing this article and suggesting some improvements. As promised, I'm leaving the application repository here, and I also have a question: Have you had water today?

Top comments (1)

Collapse
 
danieldutrabr profile image
Daniel Dutra

Incrível!