Skeleton Loader using Vue & Tailwind

kouts

Giannis Koutsaftakis

Posted on January 6, 2023

Skeleton Loader using Vue & Tailwind

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!

Image description

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>
Enter fullscreen mode Exit fullscreen mode

Customization with props and slot

Props

  • type sets the type of the skeleton loader to rectangle (default) or circle.

  • bgClass used to explicitly set the class for the background color of the skeleton loader. Usually that would be a bg- 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

Image description

<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" />
Enter fullscreen mode Exit fullscreen mode

Notice how we're using Tailwind classes to set the width and height.

Card

Image description

<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>
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
kouts
Giannis Koutsaftakis

Posted on January 6, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related