Paramanantham Harrison Web and mobile app developer. Love exploring the depth of JS full-stack development. React, Vue, React Native, Next JS, GraphQL are my current love interest. https://learnwithparam.com

Improve UX in React apps by showing skeleton UI

7 min read 2015

Learn how to use the skeleton UI to improve UX

Introduction

A skeleton screen is a UI that doesn’t contain actual content; instead, it shows the loading elements of a page in a shape similar to actual content.

Skeleton screens show users that content is loading, offering a vague preview of how content will look once it fully loads.

Frontend devs use skeleton UIs for a variety of reasons.

Chief among these are the UI’s capacity to visually streamline the user experience, mimic content loading speed, and progressively load content without requiring that all content on a page is fetched at once.

Slack, Youtube, Facebook, Pinterest, and other big tech companies display skeleton screens while their content loads to boost UX.

An example of a skeleton screen used by Medium
Medium content loading
An example of Medium using a skeleton UI to imrove UX.
Medium skeleton UI

In addition to skeleton screens, these user interfaces are commonly referred to as content placeholders, content loaders, and ghost elements.

How skeleton screens improve UX

Skeleton UI resembles the real UI, so users have an understanding of how quickly the website loads even before content shows up. Let’s see it in action in a comparison of the two screens:

The facebook page without a skeleton UI
Empty Facebook page without loader
An example of facebook with a skeleton UI
Empty Facebook with skeleton UI

Neither screen has loaded actual content, but the empty page seems slower to the user while the skeleton screen looks richer, seems faster, and feels more responsive.

Even though the real content loads at the same speed for both screens, the skeleton screen offers a superior UX.

Different skeleton UI

There are a few different kinds of skeleton UI. The major ones are content placeholders and image (or color) placeholders.

Companies like Medium, Slack, and Youtube use content placeholders in skeleton UI on their main pages.

They’re easy to build because they don’t require any details about actual content data and instead only mimic the UI.

Meanwhile, Pinterest and Unsplash — two image-heavy websites — use color placeholders. Color placeholders are harder to build because they require details about actual content data.

How it works

First, load a skeleton instead of images (usually with a gray or off-white background).

Once the data is fetched, load the actual color of the image from the image metadata.

This metadata is derived from the image while uploading through backend algorithms, as well as processing on top of the images.

Finally, lazy load the images to allow the user to actually view the content using the intersection observer API.

Demo

In our tutorial, we’re going to explore skeleton UI in React by creating a mock of the YouTube main page.

Before we get started, let’s list off the most popular packages for skeleton UI development already available in React:

These packages are pretty well-maintained, but they have their faults. We’ll look at the pros and cons of each before deciding which to use for our application.

React content loader

Pros

  • SVG-based API; you can use any SVG shapes to create the skeleton elements
  • Easy to create animated placeholder, which shines from left to right (pulse animation)
  • Has a few pre-styled content loaders (e.g, Facebook, Instagram, etc)
  • Can be used for any complex skeleton UI since SVG supports a lot of shapes

Cons

  • You need to create custom skeleton components for all your components separately
  • SVG is not the same as CSS elements, so creating custom elements with custom alignment requires a steep learning curve
  • Browser support might be inconsistent because of the SVG dependencies, so skeleton might look and feel different on different browsers

Here’s an example of a skeleton component using react-content-loader :

import ContentLoader from "react-content-loader";
    
    // API support all SVG shapes - rect is a SVG shape for rectangle
    const SkeletonComponent = () => (
      <ContentLoader>
        <rect x="0" y="0" rx="5" ry="5" width="70" height="70" />
        <rect x="80" y="17" rx="4" ry="4" width="300" height="13" />
        <rect x="80" y="40" rx="3" ry="3" width="250" height="10" />
      </ContentLoader>
    )

React placeholder

Pros

  • Component based API
  • Easy to create custom skeleton UI using the placeholder components
  • Supports pulse animation, which you can control through props

Cons

  • Similar to React content loader, we need to maintain a skeleton component separately, so updating styles to a component requires possible updates to the skeleton component as well
  • Learning curve is not very linear since there are multiple components for different needs

The following is an example of a skeleton component using react-placeholder:

import { TextBlock, RectShape } from 'react-placeholder/lib/placeholders';
import ReactPlaceholder from 'react-placeholder';

