Skeleton Loader using Vue & Tailwind
Giannis Koutsaftakis
Posted on January 6, 2023
A skeleton loader (otherwise known as skeleton screen, content loader, ghost element, and content placeholder) is a great way to improve our app's user experience. Using it instead of a loading spinner, for showing that content is loading, makes the user feel like the application is more responsive and faster.
The issue with skeleton loaders though is that they need to have a different design depending on the place that they are used, thus making them more difficult to implement.
Tailwind to the rescue
Let's see how we can create a skeleton loader component that includes an animated "shimmer" effect, using Tailwind and Vue.js. It will serve as the building block for creating skeleton content placeholders for all kinds of sections.
So, rather than creating a separate skeleton component for each element or section (e.g texts, headings, images, videos etc), the concept is to utilize Tailwind classes to shape multiple skeleton loader instances exactly how we want them and combine them in order to create the desired result!
Skeleton loader component
Our component will be as simple as possible using one div
for the skeleton and one div
for the shimmer. We'll need some props in order to set the skeleton type (rectangle
or circle
) and skeleton colors as well.
SkeletonLoader.vue
<template>
<div :class="[bgClass, loaderClass, 'relative overflow-hidden']">
<div class="shimmer absolute top-0 right-0 bottom-0 left-0" :style="shimmerStyle"></div>
<slot />
</div>
</template>
<script lang="ts">
const LOADER_TYPES = { rectangle: 'rectangle', circle: 'circle' };
const LOADER_CSS_CLASSES = {
[LOADER_TYPES.rectangle]: 'rounded',
[LOADER_TYPES.circle]: 'rounded-full',
};
type LoaderTypesKeys = keyof typeof LOADER_TYPES;
type LoaderTypesValues = typeof LOADER_TYPES[LoaderTypesKeys];
const SHIMMER_COLOR = '#ffffff';
const isHexColor = (hexColor: string) => {
const hex = hexColor.replace('#', '');
return typeof hexColor === 'string' && hexColor.startsWith('#') && hex.length === 6 && !isNaN(Number('0x' + hex));
};
const hexToRgb = (hex: string) => `${hex.match(/\w\w/g)?.map((x) => +`0x${x}`)}`;
</script>
<script setup lang="ts">
import { computed, toRefs } from 'vue';
const props = defineProps({
type: {
type: String,
default: LOADER_TYPES.rectangle,
validator(value: LoaderTypesValues) {
return Object.values(LOADER_TYPES).includes(value);
},
},
bgClass: {
type: String,
default: 'bg-gray-300',
},
cssClass: {
type: String,
default: '',
},
shimmerColor: {
type: String,
default: SHIMMER_COLOR,
},
});
const { type, bgClass, cssClass, shimmerColor } = toRefs(props);
const shimmerStyle = computed(() => {
const rgb = isHexColor(shimmerColor.value) ? hexToRgb(shimmerColor.value) : SHIMMER_COLOR;
return {
backgroundImage: `linear-gradient(90deg, rgba(${rgb}, 0) 0%, rgba(${rgb}, 0.2) 20%, rgba(${rgb}, 0.5) 60%, rgba(${rgb}, 0))`,
};
});
const loaderClass = computed(() => (cssClass.value ? cssClass.value : LOADER_CSS_CLASSES[type.value]));
</script>
<style lang="css" scoped>
.shimmer {
transform: translateX(-100%);
animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}
</style>
Customization with props
and slot
Props
type
sets the type of the skeleton loader torectangle
(default) orcircle
.bgClass
used to explicitly set the class for the background color of the skeleton loader. Usually that would be abg-
variant Tailwind class.cssClass
used to set custom CSS classes for the skeleton loader. In case this prop is used, the loader won't have any classes set by default (apart from the background class).shimmerColor
used to set the shimmer color, expects a hex color string. If no valid hex string will be provided, the shimmer will fallback to the default (white) color.
I'm sure you noticed that there are no width
and height
props. Although we could easily add those, we will instead utilize Vue
's fallthrough attributes feature to set the desired width/height using either Tailwind classes or simply by using style
.
There are other props
that we could have included in our component as well, like props to set the shimmer animation duration or the shimmer CSS class etc. We'll leave these out in order to keep the code size in this example as compact as possible, feel free to experiment by adding any props
you find useful.
Slot
Our component includes a default slot
that we can use to customize the contents of our skeleton loader div
. Useful in case we want our skeleton not to be just an empty div
but also to contain an image, a text or even another component etc.
How to use
Alright, we have our base skeleton loader component ready, it's time to use it to create some skeleton loader placeholders!
Text of various sizes
<SkeletonLoader class="w-16 h-5" />
<SkeletonLoader class="w-32 h-4 mt-4" />
<SkeletonLoader class="w-72 h-2.5 mt-2" />
<SkeletonLoader class="w-52 h-4 mt-6" />
<SkeletonLoader class="w-72 h-2.5 mt-2" />
<SkeletonLoader class="w-72 h-2 mt-2" />
Notice how we're using Tailwind classes to set the width and height.
Card
<div class="p-4 max-w-sm rounded border border-gray-200 md:p-6">
<SkeletonLoader class="mb-4 h-48 flex justify-center items-center text-gray-400"> Image </SkeletonLoader>
<SkeletonLoader class="h-2.5 w-48 mb-4" />
<SkeletonLoader class="h-2 mb-2.5" />
<SkeletonLoader class="h-2 mb-2.5" />
<SkeletonLoader class="h-2" />
<div class="flex items-center mt-4 space-x-3">
<SkeletonLoader type="circle" class="w-14 h-14 flex justify-center items-center text-gray-400"> Icon </SkeletonLoader>
<div>
<SkeletonLoader class="h-2.5 w-32 mb-2"></SkeletonLoader>
<SkeletonLoader class="w-48 h-2" />
</div>
</div>
</div>
This is a more complete example that utilizes the slot
and also the rectangle
and circle
types of the loader to create a skeleton card.
Demo with examples
Checkout more examples and the full code in the Stackblitz example here: https://stackblitz.com/edit/vue3-tailwind-vbdfja?file=src/App.vue
Thanks for reading!
Posted on January 6, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.