Doğukan Çavuş

developer, wannabe designer

Rethinking skeleton loaders with tailwindcss

When faced with a task of implementing customizable skeleton screens feature, I embarked on researching skeleton loader libraries, but each had its drawbacks.

Some libraries generate SVG output, which introduce several disadvantages. It would become necessary to work only with SVG attributes or we would have to manually create them using design tools like Figma. And the only way to achieve responsiveness was either through scaling or by creating separate components for desktop and mobile views. Unfortunately, scaled skeleton loaders don't offer an aesthetically pleasing visual.

Some libraries were generating HTML div elements based on the given props. They often come with inline styles and contain code for many props that we may never even use. They also utilize the Context API to create themes, which adds to the library's size and client payload. Essentially, they only provide a shine animation and props, like count={8}.

But do we really need a library for such a simple requirement? Especially if we're already using tailwindcss?

Yes, indeed, I can't find a valid reason to use a library for such a straightforward task. We can create this animation ourselves, exactly the way we want. Why should we stick to the style provided by a library?

Will you use pre-styled, ready-to-use loader components? Will the designer provide you with a placeholder SVG for each component that perfectly matches the design? If the answer is no, you will have to create and place simple shapes yourself. So there is no reason not to create your own components.

Pros & Cons

Pros:

  • Make it responsive by using tailwindcss classes.
  • Customize the animation as you want.
  • Framework agnostic.
  • No extra dependency, no Context, no more useless code.
  • No SVG complexity.
  • Flexible. Take full control.
  • Perfect match with texts.

Cons:

  • Nothing.

Creating skeleton loaders with tailwindcss

It's pretty straightforward. We need to add a custom config just for the animation. Add this to your tailwind config:

tailwind.config.js
module.exports = {
  theme: {
    // ...
    extend: {
      animation: {
        loading: 'loading 2s linear infinite',
      },
      keyframes: {
        loading: {
          to: { 'background-position': 'left' },
        },
      },
    },
  },
};

and add .skeleton class to your styles.

globals.css
@layer components {
  .skeleton {
    background: linear-gradient(-45deg, #0000 40%, #fff5, #0000 60%) right/300% 100%;
    @apply animate-loading;
  }
}

It's compatible with any background color. Here is a simple skeleton:

<div class="skeleton w-64 h-16 bg-neutral-200"></div>

We can create components and make use of props:

Skeleton.jsx
export default function Skeleton({ className, count = 1 }) {
  return (
    <>
      {Array(count)
        .fill(null)
        .map((_, i) => (
          <div key={i} className={clsxm('skeleton', className)} />
        ))}
    </>
  );
}
 
// <Skeleton count={3} className="bg-neutral-200 h-5 w-64 mb-3" />

We haven't missed out on any of the benefits provided by skeleton libraries so far, have we? Then let's go beyond that.

Perfect Placeholder That Matches Text Sizes Exactly

I have never seen this technique before in any skeleton library. Honestly, this is my favorite feature.

Just put a non-breaking space entity (&nbsp;) into the div. Since this invisible whitespace character is actually a text and responds to properties like font-size and line-height, it allows us to derive the height of our skeleton from the line-height. This ensures that the height of the skeleton exactly matches the height of the text to be loaded. No Cumulative Layout Shift.

You need to use a width class for controlling the width and a text size class for controlling the height. Do not use a height class, as the height is determined by the font-size and line-height. This is the trick.

SkeletonText.jsx
function SkeletonText({ className }) {
  return <div className={clsxm('skeleton', className)}>&nbsp;</div>;
}
 
// <SkeletonText className='text-sm bg-neutral-200 w-56' />
// <SkeletonText className='text-lg bg-neutral-200 w-24' />
 
 

After delving into the possibilities of skeleton loaders, it becomes clear that relying on a external library is unnecessary. Why settle for cookie-cutter solutions offered by skeleton loader libraries when you can craft loading experiences that are uniquely yours, all with the power of tailwindcss?

Updated at