// 
const MyCustomPlaceholder = () => (
  <div className='my-custom-placeholder'>
    <RectShape color='gray' style={{width: 30, height: 80}} />
    <TextBlock rows={7} color='yellow'/>
  </div>
);

// This is how the skeleton component is used
<ReactPlaceholder ready={ready} customPlaceholder={<MyCustomPlaceholder />}>
  <MyComponent />
</ReactPlaceholder>

React loading skeleton

Pros

  • Very simple API — it just has one component with props for all customization
  • Pretty easy to learn
  • Can be used as separate skeleton component and also inside any component directly, so it’s flexible to use the way we want
  • Supports animation and theming

Cons

  • Very good for simple skeleton UI, but difficult for complex skeletons

The following is an example of a React loading skeleton:

import Skeleton, { SkeletonTheme } from "react-loading-skeleton";

const SkeletonCompoent = () => (
  <SkeletonTheme color="#202020" highlightColor="#444">
    <section>
      <Skeleton count={3} />
      <Skeleton width={100} />
      <Skeleton circle={true} height={50} width={50} />
    </section>
  </SkeletonTheme>
);

For the full demo, we’ll use react-loading-skeleton.

That said, all three libraries adequately satisfy simple use cases. Feel free to go through the documentation and choose the one you feel most comfortable using in your application.

Skeleton UI example using React

We’re going to build a YouTube-like UI and show how skeleton UI works.

First, let’s create the YouTube UI:

import React from "react";
    // Youtube fake data
    import youtubeData from "./data";
    // Styles for the layout
    import "./App.css";
    
    // Each Card item component which display one video - shows thumbnail, title and other details of a video
    const Card = ({ item, channel }) => {
      return (
        <li className="card">
          <a
            href={`https://www.youtube.com/watch?v=${item.id}`}
            target="_blank"
            rel="noopener noreferrer"
            className="card-link"
          >
            <img src={item.image} alt={item.title} className="card-image" />
            <h4 className="card-title">{item.title}</h4>
            <p className="card-channel">
              <i>{channel}</i>
            </p>
            <div className="card-metrics">
              {item.views} &bull; {item.published}
            </div>
          </a>
        </li>
      );
    };
    
    // Card list component
    const CardList = ({ list }) => {
      return (
        <ul className="list">
          {list.items.map((item, index) => {
            return <Card key={index} item={item} channel={list.channel} />;
          })}
        </ul>
      );
    };
    
    // App component - each section have multiple videos
    const App = () => {
      return (
        <div className="App">
          {youtubeData.map((list, index) => {
            return (
              <section key={index}>
                <h2 className="section-title">{list.section}</h2>
                <CardList list={list} />
                <hr />
              </section>
            );
          })}
        </div>
      );
    }
    
    export default App;

Next, let’s input fake YouTube data:

const youtubeData = [
  {
    section: "JavaScript Tutorials by freeCodeCamp",
    channel: "freeCodeCamp.org",
    items: [
      {
        id: "PkZNo7MFNFg",
        image: "https://img.youtube.com/vi/PkZNo7MFNFg/maxresdefault.jpg",
        title: "Learn JavaScript - Full Course for Beginners",
        views: "1.9M views",
        published: "9 months ago"
      },
      {
        id: "jaVNP3nIAv0",
        image: "https://img.youtube.com/vi/jaVNP3nIAv0/maxresdefault.jpg",
        title: "JavaScript, HTML, CSS - Rock Paper Scissors Game",
        views: "216K views",
        published: "1 year ago"
      }
    ]
  },
  {
    section: "Small steps on React",
    channel: "Learn with Param",
    items: [
      {
        id: "ylbVzIBhDIM",
        image: "https://img.youtube.com/vi/ylbVzIBhDIM/maxresdefault.jpg",
        title: "useState example by building a text-size changer",
        views: "148 views",
        published: "3 days ago"
      }
    ]
  }
];
export default youtubeData

Let’s show the skeleton UI before loading actual data. Since our data is fake, we need to mock it like API data by loading after a two-second timeout:

import React, { useState, useEffect } from "react";

