Today, after waiting for a Pitchfork review to load, I built something small I’d like to share: 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 “6.9” for Mastodon’s Marrow Deep. Skeleton UIs show you something, like blurred text or a flashing bar, to build anticipation and fill out the page in a load 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 possibly being calculated on demand, so this choice makes sense.

My version

Here’s the version that I built. I added skeleton loading on a review summary, 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. The album’s rating will be more blurred than copy below it .
  • 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';

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

We calculate rating and copy in a side-effect with a few-seconds delay, to simulate the loading experience.

What I learned

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