Today, after waiting for an album review to load, I built something small: my own take on the Pitchfork aggregate review loading experience.

Pitchfork uses a Skeleton-style UI while returning an album’s aggregate review, such as “8.0” for Mastodon’s Crack the Skye. Skeleton UIs show you something, like blurred text or a flashing bar, to build anticipation and fill out the page while in a loading state.

They are a fantastic UX touch for the right kind of app and data. In the case of the Pitchfork app, the aggregate review is a pivotal piece of information about an album, and is presumably being calculated on demand, so this choice makes sense.

My version

Here’s the version that I built. I added a review summary sentence, which was not part of the original design.

Why did I build this? I had curiosity and time. I’ve also felt that React Skeleton UI libraries always seem to do more than I want them to. I was wondering if I could build something smaller for a constrained use case, and I did.

The module code

I built this in two parts: a Node module handling the skeleton logic, and an application to consume it. I’ll share a few small learnings at the end.

Here’s the module:

// react-skeleton-loader/index.tsx
export interface SkeletonTextProps {
  blur?: number;
  className?: string;
  placeholder: string;
  value?: string;
}

export function SkeletonText({
  blur = 4,
  className,
  placeholder,
  value,
}: SkeletonTextProps) {
  const loading = value === undefined;
  const blurRadius = Math.min(12, Math.max(0, blur));

  return (
    <div className={className}>
      <span
        style={{
          filter: loading ? `blur(${blurRadius}px)` : 'none',
        }}
      >
        {loading ? placeholder : value}
      </span>
    </div>
  );
}

A couple of noteworthy points:

  • A blur prop that uses filter: blur(radius) to customize how blurred the placeholder is on a scale of 0-12. I wanted the album’s rating to be more blurred than the copy, because it has a pretty consistent shape that a user can mentally “fill in” even when heavily blurred.
  • Inline styling and a placeholder value to create the skeleton experience.
  • A className so users can style the component; this library is about behavior, not style.

The consumer code

And here’s the consumer, edited for clarity.

// App.tsx
import {SkeletonText} from 'react-skeleton-loader';
import {useState} from 'react';

const App = () => {
  const [rating, setRating] = useState(undefined);
  const [copy, setCopy] = useState(undefined);

  const initiateLoading = () => {
    setTimeout(() => {
      setRating('9.1');
      setCopy('Back on the right track');
    }, 3000);
  };

  return (
    <>
      <button onClick={initiateLoading}>Initiate loading</button>

      <SkeletonText blur={9} placeholder="0.0" value={rating} />
      <SkeletonText blur={3} placeholder="A world appears" value={copy} />
    </>
  );
};

When rating or copy are defined, the skeleton UI considers itself loaded and replaces the placeholder with the new value. Our 3000ms wait time simulates loading.

What I learned

I’m going to share a few TILs here, now and as I add them.