ReactHustle

How to Create a React Skeleton Loader Component with TailwindCSS and NextJS

Jasser Mark Arioste

Jasser Mark Arioste

How to Create a React Skeleton Loader Component with TailwindCSS and NextJS

Hello, hustlers! In this tutorial, you'll learn how to create a dynamic react skeleton loader component using TailwindCSS. 

Why use a skeleton loader? #

A skeleton loader offers the user a simple preview of the loading content and decreases the perceived load time. Avatars, cards, lists, tables, and content blocks are good candidates for using a skeleton loader.

When to use a skeleton loader? #

  1. On heavy content pages like dashboards, message apps, cards, etc. 
  2. When there are multiple elements that require a loading indicator.
  3. On dynamically loaded components. For example, NextJS offers code splitting by using the dynamic import feature.

Final Output #

This is what we'll be making today:

React SkeletonLoader Component Tutorial Final Output

Tutorial Objectives #

Here are our tutorial objectives, We're going to make 3 components:

  1. Make a dynamic SkeletonLoader component that has a pulse animation. It can accept children components and animate them.
  2. Make a SkeletonCircle component
  3. Make a SkeletonRectangle Component.

Step 1 - Project Setup #

First, we have to install tailwind as a dependency. If you're using NextJS, you may check my previous tutorial: How to Set Up NextJS and TailwindCSS in 2023. If you're using some other react framework you may check the tailwind documentation on how to install it.

Step 2 - Creating a Skeleton Loader component #

First, let's create the SkeletonLoader component. This is just a wrapper component with a pulse animation. First, create the file components/SkeletonLoader.tsx and copy the code below:

import { ComponentPropsWithRef } from "react";

// React 18 example
// Use the div element props
type DivProps = ComponentPropsWithRef<"div">;

// Define the component. this is basically a div wrapper with a pulse animation for the skeleton loading
const SkeletonLoader: React.FC<DivProps> = ({
  children,
  className,
  ...props
}) => {
  return (
    <div className={["animate-pulse", className].join(" ")} {...props}>
      {children}
    </div>
  );
};
export default SkeletonLoader;
12345678910111213141516171819

To use this component, we can simply put any child elements there and it will create a basic skeleton loader. For example:

// pages/index.tsx
import type { NextPage } from "next";
import SkeletonLoader from "../components/SkeletonLoader";

const Home: NextPage = () => {
  return (
    <div className="container mx-auto">
      <SkeletonLoader className="flex gap-2 my-2 w-80">
        <div className="h-12 w-12 rounded-full bg-gray-200 flex-shrink-0"></div>
        <div className="w-full flex flex-col gap-2">
          <div className="h-5 bg-gray-200"></div>
          <div className="h-5 w-1/2 bg-gray-200"></div>
        </div>
      </SkeletonLoader>
    </div>
  );
};

export default Home;
12345678910111213141516171819

The output will be like this:

React skeleton loader step1 output.

Not bad at all. We're just defining the layout and how the children are set up. With TailwindCSS, we can use the utility classes to create a very flexible implementation. However, you'll also end up with a lot of code. 

Most skeleton loaders have circle and rectangle components. That's what we'll be doing in the next steps.

Step 3 - Creating a SkeletonCircle component #

Let's create a skeleton circle component. We are able to specify the size. But we still keep it very flexible by allowing us to override the className prop. Create the file components/SkeletonCircle.tsx:

// components/SkeletonCircle.tsx
type Props = {
  className?: string;
  size: number;
};

const SkeletonCircle = (props: Props) => {
  //use default styles but allow override
  const className = props.className ?? "rounded-full flex-shrink-0 bg-gray-200";
  return (
    <div
      className={className}
      style={{ height: props.size, width: props.size }}
    />
  );
};
export default SkeletonCircle;
1234567891011121314151617

Let's refactor the previous example to use this component:

// pages/index.tsx
import type { NextPage } from "next";
import SkeletonCircle from "../components/skeleton/SkeletonCircle";
import SkeletonLoader from "../components/skeleton/SkeletonLoader";

const Home: NextPage = () => {
  return (
    <div className="container mx-auto">
      <SkeletonLoader className="flex gap-2 my-10 w-80">
        <SkeletonCircle size={48} />
        <div className="w-full flex flex-col gap-2">
          <div className="h-5 bg-gray-200"></div>
          <div className="h-5 w-1/2 bg-gray-200"></div>
        </div>
      </SkeletonLoader>
    </div>
  );
};

export default Home;
1234567891011121314151617181920

Step 4 - Creating a SkeletonRectangle component #

For the SkeletonRectangle component, we want to be able to specify the height and width of each rectangle, how many rectangles to render, and the spacing/gap between each one.

First, create the file components/SkeletonRectangle.tsx:

// components/SkeletonRectangle.tsx
// Define the props
type Props = {
  lines?: number;
  gap?: number;
  height?: number;
  className?: string;
  unEqualWidth?: boolean;
};
// Add prop defaults
const SkeletonRectangle: React.FC<Props> = ({
  gap = 4, //4px
  lines = 1,
  height = 20, //20px
  className = "",
  unEqualWidth,
}) => {
  const items = new Array(lines || 1).fill("x");
  return (
    <div className="w-full flex flex-col" style={{ rowGap: gap }}>
      {items.map((_, index) => {
        const len = items.length;
        const isLast = index === len - 1;
        const moreThanOne = len > 1;
        // use half-width if it's the last element
        const width =
          isLast && unEqualWidth && moreThanOne ? "w-1/2" : "w-full";
        return (
          <div
            key={index}
            style={{ height }}
            className={[width, className].join(" ")}
          />
        );
      })}
    </div>
  );
};
export default SkeletonRectangle;
123456789101112131415161718192021222324252627282930313233343536373839

Now, we have a very flexible SkeletonRectangle component that can accommodate pretty much any use case. For example, to use this component:

// pages/index.tsx
import type { NextPage } from "next";
import SkeletonCircle from "../components/skeleton/SkeletonCircle";
import SkeletonLoader from "../components/skeleton/SkeletonLoader";
import SkeletonRectangle from "../components/skeleton/SkeletonRectangle";

const Home: NextPage = () => {
  return (
    <div className="container mx-auto">
      <SkeletonLoader className="flex gap-2 my-10 w-80">
        <SkeletonCircle size={48} />
        <SkeletonRectangle
          lines={3}
          unEqualWidth
          gap={8}
          className="bg-gray-200 rounded-md"
        />
      </SkeletonLoader>
      <SkeletonLoader className="w-80">
        <SkeletonRectangle height={100} className="bg-gray-200 rounded-md" />
      </SkeletonLoader>
    </div>
  );
};

export default Home;
1234567891011121314151617181920212223242526

Here's the output: 

React Dynamic Skeleton Loader

Now you've got a pretty legit-looking SkeletonLoader component!

Full Code and Demo #

The full code is available on GitHub: jmarioste/next-skeleton-loader-tutorial

The demo is available on Stackblitz: Next Skeleton Loader Tutorial

Conclusion #

We learned how to create a very customizable and reusable react skeleton loader component. TailwindCSS really helped us a lot by offering utility classes so we don't have to think much about styling and customization.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Credits: Image by Ales Krivec from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle