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.
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:
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.
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.
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.
Cons.
Spinners are an integral part of interfaces, but they're not exactly suitable for replacing skeletons.
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.
Cons.
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.
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.
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.
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).
Since every project and every case can be very different, my skeleton needed to be able to be configurable.
In addition to the standard set of features, I wanted to fill it with support for additional useful and necessary features.
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.
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.
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);
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.
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
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);
}
}
With a CSS media feature
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.*/
}
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
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);
}
}
}
}
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,
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 applyJSON.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. */
}
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;
}
You can try out the finished result in action at the following link:
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.
If you find my library useful and would like to show your support, there are simple ways to do so:
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.
Also published here.