The Illusion of Speed: Skeleton Screens with React

Waiting for information to load on a website draws several parallels to waiting in real life—whether that’s for the train, for your freshly pressed dress clothes from the dry cleaners down the street, or for the nearest nurse to quit triaging the guy with the broken rib and take a look at your sore throat. And whether the waiting, waiting, waiting you do is virtual or in real life, we can all agree on one thing: It sucks.

Although visitors to your website may file the act of waiting as one of life’s certainties—alongside death and taxes—you can help mitigate this tortuous experience with some clever, visual illusions. This blog post covers a nice little exercise in slimming the perceptive barrier of “Waiting” between your users and their data.

The solution from, say, 2002 prescribes dropping a loading spinner GIF or some other circular image or progress bar to signify that things are taking a bit longer than expected, so sorry for the delay. Thankfully, this trend has fallen out of fashion for reasons similar to why you should never use stock images on your website: They’re impersonal, unoriginal, and they bridge no connection between your user and the sexy content on your platform.

Loading state from

Nowadays, websites have begun using loading screens that are more in tune with the platform’s vision: If they fall back to platitudinous progress bars, at least they spice it up with the product’s general aesthetic. Take a look at this progress indicator: There are some commonalities between the platform’s theme and the loading GIF. (And although the simple design may not promote the depth of the app’s capabilities, at least the user can develop a mental image/design motif for what kinds of patterns and themes to expect throughout the app).

Bringing placeholders closer to their content

Inspiration for Gronda loading states: straight from

Take a look above at the loading state for the ReduxJS website. What can we predict about the content? Well, there’s a button containing an icon and some text on the top right; there’s a section with a heading and a paragraph of text. Top left we have some kind of quote or navigation with a blurb and description text. What does this translate into?

Actual content from the ReduxJS website. Note the form similarities to the loading state.

Surprisingly apt, you are! All those placeholder elements are more or less exactly what we expect: the button, the navigation blurb, and the paragraph. But this act of pulling words and icons out of grey boxes isn’t just a mere magic trick borne out of the Gestalt school of psychology. This technique helps bridge the gap between loading and data—a very attractive consequence is that it reduces the cognitive load on the user: Not only will they know that something is loading, but they can predict the type of content that will load. For first time users, it engenders a helpful mental model of the platform. “Okay, boxes containing this type of information are about this big; I should expect some kind of subtitle underneath a main heading; there’s a button here that might have relevant information to me later on.”

The UX world has christened these states as “skeleton screens” or “skeleton loading”.

Developing skeleton screens in React

A simple solution to recreating skeleton screens like above would be from the React Placeholder package. But we’re developers here: We like to make our tasks needlessly complicated and sacrifice our nasty, brutish, and short lives to the altar of user experience. So let’s season this bad boy.

Here we would like to introduce a library that we’ve dev—just kidding. You need nothing more than react-transition-group for this. (Our platform uses styled-components as well, but these can be accomplished with vanilla CSS.)

We assume you already have familiarity with react-transition-group. If not, the documentation is a nice place to start. We begin by utilizing <TransitionGroup /> to handle all of the “states” we have, like “loading”, “data”, “empty”, and “error”.

The components are arbitrary; focus on the organization of the Transition components.

Note that <TransitionGroup /> requires all the direct children to be <Transition /> components with a specific key. In the above example, we waffle between the “loading” <Transition /> and the “data” one. If the data is loaded, show this data-filled component; if not, show that one. (If you’re only slightly familiar with react-transition-group, note that <TransitionGroup /> handles all of the in={Boolean} props that should be on the <Transition /> children behind the scenes.)

Copy and paste the above code several times, change some stuff in between, and what does this translate to?

Sexy, sexy, skeleton screens.

So according to this placeholder, there’s an avatar in the top left with two lines of text (the top one being more prominent); there are message boxes. On the top left of each message box is a square (probably an indicator or avatar of some sort) with associated text. The top right has a string of text: perhaps some link or metadata; finally, each box has a paragraph.

And what’s really rendered?

Not bad!

Of course the transition state can be improved, but it’s definitely close to what’s actually rendered on screen!

Miscellaneous tricks with skeleton screens

Our toolbox/magician’s hat is ever expanding, and to impart some knowledge to you, dear reader, we’ll share some tricks to embellish your skeleton screen beyond its bare-bones implementation.

Fake words

Paragraphs are made of words, and words are splotches of ink on a page that would like to convey some information by one person to another through light waves (in the case of a computer screen). These words are composed of letters, and each word is of different length. Words tend to be separated by spaces in many languages. So from the bottom-up, we need to create an element that is variable in width (not necessarily height), has some margin, and can replicate the appearance of ink on paper (or screen).

Lots of words that make up a fake word.

Again, we use styled-components for this guy. Note how we opt to pass height and width as attributes to create a style object rather than as something passed to the template string. Learning the hard way, we discovered that styled-components generates classes for every different combination of props. So the class hash for width={36} will be completely different from that of width={35} though their widths differ by only one pixel. Hundreds of classes would be generated for no good reason. Keep it simple and pass those highly variable props to the style object.

Fake paragraphs

And since paragraphs are just strings of words, let’s string them together.

Is a paragraph really a paragraph without a double carriage return? If so, then this is a couple of paragraphs that creates a paragraph.

Note that the line-height CSS property is changed as well. This is to compress the lines of “text” placeholder.

Transition animations

