paint-brush
Skeleton Mammoth : A Universal Approach to the Problem of Reusable Skeletons Loadersby@wolfriend
568 reads
568 reads

Skeleton Mammoth : A Universal Approach to the Problem of Reusable Skeletons Loaders

by Oleksandr TkachenkoAugust 7th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this article, I will describe the process of creating the skeleton loader solution and turning it into a library.
featured image - Skeleton Mammoth : A Universal Approach to the Problem of Reusable Skeletons Loaders
Oleksandr Tkachenko HackerNoon profile picture

Introduction.

There are a lot of great articles on the Internet devoted to skeleton loaders that cover 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 Are Skeleton Loaders?

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 do 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 a 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 use screen readers.
  • Versatility and reusability: Most approaches to creating skeletons offer either creating a shallow copy of a component’s 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.
  • Binding to a specific framework or library: Often, ready-made solutions are either only stuck for certain frameworks (such as React.js or Vue.js), or they are part of a larger library. So, for example, if you only want to use skeleton from popular libraries like MUI, you still need to install the core files/components of that library to use only skeleton.

Skeleton Loader 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 requires 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. It 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 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 skeletons are, when to use them, 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 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 customizes them with their own styles.


Over time, for development purposes, I've rewritten CSS syntax to SCSS. This made it possible to decompose the code into separate logical parts, making it more concise and reusable. But the end result is still pure CSS without any dependencies.

Base Card.

As a basic 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 will be links for the source code of the library and demo.


Here is a card markup example that has its own styles and doesn't know about the existence of skeletons 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>

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>


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

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

I apply the sm-loading class depending on the condition. If the status of the dataState.dataStatus is loading then the 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 three classes will make this skeleton work.

Base skeleton styles.

Root variables.

For reusability, as well as advanced usage of the skeleton (which I will discuss later in the article), the main style values are placed in root variables.


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


File: ./src/styles/variables/colors.scss

/* Root variables.
Needed here to be able to override them after compilation:
https://github.com/WOLFRIEND/skeleton-mammoth#overriding-styles-with-global-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)) 85%);
  --sm-color-light-animation-secondary: color-mix(in srgb, #fff 15%, rgba(var(--sm-color-light-secondary)) 85%);

  /* 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)) 98%);
  --sm-color-dark-animation-secondary: color-mix(in srgb, #fff 2%, rgba(var(--sm-color-dark-secondary)) 98%);
}

/* Light theme colors. */
$--sm-color-light-primary: var(--sm-color-light-primary);
$--sm-color-light-secondary: var(--sm-color-light-secondary);
$--sm-color-light-animation-primary: var(--sm-color-light-animation-primary);
$--sm-color-light-animation-secondary: var(--sm-color-light-animation-secondary);

/* Dark theme colors. */
$--sm-color-dark-primary: var(--sm-color-dark-primary);
$--sm-color-dark-secondary: var(--sm-color-dark-secondary);
$--sm-color-dark-animation-primary: var(--sm-color-dark-animation-primary);
$--sm-color-dark-animation-secondary: var(--sm-color-dark-animation-secondary);


Here are variables that set the values for the animation settings of the skeleton.


File: ./src/styles/variables/animations.scss

/* Root variables.
Needed here to be able to override them after compilation:
https://github.com/WOLFRIEND/skeleton-mammoth#overriding-styles-with-global-variables
--------------------------------------------------------------------------------*/
:root {
  /* Animations values. */
  --sm-animation-duration: 1.5s;
  --sm-animation-timing-function: linear;
  --sm-animation-iteration-count: infinite;

  /* Animations. */
  --sm-animation-none: none;
  --sm-animation-wave: --sm--animation-wave var(--sm-animation-duration) var(--sm-animation-timing-function)
    var(--sm-animation-iteration-count);
  --sm-animation-wave-reverse: --sm--animation-wave-reverse var(--sm-animation-duration)
    var(--sm-animation-timing-function) var(--sm-animation-iteration-count);
  --sm-animation-pulse: --sm--animation-pulse var(--sm-animation-duration) var(--sm-animation-timing-function)
    var(--sm-animation-iteration-count);
}

/* Animations values. */
$--sm-animation-duration: var(--sm-animation-duration);
$--sm-animation-timing-function: var(--sm-animation-timing-function);
$--sm-animation-iteration-count: var(--sm-animation-iteration-count);
$--sm-animation-wave-background-position-x: -200%;
$--sm-animation-wave-reverse-background-position-x: 200%;
$--sm-animation-pulse-percentage-0: 1;
$--sm-animation-pulse-percentage-50: 0.6;
$--sm-animation-pulse-percentage-100: 1;

