Ever seen those loading screens where you see gray bars instead of the actual content while the page is loading? These are called skeleton loading screens and used by companies like Facebook, Google, Slack, YouTube, Dev.to and others.
In this article 80 individuals were asked to rate their perceived loading time while seeing a blank page, a sekeleton screen and a spinner screen. They perceived the skeleton screen as quickest, then the spinner and as last the blank screen came. Let's create a skeleton screen together! 😀
What we'll be building (put to 0.5 view):
CSS
You could use an image to display the skeleton but this will create for additional data overhead. Plus it's not responsive. A better option is to build the screen purely with HTML andCSS.
In this example we will build a skeleton screen for a webshop. The skeleton will consists of:
- navbar with 5 items
- header with a circle and 2 squares
- 3 products
If you're wondering what that weird CSS naming conventions is I'm using, I try to use the BEM naming convention for my CSS!
Let's start with setting up our HTML, this section should be placed at the first item in our body:
<section class="skeleton">
<div class="skeleton__navbar">
<div class="skeleton__nav-item"></div>
<div class="skeleton__nav-box">
<div class="skeleton__nav-text">Item 1</div>
<div class="skeleton__nav-text">Item 2</div>
<div class="skeleton__nav-text">Item 3</div>
</div>
<div class="skeleton__nav-item"></div>
</div>
<div class="skeleton__header">
<div class="skeleton__circle"></div>
<div class="skeleton__img"></div>
<div class="skeleton__info"></div>
</div>
<div class="skeleton__products">
<div class="skeleton__product"></div>
<div class="skeleton__product"></div>
<div class="skeleton__product"></div>
</div>
</section>
Then we create a separate CSS file which we place first thing in the head section of the document, so the skeleton div and css are loaded before the rest of the page.
Starting styles for our skeleton:
.skeleton {
z-index: 100;
position: fixed;
left: 50%;
transform: translate(-50%, 0);
width: 100%;
height: 100%;
background-color: white;
}
@media (min-width: 1200px) {
.skeleton {
max-width: 1200px;
}
}
Let's add some raw CSS variables and the navbar:
:root {
--grey: #eee;
--text: #ccc;
}
.skeleton {
z-index: 100;
position: fixed;
left: 50%;
transform: translate(-50%, 0);
width: 100%;
height: 100%;
background-color: white;
}
@media (min-width: 1200px) {
.skeleton {
max-width: 1200px;
}
}
.skeleton__navbar {
width: 100%;
height: 100px;
background: white;
display: flex;
align-items: flex-end;
justify-content: space-between;
}
.skeleton__nav-item {
width: 100px;
height: 50px;
background-color: var(--grey);
}
.skeleton__nav-box {
height: 50px;
display: flex;
flex-grow: 1;
justify-content: space-evenly;
align-items: center;
}
.skeleton__nav-text {
color: var(--text);
}
Then we will replace the variables for a gradient and add the other elements.
:root {
--gradient: linear-gradient(90deg, #ddd 0px, #e8e8e8 40px, #ddd 80px);
--grey: #eee;
--text: #ccc;
}
.skeleton {
z-index: 100;
position: fixed;
left: 50%;
transform: translate(-50%, 0);
width: 100%;
height: 100%;
background-color: white;
}
@media (min-width: 1200px) {
.skeleton {
max-width: 1200px;
}
}
.skeleton__navbar {
width: 100%;
height: 100px;
background: white;
display: flex;
align-items: flex-end;
justify-content: space-between;
}
.skeleton__nav-item {
width: 100px;
height: 50px;
background-color: var(--grey);
}
.skeleton__nav-box {
height: 50px;
display: flex;
flex-grow: 1;
justify-content: space-evenly;
align-items: center;
}
.skeleton__nav-text {
color: var(--text);
}
.skeleton__header {
margin-top: 2rem;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 2rem;
}
.skeleton__circle {
width: 275px;
height: 275px;
background-color: var(--grey);
border-radius: 50%;
}
.skeleton__img {
width: 325px;
height: 250px;
background-color: var(--grey);
}
.skeleton__info {
width: 200px;
height: 250px;
background-color: var(--grey);
}
.skeleton__products {
margin-top: 2rem;
display: flex;
justify-content: space-evenly;
}
.skeleton__product {
width: 200px;
height: 200px;
background-color: var(--grey);
}
Yay, we have a basic skeleton! Let's add some animation
Add animation
For the animation we need to add a gradient which changes position over time.
:root {
--gradient: linear-gradient(90deg, #ddd 0px, #e8e8e8 40px, #ddd 80px);
--animation: shine 1.6s infinite linear;
}
.skeleton__product {
width: 200px;
height: 200px;
background-image: var(--gradient);
animation: var(--animation);
background-size: 300px;
}
@keyframes shine {
0% {
background-position: -100px;
}
40%,
100% {
background-position: 200px;
}
}
Now attach it to the rest of the elements and we're done!
:root {
--gradient: linear-gradient(90deg, #ddd 0px, #e8e8e8 40px, #ddd 80px);
--grey: #eee;
--text: #ccc;
--animation: shine 1.6s infinite linear;
--animation-header: shine-header 1.6s infinite linear;
}
.skeleton {
z-index: 100;
position: fixed;
left: 50%;
transform: translate(-50%, 0);
width: 100%;
height: 100%;
background-color: white;
}
@media (min-width: 1200px) {
.skeleton {
max-width: 1200px;
}
}
.skeleton__navbar {
width: 100%;
height: 100px;
background: white;
display: flex;
align-items: flex-end;
justify-content: space-between;
}
.skeleton__nav-item {
width: 100px;
height: 50px;
background-image: var(--gradient);
animation: var(--animation);
background-size: 275px;
}
.skeleton__nav-box {
height: 50px;
display: flex;
flex-grow: 1;
justify-content: space-evenly;
align-items: center;
}
.skeleton__nav-text {
color: var(--text);
}
.skeleton__header {
margin-top: 2rem;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 2rem;
}
.skeleton__circle {
width: 275px;
height: 275px;
background-image: var(--gradient);
animation: var(--animation-header);
background-size: 300px;
border-radius: 50%;
}
.skeleton__img {
width: 325px;
height: 250px;
background-image: var(--gradient);
animation: var(--animation-header);
background-size: 300px;
}
.skeleton__info {
width: 200px;
height: 250px;
background-image: var(--gradient);
animation: var(--animation-header);
background-size: 300px;
}
.skeleton__products {
margin-top: 2rem;
display: flex;
justify-content: space-evenly;
}
.skeleton__product {
width: 200px;
height: 200px;
background-image: var(--gradient);
animation: var(--animation);
background-size: 300px;
}
@media (max-width: 1200px) {
.skeleton__navbar,
.skeleton__header,
.skeleton__products {
flex-direction: column;
}
.skeleton__navbar {
align-items: flex-start;
}
.skeleton__nav-box,
.skeleton__nav-text,
.skeleton__img,
.skeleton__info,
.skeleton__products {
display: none;
}
.skeleton__nav-item {
width: 100%;
}
@keyframes shine {
0% {
background-position: -100px;
}
40%,
100% {
background-position: 200px;
}
}
@keyframes shine-header {
0% {
background-position: -100px;
}
40%,
100% {
background-position: 270px;
}
}
Disappear on page load
Next up we have to show the skeleton on page load and remove it when the page is ready.
First set the body to have hidden overflow with inline styles, so this gets loaded before all other stylesheets:
<body style="overflow: hidden;">
Then in your main javascript file, add an EventListener to the window that listens for the page to load. When loaded, remove the skeleton and give the body her overflow back! 😄
window.addEventListener("load", () => {
document.body.style.overflow = "visible";
elements.skeleton.style.display = "none";
});
That's all! Have fun building those skeletons. ✌️
Make sure to follow me for more tricks. 🧠
Top comments (13)
At the risk of sounding poignant... in my opinion, skeleton loaders are either:
For example, Netflix often takes way too long to load, and I find it obnoxious - having to watch a screen full of shimmering boxes feels like a cheap trick to keep me transfixed during the wait.
It's also a lot of complexity (lots of extra CSS to maintain) to communicate something as trivial as "please wait", which can be just as (or more) clearly communicated with a single, conventional, spinning element in the center of the screen.
Sorry, but that's just my honest point of view... If you want your app to feel fast, invest your time in optimizations and make it fast - instead of investing deeper into your performance problem itself, just to cover it up.
I know that these skeleton loaders are trendy, but I still feel like they're symptomatic - and, on top of that, I don't feel like they honestly address the problem.
Just my two cents. 🙂
This is exactly what I was looking for! Finally, someone explained it!
Good to hear! It's actually pretty simple right? I might made it too complex by adding so much div's.
I wish there was a website that takes your fully loaded webpage and generates a skeleton screen that you can add to your code.
This wouldn't work with dynamic content- and that's actually fine.
The skeleton is not intended to exactly match the content of the loaded site - But it is supposed to hint what's coming.
Also, don't forget it's normally only the "Above the fold" content we care about.
My favourite way of applying a skeleton is to keep the structure of the html same as the loaded content but have the ".skeleton" class added to each element.
The class makes it look like a skeleton (i.e. apply the grey background, loading animation etc) and when the content has finished loading for the specific div - the class gets removed (by javascript).
Awesome idea, I'm gonna try to build this!
Could you please remind me the purpose of "overflow: hidden" in the body? Thanks
Sure, this is a bit unclear indeed. If you have a long page with scrolling, you only want to show the scroll bar to the user when the page has finished loading. That's why I've hidden the overflow directly inline in the body in my example (which actually does not have a scrollbar when finished, but the project I was working on had it).
Very well explained...
When we added skeletons to our site the management complained that it's broken, makes everything slow, and they want white screen instead... :broken-heart:
Ha! Did they want the page load 'naturally' or just a blank screen? A loading spinner would be another option.
Another smart trick is to only load the 'above the fold' part directly and show the rest when it's finished. Checkout how these guys do it, the top part almost loads instantly and the rest comes later: coolblue.nl/
Just blank, they hate all sorts of loaders, spinners, etc.
how to do it dynamically?