Where this becomes oh-so charming is by having react-transition-group animate the entrances and exits of these components, like a beautifully choreographed, although sometimes jittery, dance on your webpage. We leverage the render prop of the <Transition /> component to do so.

Passing the transitioning state as a dataset attribute makes choreographing transitions much easier.

Note how that the <AnimatedPlaceholder /> component passes the render prop as data-state. Rather than muck around with a cacophony of booleans like

state === 'entered' && state !== 'exiting' ?
'class-name' : state === 'entering' ?
'another-class-name' : null

we assert the transitioning state as data-state={state}.

Using styled-components makes targeting these transition states much easier. (Of course, there are some performance considerations apropos of targeting attributes versus class selectors, but as always, benchmark before you optimize!)


This method is not without caveats, however. Here’s a list, though definitely not an exhaustive one.

You’re increasing the amount of manageable code…

Note that integrating transition states and auxiliary components adds much more code to what was originally a simple React component. In some cases it might be easier to wrap everything in React Placeholder, since that API is much more manageable and predictable. But for edge cases, it might be best to develop an in-house solution.

This is even more apparent when you’re integrating more than one kind of “state” for your placeholder. The above example only considers the “data” and the “loading” states—essentially a binary “yes, data is here” or “no, data isn’t”. Just two data states, and it takes what was originally three lines (the <Link /> tag sandwich) and blows it up it like an aggressive patch of kudzu.

…that might be a nightmare to debug

And whoever has to untangle the ruthless knots and twigs and vines of your kudzu code has to know react-transition-group (or insert placeholder library here) on a deep level.

Change the component? Change the placeholder, too

This might go without saying, but if you decide to introduce skeleton screens or something similar on your platform, make sure everyone’s on board and is comfortable with authoring parallel changes between component style and placeholder style. But this may not provide to be a huge problem. If the placeholder kinda, sorta, “yeah maybe” looks like the actual component when you’re squinting, then you’re fine.

You may have to manually manage element dimensions

The above example requires adding a bunch of empty <span /> tags, and without any content, those elements have no height. If the end goal is having a 1:1 correspondence between your loading states and your components, you should have a pretty solid idea of e.g., how tall the text tags will be, the precise dimensions of images, the average paragraph length, among other things. The implementation on Gronda has a dynamic number of “words” for each placeholder paragraph, but we had to figure out at a later moment that the placeholder<span /> height had to be in the 8–12 pixel ballpark range. If you’re unsure, you may be making some guesses here and there about e.g., the average length of a string, which in turn might affect how far you transform: translateY(...).

Animation (unsurprisingly) adds another layer of complexity

Our implementation of the User Avatar loading state is a relatively positioned div that has the placeholder div absolutely positioned inside it. If the structure of your components is sensitive to HTML semantics, then you might have to deeply consider whether the “overhead” of having an intercalating layer is worth it. (Some instance I can think of off the top of my head include the accessibility side effects of having a ul > div.transition-layer > li kind of hierarchy. Or animating <table/> rows. But again, YMMV.)

Be sensitive to the engine you’re working on as well! Sometimes the Blink engine might not save the last frame of a transition asynchronously, or maybe the Webkit engine might render a transition that’s too choppy.

Unanimated skeleton screens are truly from beyond the grave

Without any signs of motion or behavior, skeleton screens reflect the very same biological detritus it was named after. Unanimated skeleton screens don’t inform the user much with regards to the loading process: Is the website broken, or loading? Remember, the main goal is to reduce the perceptive wait time. And by bringing in the best parts of progress bars and loading spinners — that dynamic movement translates to “hold on just a moment” — we introduce something not too unfamiliar with user experiences.

Let your placeholder components breathe. Give them a pulsing animation. Cycle the opacity between 0.85–1. Our pulsating User Avatar placeholder has CSS keyframes similar to this:

from, to { transform: scale(1); }
25% { transform: scale(1.15); }

Paired with a nice cubic-bezier transition curve, and by biasing the maximum scaled size closer to one end (instead of at 50%), we paint an animation replicating a human heartbeat—a pattern of nature imbued with the eloquence of the Golden Ratio and with the simplicity of 2 lines of CSS.

Plus, it’s pretty too.


Other-abled users who use screen readers may require some kind of ARIA-flavoring inside your placeholder components. Season them with some combination of aria-disabled={true} and aria-label="loading" or something or other. I’m sure there are better ways to accomplish this, so feel free to comment any kind of recommendations you have!

The jury is still out on its effectiveness

An interesting article from Viget raises a firm eyebrow on the effectiveness of skeleton screens; their hard data suggests that skeleton screens may perform worse for perceived loading time than a loading spinner or a blank screen. Some users in this Twitter thread voice concern saying that the science isn’t sound, citing small sample size, lack of variation of screens, among other complaints.

At best, skeleton screens can really help reduce the imagined amount of time a user has waited for their data. But at worst, pouring development time into React-based loading screens might be an exercise in futility. Nothing beats user testing when deciding to go down the beaten path of loading spinners or the road “less traveled” (ha, who am I kidding?) with skeleton screens.

The bottom line

Experiment a little. Plan a sprint to just implement skeleton screens for one of your website’s/app’s pages. Increase the usability of your website. Cry a little bit when your placeholder animations aren’t cooperating. And if it turns out that this solution isn’t for your team, then so be it. The web is evolving. The bottom line is to make sure your users’ experience isn’t bone dry.