/* Animations. */
$--sm-animation-none: var(--sm-animation-none);
$--sm-animation-wave: var(--sm-animation-wave);
$--sm-animation-wave-reverse: var(--sm-animation-wave-reverse);
$--sm-animation-pulse: var(--sm-animation-pulse);

Base styles.

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


File: src/styles/base-styles.scss

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

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

    &::placeholder {
      color: transparent !important;
    }

    * {
      visibility: hidden;
    }

    :empty:after {
      content: "\00a0";
    }
  }
}


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

Skeleton Mammoth 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 are divisions into thematic sections, such as color scheme, animations, and accessibility. Let's look at the color styles for the light theme.


Here are set the default styles applied to the skeleton. In other words, these are the styles that will be applied by default if no settings are specified. It will be a light theme and a wave animation type.


File: ./src/styles/skeleton-mammoth.scss

/* Light theme.
The library's default color scheme.
Styles applied to the light color scheme by default.
--------------------------------------------------------------------------------*/
.sm-loading {
  .sm-item-primary {
    @include mixin-sm-animate-loading(
      $--sm-animation-wave,
      $--sm-color-light-animation-primary,
      $--sm-color-light-primary
    );
  }

  .sm-item-secondary {
    @include mixin-sm-animate-loading(
      $--sm-animation-wave,
      $--sm-color-light-animation-secondary,
      $--sm-color-light-secondary
    );
  }
}

It uses mixin mixin-sm-animate-loading which applies animation settings depending on the arguments passed.


File: ./src/styles/mixins/animations.scss

@import "../variables/props";
@import "../variables/animations";
@mixin mixin-sm-animate-loading($animation, $animationColor, $backgroundColor) {
  animation: $animation;

  @if $animation == $--sm-animation-pulse or $animation == $--sm-animation-none {
    background: rgba($backgroundColor);
  } @else {
    background: linear-gradient(90deg, transparent 40%, $animationColor 50%, transparent 60%) rgba($backgroundColor);
    background-size: 200% 100%;
  }

  /* 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
  Should be duplicated here due to the CSS rule precedence.
  --------------------------------------------------------------------------------*/
  //@include mixin-apply-accessibility-reduced-motion();
  @media (prefers-reduced-motion) {
    animation: $--sm-animation-none;
    background: rgba($backgroundColor);
  }
}

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.


File: ./src/styles/skeleton-mammoth.scss

/* 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.*/
}

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 featureprefers-reduced-motion comes to the rescue.


File: ./src/styles/skeleton-mammoth.scss

/* 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
--------------------------------------------------------------------------------*/
.sm-loading {
  @include mixin-apply-accessibility-reduced-motion();
}

It uses mixin mixin-apply-accessibility-reduced-motion which applies animation settings for reduced motion.


File: src/styles/mixins/accessibility.scss

@import "../variables/colors";
@import "../variables/animations";

@mixin mixin-apply-accessibility-reduced-motion() {
  @media (prefers-reduced-motion) {
    .sm-item-primary,
    .sm-item-secondary {
      animation: $--sm-animation-none;
    }

    .sm-item-primary {
      background: rgba($--sm-color-light-primary);
    }

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

    /* Dark theme related styles. */
    @media (prefers-color-scheme: dark) {
      .sm-item-primary {
        background: rgba($--sm-color-dark-primary);
      }

      .sm-item-secondary {
        background: rgba($--sm-color-dark-secondary);
      }
    }
  }
}

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",
})


**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 attributedata-sm-config:

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


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

/* { theme: "dark" }. */
.sm-loading[data-sm-config*='"#{$--sm-props-theme-name}":"#{$--sm-props-theme-type-dark}"'] {
  @include mixin-apply-configuration-object-animation-and-theme(null, "#{$--sm-props-theme-type-dark}");
}


At first glance, it doesn’t looks obvious, since here, we used variables as values and mixin mixin-apply-configuration-object-animation-and-theme to apply styles. Here is how it will look in pure CSS:

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

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 classsm-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;
}

Live Demo

Skeleton Mammoth live demo.

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, and approaches to development, I managed to collect the essence of useful information and turn it into a final product. Having collected best practices, improved them, and combined them 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 in a blog post, mentioning it on social media, or discussing it in relevant developer communities, would be immensely helpful.


It's very crucial for me to receive feedback on my library. I warmly welcome new contributors and any suggestions for improvement. Below, I will post a list of useful links, including a link to the library.

Useful links.


Also published here.