I'm using Vue 3 and I try to implement a skeleton loader for a placeholder when the data is loading and I can see it works if I turn on throttling in the Network tab of the browser. However, when the data loads too quick, I can also see how the skeleton loader blinks.
Here I tried using a setTimeout
and inside I put the isLoaded
ref which value is set to true when the image is fully loaded. But what the delay does is it prolongs time, and I need to skeleton loader be not visible when the data loads fast. I want it only be visible, when the data loads slowly.
onMounted(() => {
const img = new Image(getSrc('.jpg'));
img.onload = () => {
setTimeout(() => {
isLoaded.value = true;
}, 300);
};
img.src = getSrc('.jpg');
});
Update:
This is how I use skeleton loader in the <template>
:
<ItemCardSkeleton v-if="pending === false || !isLoaded" />
<template v-else>
<img
class="card__image"
:src="getSrc('.jpg')"
:alt="recipe.alt"
width="15.625rem"
loading="lazy" />
<div class="card__content">
<h2 class="card__title">{{ recipe.title }}</h2>
</div>
</template>
Please, give a solution.
I would leave the skeleton logic alone, just fade it in after some time, avoiding the jump in content if you delay displaying the skeleton (as discussed in @Phil's answer)
The example below, there's no skeleton visible for 1000ms, then the skeleton will fade in for 1000ms - the times are probably longer than you want, I just wrote it this way to see it actually working - so, just adjust the animation-duration
(and animation-timing-function
if you want) as appropriate to your needs
<style scoped>
@keyframes skeletonAnimate {
0% {
opacity:0;
}
50% {
opacity:0;
}
100% {
opacity:1;
}
}
.skeleton {
opacity:0;
animation-name: skeletonAnimate;
animation-duration: 2000ms;
animation-direction: forward;
animation-delay: 0;
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-timing-function: ease-out;
}
</style>
Not sure how your skeleton is done, but here's a non-vue demo in action
@keyframes skeletonAnimate {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.skeleton-content {
opacity: 0;
animation-name: skeletonAnimate;
animation-duration: 2000ms;
animation-direction: forward;
animation-delay: 0;
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-timing-function: ease-out;
}
@keyframes bgAnimate {
0% {
background-position: 50% 0;
}
100% {
background-position: -150% 0;
}
}
.skeleton {
height: 100px;
width: 200px;
background-image: linear-gradient( to right, hsla(210, 2%, 54%, 20%) 0%, hsla(210, 4%, 89%, 20%) 10%, hsla(210, 2%, 54%, 20%) 40%, hsla(210, 2%, 54%, 20%) 100%);
background-repeat: repeat-x;
background-size: 200% 100%;
box-shadow: 0 4px 6px -1px hsla(0, 0%, 0%, 0.1), 0 2px 4px -2px hsla(0, 0%, 0%, 0.1);
animation: bgAnimate 2s linear infinite;
}
<div class="skeleton-content">
<div class="skeleton"></div>
</div>
Other content