Building Pixel-Perfect Skeleton Loaders in CSS

Skeleton loaders are a common alternative to the ol’ reliable loading spinner. If you have a good idea of how much space the content will take once it’s finished loading, then skeleton loaders can provide a pretty seamless user experience. But for the effect to work, the skeleton needs to be near-perfect to avoid layout shift.

There’s lots of different techniques for building skeleton loaders in CSS. Graphical elements are reasonably easy to swap out with simple CSS shapes or inline SVGs, but text makes things more difficult. There’s a lot of variables that affect the dimensions of a text element, so it’s easy to end up with a skeleton that’s just a little bit off. To get that pixel-perfect layout, you need to use the browser’s text layout engine to lay out the non-text placeholder elements.

Dummy text is one possible approach, but I’m not a fan. It feels like too much of a hack for me, and I want to be able to set the exact width of the placeholder via the width property. While working on Hacker Neue I stumbled upon a technique that cleanly fulfills these requirements.

(I’ll be using Tailwind CSS classes in these examples for brevity.)

We’re going to start with the content we want to skeletonize:

<article>
	<div class="text-sm font-medium uppercase text-gray-700">
		Lorem ipsum dolor
	</div>
	<div class="text-xl font-semibold leading-tight">
		Lorem Ipsum Dolor Sit Amet
	</div>
	<div class="mt-1 leading-snug">
		Lorem ipsum dolor sit amet
	</div>
</article>

First, we’ll strip the code down to just its basic layout. I picked an example with a good variety of font sizes and line heights, and we’ll be careful to preserve those.

<article>
	<div class="text-sm"></div>
	<div class="text-xl leading-tight"></div>
	<div class="mt-1 leading-snug"></div>
</article>

Since we need something with a settable width and text-like layout, my first thought was to use element with display: inline-block as the placeholder. We also need to add a little bit of text content (&nbsp;) to give the element some height.

<div>
	<span class="inline-block bg-gray-300 w-64">&nbsp;</span>
</div>

This works, but it looks a little… chunky?

This has to do with how text layout works in the CSS spec. There’s a distinction between the the vertical space taken up by the font’s characters and the whitespace added around each line by the line-height property. For inline-block elements, their boundaries include that extra whitespace. inline elements don’t include the line-height space (nice), but you can’t set the width of those elements (lame).

The solution is to use both. We’ll add two elements: an outer inline element that has the background color applied to it, then an inner inline-block that we can set the width on. We don’t even need our &nbsp; anymore.

<div>
	<span class="bg-gray-300">
		<span class="inline-block w-64"></span>
	</span>
</div>

Here’s what that looks like when we apply it to our original example:

<article>
	<div class="text-sm">
		<span class="bg-gray-300">
			<span class="inline-block w-32"></span>
		</span>
	</div>
	<div class="text-xl leading-tight">
		<span class="bg-gray-300">
			<span class="inline-block w-72"></span>
		</span>
	</div>
	<div class="mt-1 leading-snug">
		<span class="bg-gray-300">
			<span class="inline-block w-48"></span>
		</span>
	</div>
</article>

Then just tweak the border radius, sprinkle in a bit of animation, and we get a pretty nice looking skeleton loader.

Bonus tip: if you need line breaks, you can just keep adding more inner elements. Each inline-block element can wrap like words in a sentence.

<div>
	<span class="bg-gray-300">
		<span class="inline-block w-32"></span>
		<span class="inline-block w-32"></span>
		<span class="inline-block w-32"></span>
		<span class="inline-block w-32"></span>
	</span>
</div>