const App = () => {
  const [videos, setVideos] = useState([]);
  // Load this effect on mount
  useEffect(() => {
    const timer = setTimeout(() => {
      setVideos(youtubeData);
    }, 2000);
    // Cancel the timer while unmounting
    return () => clearTimeout(timer);
  }, []);

  return (
    <div className="App">
      {videos.map((list, index) => {
        ...
      })}
    </div>
  );
};

You’ll see a white screen for three seconds, and then the data loads abruptly.

Now, we’ll install react-loading-skeleton:

yarn add react-loading-skeleton

Let’s create a skeleton component for our videos data:

import Skeleton from "react-loading-skeleton";

/* 
   Separate Skeleton component 
  - It is created with the same shape as Card component
  - Pros: Component will be isolated from the skeletons so the component won't become complex or heavy
  - Cons: Maintaining separate skeleton component will make it harder to maintain when UI changes and style gets changed
*/
const CardSkeleton = () => {
  return (
    <section>
      <h2 className="section-title">
        <Skeleton height={28} width={300} />
      </h2>
      <ul className="list">
        {Array(9)
          .fill()
          .map((item, index) => (
            <li className="card" key={index}>
              <Skeleton height={180} />
              <h4 className="card-title">
                <Skeleton height={36} width={`80%`} />
              </h4>
              <p className="card-channel">
                <Skeleton width={`60%`} />
              </p>
              <div className="card-metrics">
                <Skeleton width={`90%`} />
              </div>
            </li>
          ))}
      </ul>
    </section>
  );
};

You can also create a skeleton component by embedding the skeleton directly into the component, like this:

import Skeleton from "react-loading-skeleton";

/*
  Cards component with embedded skeleton UI
  - Pros: This is much easier to maintain for UI and styles changes
  - Cons: UI will become complex and heavy with lot of unnecessary elements in it
*/
const Card = ({ item, channel }) => {
  return (
    <li className="card">
      <a
        href={item.id ? `https://www.youtube.com/watch?v=${item.id}` : `javascript:void(0)`}
        target="_blank"
        rel="noopener noreferrer"
        className="card-link"
      >
        {
          item.image ? 
          <img src={item.image} alt={item.title} className="card-image" /> 
          : 
          <Skeleton height={180} /> 
        }
        <h4 className="card-title">
          {
            item.title ? item.title : 
            <Skeleton height={36} width={`80%`} />
          }
        </h4>
        <p className="card-channel">
          { channel ? <i>{channel}</i> : <Skeleton width={`60%`} /> }
        </p>
        <div className="card-metrics">
          {
            item.id ? 
            <>{item.views} &bull; {item.published}</>
            :
            <Skeleton width={`90%`} />
        </div>
      </a>
    </li>
  );
};

I used the isolated skeleton component in my example, but feel free to use whatever style component best fits your needs. It all depends on personal preference and the complexity of the component.

Finally, here’s the CardSkeleton component before actual data gets loaded:

const App = () => {
  const [videos, setVideos] = useState([]);
  // Manage loading state - default value false
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    // set the loading state to true for 2 seconds
    setLoading(true);
  
    const timer = setTimeout(() => {
      setVideos(youtubeData);
      // loading state to false once videos state is set
      setLoading(false);
    }, 2000);

    return () => clearTimeout(timer);
  }, []);

  // Show the CardSkeleton when loading state is true
  return (
    <div className="App">
      {loading && <CardSkeleton />}
      {!loading &&
        videos.map((list, index) => {
          return (
            <section key={index}>
              <h2 className="section-title">{list.section}</h2>
              <CardList list={list} />
              <hr />
            </section>
          );
        })}
    </div>
  );
};

We now have a fully-functional example of a skeleton UI. Our example loads the skeleton for 2 seconds before showing the data. See it in action here.

Codebase for this example is available in Github. I’ve written the branches so you can run all intermediate stages and see the differences.

Conclusion

Skeleton screens significantly improve UX by mitigating the user frustrations associated with entirely blank screens and giving users an idea of what content will look like before it loads.

It’s easy to use skeleton UI in your React applications.

If you don’t want to use an existing package, you can also create your own skeleton UI pretty easily by creating div elements that mimic skeletons by creating rectangle and circle elements.

Share your experience with using skeleton UI in the comment section.

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Paramanantham Harrison Web and mobile app developer. Love exploring the depth of JS full-stack development. React, Vue, React Native, Next JS, GraphQL are my current love interest. https://learnwithparam.com

Leave a Reply