Skeleton Loader using Vue & Tailwind
Tags: webdev, vue, ux, javascript
Published: 2023-01-06 15:42:28

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](https://tailwindcss.com/) and [Vue.js](https://vuejs.org/). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kh7po0zc7wrbqmbok1po.gif)

## 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`

```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 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](https://vuejs.org/guide/components/attrs.html#fallthrough-attributes) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/79wgp2hy9l2w1epdmvqo.png)

```vue
<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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xsushgnsyk3coe1wgs0x.png)

```vue
<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!**