red line

Skeletons in your website

Skeleton loaders have gained popularity in recent years as a more user-friendly alternative to traditional loading spinners.

People are impatient. It’s better to quickly show a user a partially loaded page than to wait until all the information is gathered to show a fully populated page. For example, people generally like to see the text on a page before all the images and forms load in.

People don’t enjoy seeing page elements move around on the page as items load in. The shifting of page elements as content loads in is called Cumulative Layout Shift (CLS).

A way to show impatient users what the page is going to look like while reducing CLS is with the use of skeleton loaders. Skeleton loaders show a rough outline of what certain elements on the page will look like. Later, when those elements load in, there is no CLS if the skeleton loader is sized correctly.

Take a look at the example below. Click, “Reload Form” to see the skeleton loader. A few seconds later the form will load in.

Skeleton loaders are superior to loading spinners because skeleton loaders only cover a portion of the page and skeleton loaders give the user a hint of what’s going to be loaded in.

The main challenge of setting up a skeleton loader is that you must generally know the exact width and height of the content the skeleton loader is filling in for all screen sizes.

Let’s go over the general steps to make a skeleton loader. Once we’re done, the code for the example above should make more sense to you.

Step One:

To get started with a skeleton loader, first, create a simple pattern with the same general shape as the content that will load in. Use simple shapes like rectangles and ovals. Make sure your skeleton matches the size of what’s loading in at each screen size.

To get a picture in your mind’s eye of what a skeleton loader is take a look a this example:

It is extremely important to test out your skeleton at all screen sizes. The reason I suggest building skeleton loaders from HTML and not using an image facade is that it’s much easier to create a responsive skeleton loader using divs than a static image.

Step Two

Once your skeleton is in place, the next step is to add an animation to indicate that the skeleton loader is showing content that will come in later.

There are many animations to pick from. I find that the simplest one is an alternation of the opacity over time. Changing the opacity over time can be done with keyframes:

					@keyframes pulse {
  0% { opacity: 0.7; }
  50% { opacity: 1; }
  100% { opacity: 0.7; }

Once the keyframes are set up, you can attach the keyframes to the container element of the skeleton loader.

Another option to consider is adding a shimmer. Take a look at this CSS Tricks post to see how a shimmer can be implemented.

Step 3

The final step is to write the JavaScript that hides the skeleton and shows the fully loaded content at the correct time. The general approach is to find the correct event to listen to. For example, <img> tags have a ‘load’ event that fires when the image comes in. HubSpot forms trigger an ‘onFormReady‘ event once the form is fully loaded and ready to show.

Now that you’re familiar with the general steps required to build a skeleton loader, let’s build one for HubSpot.

First, we’ll start by looking at our form and creating a skeleton that roughly looks like the form and is the correct height for all screen sizes. For this example, we don’t need any media queries, but if you have multi-column forms, you probably will need media queries to change the height of the form at various screen sizes.

Below is what the final form will look like:

To achieve the look above, we’ll add the skeleton HTML below. The skeleton and the final form are sibling elements and share a single parent container. Putting both the final form and the skeleton in the same parent container makes it easier to control the simultaneous hiding of the skeleton and showing of the complete form.

					<div id="form1" class="skeleton-loader-container">  
  <div class="skeleton">
    <div class="skeleton-label"></div>
    <div class="skeleton-input"></div>
    <div class="skeleton-label"></div>
    <div class="skeleton-input"></div>
    <div class="skeleton-label"></div>
    <div class="skeleton-input"></div>
    <div class="skeleton-submit"></div>
  <div id="form-container"></div>

The above was Step One. The next step is to style the skeleton. This is a simple form with repeating visual elements, so the trick is to figure out how tall the labels, inputs, and submit button are. After a bit of experimenting, I came up with the below:

					.skeleton-label {
  height: 1.3rem;
  width: 5rem;
  background-color: lightgray;
  margin-bottom: 0.1rem;
  margin-top: 0.8rem;
.skeleton-input {
  height: 2rem;
  width: 100%;
  background-color: lightgray;
  margin-bottom: 0.1rem;
.skeleton-submit {
  height: 3.4rem;
  background-color: lightgray;
  margin-top: 1rem;

Now we’re done with Step Two. The final step is to use JavaScript to hide the skeleton at the correct time. We’ll also make sure that the form is initially hidden, so it doesn’t show up partially loaded in. The JavaScript will have to listen for the HubSpot onFormReady event. I found this event name by looking at the HubSpot documentation.

We’ll add the class ‘loaded’ to the parent container once the HubSpot form is ready to show. Note that if there can be two HubSpot forms on the same page, then we’d have to check which HubSpot form is ready to show inside the HubSpot listener callback.

					window.addEventListener('message', event => {
	if( === 'hsFormCallback' && === 'onFormReady'
	    ) {


Finally, we can use CSS to display the skeleton at the correct time:

					/* Initial state */
.#form-container {
  display: none;

/* Form loaded */
.loaded > .skeleton {
  display: none;
.loaded > #form-container {
  display: block;

You can take a look at the HTML, CSS, and JavaScript of the final version below.

Each skeleton loader you create will be slightly different. The details of when to hide the skeleton have to be determined for each service you’re building a skeleton loader for, but these things are details you can figure . What’s important is that you understand the general principles outlined above of how to build a skeleton loader.

Related resources