DEV Community

Cover image for Skeleton Mammoth - or how I've been solving the problem of reusable Skeleton Loaders.
Oleksandr Tkachenko
Oleksandr Tkachenko

Posted on

Skeleton Mammoth - or how I've been solving the problem of reusable Skeleton Loaders.

Introduction.

There are a lot of great articles on the Internet devoted to the skeleton loaders, that cover about their types, cases, and needs for their use. I will not list them here, you can easily find them in your favorite search engine.

After investigating this topic in detail, I've decided to create by myself a very simple, flexible, reusable, customizable and lightweight solution that would suit most use cases.
In this article, I will describe the process of creating this solution and turning it into a library, as well as the difficulties that I encountered while working on it.

What Skeleton Loaders are?

Note: You can skip this section if you know what skeleton loaders are.

A skeleton loader, also known as a skeleton screen or content placeholder, is a user interface design pattern used to enhance the user experience during content loading in web and mobile applications. When data is being fetched or processed in the background, instead of displaying a blank or empty screen, a skeleton loader mimics page layout by providing users with a visual cue of what to expect, reducing perceived loading times and mitigating potential frustration.

Here are examples of skeleton loaders from LinkedIn and Youtube:

LinkedIn skeleton screen

YouTube skeleton screen

Why you have to use Skeleton Loaders?

  • Improve user experience: Skeleton loaders enhance the user experience by providing visual feedback and reducing the perception of content loading delays.
  • Reduce bounce rate: Skeleton loaders can prevent users from leaving the page due to loading delays.
  • Smooth transitions: They create smoother transitions between different states of a page or application.
  • Unlike spinners, skeleton loaders attract the user's attention to progress rather than waiting time.

Problems of most existing skeletons.

Taking into account that there are a lot of examples of creating your own skeleton loaders or libraries that did it for you, there are still a number of problems with them.

  • Limited customization: Many existing skeletons have limited customization options. It leads to mismatch of the actual content design and skeleton style.
  • Although their purpose is to provide a visual representation, most of them are not adapted to users with visual impairments or those who using screen readers.
  • Versatility and reusability. Most approaches to creating skeletons offer either creating a shallow copy of components placeholders, resulting in many similar copies, or essentially changing the structure of existing components. Both of the approaches require a lot of additional code and assets.
  • Maintenance complexity: As websites evolve and content changes, keeping skeleton loaders up-to-date can become a maintenance burden.

Skeleton loaders alternatives.

There are several "alternatives" to using skeletons. Looking ahead and answering whether there really are alternatives, my answer is no rather than yes. If we talk about correct usage, then the skeleton is one of the best solutions. Below, I will still give a couple of alternatives, along with their pros and cons.

Spinner.

Spinners, are a common alternative to skeleton loaders. They consist of animated icons that rotate continuously, providing a visual cue that the content is loading.

Pros.

  • Simplicity: Simple implementation that, often requiring only a few lines of code or using pre-designed libraries.
  • Universal understanding: Spinners are widely recognized across different platforms and applications, ensuring users understand that content is loading.

Cons.

  • Limited information: spinners do not provide any context about the content being loaded.
  • Overlaps the entire page or most of it, not individual elements. What gives the feeling of loading not individual elements, but the entire site as a whole.

Spinners are an integral part of interfaces, but they're not exactly suitable for replacing skeletons.

Progress Bar.

A progress bar is a visual element that indicates the completion status of a task or process. It provides a linear representation, typically with a filled portion that grows gradually.

Pros.

  • Precise feedback: Provides accurate and precise feedback on the completion status of a task.
  • Time estimation: Progress bars can give users an estimate of the remaining time required for completion.
  • Multi-Purpose: Progress bars can be used in various contexts and scenarios, making them a versatile component in web and application development.

Cons.

  • Lack of context: In some cases, progress bars might not provide sufficient context about the actual task or process they represent.
  • Implementation complexity: Creating progress bars with accurate representation and smooth animations can be complex, especially when dealing with varying task durations and responsiveness.

Progress bars are more suitable for scenarios showing the progress of a file upload, or quantitative progress. They are often used at the top of pages to show the progress of an entire page loading. But they cannot serve as an equivalent replacement for the skeleton, because they are intended for other purposes.

The absence of any visual.

Yes, not having any loaders or placeholders is also an alternative. And in some cases, this will be a better solution than using unsuitable elements.
The main and probably the only pros is that you don't need an additional time and resources spent on implementations. But here comes the obvious cons – a less attractive design for your site, and the perception of slower loading time.

Creating versatile and reusable skeleton loader.

After I had gained enough knowledge of what a skeletons are, when to use it, what they are, and approaches to their development, I tried to determine for myself what my final result should be.

Versatile and Reusable.
There are a lot of examples over the internet, with overcomplicated approaches, where you have to create a separate skeleton for every component you want to have them on. In my case, I wanted it to be something singular that can be reused for most cases and not stick to any JavaScript framework (like React.js or Vue.js).

Configuration Flexibility.
Since every project and every case can be very different, my skeleton needed to be able to be configurable.

Feature rich.
In addition to the standard set of features, I wanted to fill it with support for additional useful and necessary features.

Lightweight and Dependencies-Free.
Lightweight and as free as possible from other 3rd party dependencies.

All of these expectations and investigations, had led me to the fact, that my future skeleton had to be written in pure CSS, without any JavaScript, and third party dependencies. This makes it possible to be lightweight, and dependencies free. The main idea is that it inherits layouts of components it applied to, and customize them with their own styles.
Perhaps in the future, for development purposes, it would make more sense to rewrite all this to the SCSS syntax, since this will make the code shorter and more reusable, and the final assembly will still compile to pure CSS.

Base Card.

As a basis example and for demo purposes, I'll use React.js and take some base card markup, to show how it works. But I remind you, that it's not tied to any of the frameworks, and at the end of the article there are will be links for source code of the library and demo.

Here is a card markup example, that has it its own styles and doesn't know about the existence of skeleton yet.

<div className='card'>
    <div className='card__img-wrapper'>
        <img className='card__img' src={require(`../../images/cards/${imgUrl}`)}/>
    </div>
    <div className='card__body'>
        <div className='card__details'>
            <p className='card__title'>{title}</p>
            <p className='card__subtitle'>{subtitle}</p>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

In order for the skeleton to become active, it is only necessary to apply the parent class sm-loading to the card itself and the child classes sm-item-primary or sm-item-secondary to those elements on which we want to see the skeleton. So the updated result will look like this:

<div className={`card ${dataState.dataStatus === 'loading' ? "sm-loading" : ""}`}>
    <div className='card__img-wrapper sm-item-primary'>
        <img className='card__img' src={require(`../../images/cards/${imgUrl}`)}/>
    </div>
        <div className='card__body'>
            <div className='card__details'>
                <p className='card__title sm-item-secondary'>{title}</p>
                <p className='card__subtitle sm-item-secondary'>{subtitle}</p>
            </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Let me break it down and explain a few moments:
In the following line of code

<div className={`card ${dataState.dataStatus === 'loading' ? "sm-loading" : ""}`}>
Enter fullscreen mode Exit fullscreen mode

I apply the sm-loading class depending on the condition. If the status of the dataState.dataStatus is loading then class will be applied, otherwise - no. The sm-loading class should only be set/present while your data is loading. It's kind of a switcher. Only when it is present, child elements with the presence of appropriate classes sm-item-primary or sm-item-secondary will display the skeleton. So, only 3 classes that will make this skeleton work.

Base skeleton styles.

Root variables.

In order to have a neat and reusable code, as well as the possibility of further configuration (overriding), I've created root variables with basic styles.

/* Root variables.
--------------------------------------------------------------------------------*/
:root {
    /* Light theme colors. */
    --sm-color-light-primary: 204, 204, 204, 1;
    --sm-color-light-secondary: 227, 227, 227, 1;
    --sm-color-light-animation-primary: color-mix(
            in srgb,
            #fff 15%,
            rgba(var(--sm-color-light-primary))
    );
    --sm-color-light-animation-secondary: color-mix(
            in srgb,
            #fff 15%,
            rgba(var(--sm-color-light-secondary))
    );

    /* Dark theme colors. */
    --sm-color-dark-primary: 37, 37, 37, 1;
    --sm-color-dark-secondary: 41, 41, 41, 1;
    --sm-color-dark-animation-primary: color-mix(
            in srgb,
            #fff 2%,
            rgba(var(--sm-color-dark-primary))
    );
    --sm-color-dark-animation-secondary: color-mix(
            in srgb,
            #fff 2%,
            rgba(var(--sm-color-dark-secondary))
    );

    /* Animations. */
    --sm-animation-duration: 1.5s;
    --sm-animation-timing-function: linear;
    --sm-animation-iteration-count: infinite;
}
Enter fullscreen mode Exit fullscreen mode

Here the color values for the static (with no animation) and animated skeleton are set, as well as the animation settings.

Base styles.

The next section of the file is dedicated to the base styles, that doesn't relate to any color scheme or configuration.

/* Base styles.
Applied by default and not related to any of the color scheme.
--------------------------------------------------------------------------------*/
.sm-loading .sm-item-primary,
.sm-loading .sm-item-secondary {
    border-color: transparent !important;
    color: transparent !important;
    cursor: wait;
    outline: none;
    position: relative;
    user-select: none;
}

.sm-loading .sm-item-primary:before,
.sm-loading .sm-item-secondary:before {
    clip: rect(1px, 1px, 1px, 1px);
    content: "Loading, please wait.";
    inset: 0;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
}

.sm-loading .sm-item-primary::placeholder,
.sm-loading .sm-item-secondary::placeholder {
    color: transparent !important;
}

.sm-loading .sm-item-primary *,
.sm-loading .sm-item-secondary * {
    visibility: hidden;
}

.sm-loading .sm-item-primary :empty:after,
.sm-loading .sm-item-primary:empty:after,
.sm-loading .sm-item-secondary :empty:after,
.sm-loading .sm-item-secondary:empty:after {
    content: "\00a0";
}

/* Animations related styles. */
@keyframes --sm--animation-wave {
    to {
        background-position-x: -200%;
    }
}

@keyframes --sm--animation-wave-reverse {
    to {
        background-position-x: 200%;
    }
}

@keyframes --sm--animation-pulse {
    0% {
        opacity: 1;
    }
    50% {
        opacity: 0.6;
    }
    100% {
        opacity: 1;
    }
}

.sm-loading .sm-item-primary,
.sm-loading .sm-item-secondary {
    animation: var(--sm-animation-duration) --sm--animation-wave
    var(--sm-animation-timing-function) var(--sm-animation-iteration-count);
}
Enter fullscreen mode Exit fullscreen mode

As stated earlier, parent class sm-loading is used to activate the skeleton loader style. The sm-item-primary and sm-item-secondary classes overrides element’s styles and displays a skeleton.

Skeleton Mammoth items structure

In this way, the layout styles and dimensions of elements (card component in our case) are persisted and inherited by the skeleton loader. Additionally, I would like to say that with this approach, we guarantee that all child elements of sm-item-primary or sm-item-secondary classes are hidden and at least have a Non-breaking space character. If an element contains no content at all, this symbol ensures that the element is displayed and rendered. There is also a part that is responsible for users of screen readers, and lets them know that the content is in the process of loading.

Further, there is a division into thematic sections, such as color scheme, animations, accessibility. Let's look at the color styles for the light theme.

/* Light theme.
The library's default color scheme.
Styles applied to the light color scheme.
--------------------------------------------------------------------------------*/
.sm-loading .sm-item-primary {
    background: rgba(var(--sm-color-light-primary));
}

.sm-loading .sm-item-secondary {
    background: rgba(var(--sm-color-light-secondary));
}

/* Animations related styles. */
.sm-loading .sm-item-primary {
    background: linear-gradient(
            90deg,
            transparent 40%,
            var(--sm-color-light-animation-primary) 50%,
            transparent 60%
    )
    rgba(var(--sm-color-light-primary));
    background-size: 200% 100%;
}

.sm-loading .sm-item-secondary {
    background: linear-gradient(
            90deg,
            transparent 40%,
            var(--sm-color-light-animation-secondary) 50%,
            transparent 60%
    )
    rgba(var(--sm-color-light-secondary));
    background-size: 200% 100%;
}
Enter fullscreen mode Exit fullscreen mode

Color scheme.

With a CSS media feature prefers-color-scheme I've implemented automatic support of the light and dark themes. Depending on users' settings, it will be applied automatically. Of course, it's possible to set it manually, I'll talk about it later in the article.

/* Dark theme.
Styles to apply if a user's device settings are set to use dark color scheme.
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
--------------------------------------------------------------------------------*/
@media (prefers-color-scheme: dark) {
    /*Omitted pieces of code.*/
}
Enter fullscreen mode Exit fullscreen mode

Accessibility.

Animations.

By default, in the skeleton, I decided to make animation enabled, but there are cases when developers or users would prefer not to have it. And if for the former this may be dictated by design and requirements, then for the latter it may be due to vestibular motion disorders.
For this, the CSS media feature prefers-reduced-motion comes to the rescue.

/* Accessibility.
Disable animations if a user's device settings are set to reduced motion.
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
--------------------------------------------------------------------------------*/
@media (prefers-reduced-motion) {
    /*Omitted pieces of code.*/

    .sm-loading .sm-item-primary,
    .sm-loading .sm-item-secondary {
        animation: none;
    }

    /*Omitted pieces of code.*/
}
Enter fullscreen mode Exit fullscreen mode

Configuration.

At this stage, the main styles are over, and the skeleton can be considered ready. But, I was haunted by the thought that I should be able to configure all of the above. What if I want to turn off the animation if I want to always have a dark theme?
Since it is not possible for CSS to receive any values as arguments, like JavaScript functions do, the addition of JavaScript was excluded (at least at this stage). Because it would completely break the main concept of being as simple and lightweight as possible.
But still, we can implement something similar to arguments if we know their values in advance. And here data-* attributes come to our aid. With their help, we can check for the presence of the value we need in the attribute, and apply the desired styles.
I will show you how I've implemented it on a small piece of code, and you can find the full implementation in the source code at the link at the end of the article.

For example, if you want to explicitly use a dark theme, you need to make a JSON object:

const config = JSON.stringify({
  theme: "dark",
})
Enter fullscreen mode Exit fullscreen mode

Note:
data-* attributes can only work with strings, so it's significant to apply JSON.stringify() method to the configuration object.

Next, pass this object to the custom attribute data-sm-config:

<div class="card sm-loading" data-sm-config={config}>
    <!-- Omitted pieces of code. -->
</div>
Enter fullscreen mode Exit fullscreen mode

This is how it looks in the CSS file. If there is a value "theme":"dark" in the data-sm-config, apply desired styles.

.sm-loading[data-sm-config*='"theme":"dark"'] .sm-item-primary,
.sm-loading[data-sm-config*='"theme":"dark"'] .sm-item-secondary {
    /* Omitted pieces of code. */
}
Enter fullscreen mode Exit fullscreen mode

Advanced usage.

Overriding styles with global variables.

Each project and case are unique, and it is impossible to predict and make everything versatile. Especially when it comes to colors. That is why, as was said at the beginning of the article, most of the values are placed in variables. If you want to adjust the default styles, just override appropriate variables in your own *.css file inside the :root CSS pseudo-class.
So, for example, if you want to change the color of the primary item (with a class sm-item-primary), you only need to overwrite the corresponding variable:

/* Your own custom.css file: */
:root {
  --sm-color-light-primary: 255, 0, 0, 0.5;
}
Enter fullscreen mode Exit fullscreen mode

Live demo.

Skeleton Mammoth live demo animation

You can try out the finished result in action at the following link: Live demo.

Let's wrap it up.

After I had studied the topic of skeleton loaders for a long time, their varieties, usage, approaches to develop, I managed to collect the essence of useful information and turn it into a final product. Having collected best practices, improved them and combined into a single entity, I've created the library called Skeleton Mammoth. I believe that I managed to achieve my goals and create a pretty good library with all the advantages described in this article. I hope that this library is able to benefit people when using it, or provide new knowledge and experience to create something of their own.

Show your support.

If you find my library useful and would like to show your support, there are simple ways to do so:
Star the GitHub Repository: This helps to increase its visibility and lets others know that the library has a strong user base.

Spread the Word: You can introduce new users to the library by sharing information about it on any platform. Such as writing about it on a blog post, mentioning it on social media, or discussing it in relevant developer communities, it would be immensely helpful.

Below, I will post a list of useful links, including a link to the library.

Useful links.

Top comments (0)