Skeleton Component in UX and Performance in Vue
Tags: vue, javascript, webdev, ux
Published: 2021-10-20 04:53:18

A lot of modern websites handle data fetching in the browser instead of the server, this is good because the user does not need to wait too long for the page to load from the server but they then need to wait for any data to be fetched from the browser once they arrived, the data can be a blog post, form data, etc. Usually, when this process happens, the user will be shown with the spinner that indicates the data is fetched in the background. While that is a great solution, some popular websites such as Youtube or Facebook choose to not use that, instead, they use a skeleton loader screen.

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

The skeleton loader screen shows representation an outline of the content while it's being fetched, because of the various shapes of the skeleton they look more fun and interesting compare to a dummy animated spinner like it’s a clock.

You can see the full code here: 

- [Github](https://github.com/akbarnafisa/skeleton-component)

## Skeleton Component and UX

A skeleton UI is a placeholder structured UI that represents the content as it is loading and becoming available once it's loaded. Because the skeleton mimics the page load while it's loading, the users will feel less interrupted on the overall experience. Take a look image bellow.

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

All of the pages above load the content at the same speed, but the empty page seems to perform worse than the other, while the skeleton page seems faster and more engaging compare to the others. The skeleton page gives the user a better experience by reducing frustration feeling while they wait for the content to load because let's be honest, no one like to wait, you can read more about research in skeleton in [this amazing article](https://uxdesign.cc/what-you-should-know-about-skeleton-screens-a820c45a571a).

## Skeleton Component and Performance

A skeleton component can be used when we do a lazy load on our component. The lazy load purpose is to split the code that is usually not in the user's main flow at the current page and to postpone downloading it until the user needs it. Let's take look at the lazy load dialog component in Vue.

```jsx
<template>
  <div class="dialog">
		<dialog-content />
  </div>
</template>

<script>
export default {
	name: 'Dialog',
  components: {
    DialogContent: () => import('./DialogContent.vue')
  }
}
</script>
```

And here is the result

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/87o6xdsysf6fx5douke4.gif)

From the image above we know that when a user requests to download the lazy component there is a slight delay, it will become apparent if the connection of the user is slow and that's where the skeleton loader comes to play. We will use the skeleton loader to indicate that the component is being loaded and we can also combine it with The Vue async component for additional error handling.

## What we’re making

The skeleton loader type that we going to make is a content placeholder, from the technical perspective we will replicate the final UI to the skeleton. From [the research](https://uxdesign.cc/what-you-should-know-about-skeleton-screens-a820c45a571a) that has been done by Bill Chung, the participant perceive a shorter duration of the loader if:

- The skeleton has waving animation instead of static or pulsing animation,
- The animation speed is slow and steady instead of fast
- The wave animation is left to right instead of right to left

The skeleton that we are going to make should have this requirement: 

- Support animation and can be controlled through component props
- Easy to customize and flexible, the shape of the skeleton can be modified through component props

Here’s a quick screenshot of what we’ll be building!
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9qr1m9xi0v0yb8mv000l.gif)

## Setup Project

For this project, we will use [Nuxt](https://nuxtjs.org/) to play around with our code. Open up a terminal in a directory of your choice and create a new project with this command:

```bash
$ yarn create nuxt-app <project-name>
```

You’ll see a follow-up questionnaire like this, you can follow what we did or not, that's up to you but we suggest installing Tailwind CSS for this project, it will become make the project much easier later

```bash
create-nuxt-app v3.6.0
✨  Generating Nuxt.js project in docs
? Project name: my-skeleton
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: Tailwind CSS
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint, Prettier, StyleLint
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git
```

## Create Our Component

First let's create file `index.vue` page in the folder `pages` to setup the main page

```jsx
<template>
  <div class="flex flex-wrap justify-around p-4 lg:p-16">
    <Card
      v-for="(item, i) in items"
      :key="i"
      :item="item"
      :is-loaded="isLoaded"
    />
  </div>
</template>

<script>
import Card from '../components/Card.vue'
export default {
	name: 'Home',
  components: {
    Card,
  },
  data() {
    return {
      isLoaded: false,
      items: [
        {
          thumbnail: 'laptop.svg',
          avatar: 'avatar_1.jpeg',
          bgColor: '#BCD1FF',
          tag: 'PRODUCTIVITY',
          date: '3 days ago',
          title: '7 Skills of Highly Effective Programmers',
          desc: 'Our team was inspired by the seven skills of highly effective programmers created by the TechLead. We wanted to provide our own take on the topic. Here are our seven...',
          author: 'Glen Williams',
        },
      ],
    }
  },
  mounted() {
    this.onLoad()
  },
  methods: {
    onLoad() {
      this.isLoaded = false
      setTimeout(() => {
        this.isLoaded = true
      }, 3000)
    },
  },
}
</script>
```

then let's create `Card.vue` file in `components` folder to render each data

```jsx
<template>
  <div
    class="flex flex-col mb-6 w-full max-w-sm bg-white rounded-2xl overflow-hidden lg:flex-row lg:mb-16 lg:mx-auto lg:max-w-screen-lg lg:h-96"
  >
    <div
      class="flex items-center justify-center w-full h-56 lg:max-w-sm lg:h-96"
      :style="{
        background: item.bgColor,
      }"
    >
      <img class="w-36 lg:w-60" :src="require(`~/assets/${item.thumbnail}`)" />
    </div>

    <div class="relative flex-1 p-6 pb-12 lg:p-8">
      <div class="flex justify-between mb-3 lg:mb-6">
        <div
          class="text-gray-500 font-body text-xs font-semibold uppercase lg:text-xl"
        >
          {{ item.tag }}
        </div>

        <div class="text-gray-500 font-body text-xs lg:text-xl">
          {{ item.date }}
        </div>
      </div>
      <div class="flex flex-col">
        <div class="h mb-1 font-title text-xl lg:mb-4 lg:text-4xl">
          {{ item.title }}
        </div>

        <div class="mb-6 text-gray-900 font-body text-sm lg:text-lg">
          {{ item.desc }}
        </div>
      </div>
      <div
        class=" absolute bottom-0 left-0 flex items-center justify-between pb-6 px-6 w-full lg:px-8"
      >
        <div class="flex items-center text-center">
          <div
            :style="{
              backgroundImage: `url(${require(`~/assets/${item.avatar}`)})`,
            }"
            class="mr-3 w-8 h-8 bg-cover bg-center rounded-full lg:w-11 lg:h-11"
          ></div>

          <div class="text-blue-500 text-xs font-semibold lg:text-xl">
            {{ item.author }}
          </div>
        </div>

        <div class="flex items-center">
          <div class="mr-1 text-blue-500 text-xs font-semibold lg:text-xl">
            Read More
          </div>
          <svg
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="#3b82f6"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              d="M16.17 13L12.59 16.59L14 18L20 12L14 6L12.59 7.41L16.17 11H4V13H16.17Z"
              fill="#3b82f6"
            />
          </svg>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Card',
  props: {
    item: {
      type: Object,
      default: () => ({}),
    },
    isLoaded: {
      type: Boolean,
      default: true,
    },
  },
}
</script>
```

Now, our Card component is complete and it should look like this

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

The design is coming from the Card Templates by Figma Design Team, you can check the full design [here](https://www.figma.com/community/file/777205710295345433)

## The Skeleton Component

Let's create new file namely `Skeleton.vue` inside `components` folder

```jsx
<template>
  <transition
    name="skeleton"
    mode="out-in"
    :css="transition && hasChild ? true : false"
  >
    <slot v-if="isLoaded" />
    <span v-else>
      <span
        v-for="index in rep"
        :key="index"
        :class="componentClass"
        :style="componentStyle"
      />
    </span>
  </transition>
</template>

<script>
export default {
	name: 'Skeleton',
  props: {
    animation: {
      type: [String, Boolean],
      default: 'wave',
      validator: (val) => ['wave', false].includes(val),
    },
    h: {
      type: String,
      default: '20px',
    },
    isLoaded: {
      type: Boolean,
      default: false,
    },
    m: {
      type: String,
      default: '0px',
    },
    rep: {
      type: Number,
      default: 1,
    },
    radius: {
      type: String,
      default: '4px',
    },
    skeletonClass: {
      type: String,
      default: '',
    },
    transition: {
      type: Boolean,
      default: true,
    },
    w: {
      type: String,
      default: '100%',
    },
  },
  computed: {
    componentClass() {
      return [
        this.skeletonClass,
        'skeleton',
        this.animation ? `skeleton--${this.animation}` : null,
      ]
    },
    componentStyle() {
      return {
        width: this.w,
        height: this.h,
        borderRadius: this.radius,
        margin: this.m,
      }
    },
    hasChild() {
      return this.$slots && this.$slots.default
    },
  },
}
</script>
```

The idea for the skeleton component is quite simple, we only make `span` element as a skeleton to replace the main content during the load time but to make the component more reusable and functional we add a bunch of other props, let's take a close look at each of them

- `animation` - set the type of the animation of the skeleton, you can set it to `wave` or `false` to disable the animation
    
   ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/inli0af88rpceew2y8aj.gif)
    
- `h` - set the height of the skeleton, it's in string format, so you can set the value to be `px`, `percentage`, `vh`, or `rem`
- `isLoaded` - set the state for the component to show skeleton or content
- `m` - set the margin of the skeleton, same as the `h` props, you can set the value to various format
- `rep` -  repeat the skeleton component as much as the value, this will become useful if we want to create a paragraph-like skeleton
- `radius` - set the border radius of the skeleton, same as the `h` props, you can set the value to various format
- `skeletonClass` -  set class for skeleton component, use these props to add more flexibility to your component, especially when you dealing with responsive design
- `transition` - set the animation during the transition of the `isLoaded` component, we use Vue's `transition` component
    
    ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bks700qxjp79rswqc8c8.gif)
    
- `w` - set the width of the skeleton, same as the `h` props, you can set the value to various format

## The Styling and Animation

The next step is to add some scoped styles in the `Skeleton.vue` file

```css

.skeleton {
  color: transparent;
  display: block;
  user-select: none;
  background: #d1d5db;

  * {
    visibility: hidden;
  }

  &--wave {
    position: relative;
    overflow: hidden;
    -webkit-mask-image: -webkit-radial-gradient(white, black);
    &::after {
      animation: wave 1.5s linear 0s infinite;
      background: linear-gradient(
        90deg,
        transparent,
        rgba(255, 255, 255, 0.5),
        transparent
      );
      content: '';
      position: absolute;
      transform: translate3d(-100%, 0, 0);
      will-change: transform;
      bottom: 0;
      left: 0;
      right: 0;
      top: 0;
    }
  }
}

@keyframes wave {
  0% {
    transform: translate3d(-100%, 0, 0);
  }
  60% {
    transform: translate3d(100%, 0, 0);
  }
  100% {
    transform: translate3d(100%, 0, 0);
  }
}

.skeleton-enter-active,
.skeleton-leave-active-active {
  transition: opacity 0.1s ease-in-out;
}

.skeleton-enter,
.skeleton-leave-active {
  opacity: 0;
  transition: opacity 0.1s ease-in-out;
}
```

The skeleton component styling is quite simple, we only need to add background color to the component, and the width and height are passed through the props. The waving animation is implemented by using CSS animation, the duration that we set is 1500ms and it makes the animation is slow and steady for the user. We also animate the wave animation using `translate3d` and `will-change` properties to achieve that 60 fps performance. Finally, let's add a simple animation effect for the `transition` component, for this animation we only use the fade transition to make it simple and smooth for the user.

## Implement Skeleton to Card Component

Now, let's implement the skeleton component inside our card component, the implementation of the skeleton can be in various forms, here is some of it and our thoughts about it

### If Operator

The Vue's conditional rendering might be the common practice to render which component that we want to show, this method makes the code clearer and easier to maintain because the separation of the component is obvious but the downside is you need to maintain styling on the skeleton and the main component especially on flex-box and also the `transition` props animation won't work in this method.

```jsx
<div v-if="isLoaded">
    My Awesome Content
</div>
<skeleton v-else :is-loaded="isLoaded"/>

// or

<template v-if="isLoaded">
  <Card
    v-for="(item, i) in items"
    :key="i"
    :item="item"
  />
</template>
<template v-else>
  <MyCardSkeleton
    v-for="(item, i) in dummyItems"
    :key="i"
    :item="item"
    :is-loaded="isLoaded"
  />
</template>
```

### Component Wrapper

This method is the opposite of the previous method, with this method the styling of the component is maintained and `transition` props animation is working, the downside is the code might be messier because you wrap the skeleton component instead of putting it side by side to the main component.

```jsx
<skeleton :is-loaded="isLoaded">
  <div>
    My Awesome Content
  </div>
</skeleton>
```

For our implementation, we choose to use component wrapper method, and here is the code:

```jsx
<template>
  <div
    class="flex flex-col mb-6 w-full max-w-sm bg-white rounded-2xl overflow-hidden lg:flex-row lg:mb-16 lg:mx-auto lg:max-w-screen-lg lg:h-96"
  >
    <skeleton
      :animation="false"
      :is-loaded="isLoaded"
      skeleton-class="w-full h-56 w-36 lg:w-96 lg:h-96"
      :w="null"
      :h="null"
      radius="0px"
    >
      <div
        class="flex items-center justify-center w-full h-56 lg:max-w-sm lg:h-96"
        :style="{
          background: item.bgColor,
        }"
      >
        <img
          class="w-36 lg:w-60"
          :src="require(`~/assets/${item.thumbnail}`)"
        />
      </div>
    </skeleton>

    <div class="relative flex-1 p-6 pb-12 lg:p-8">
      <div class="flex justify-between mb-3 lg:mb-6">
        <skeleton
          skeleton-class="w-28 h-4 lg:h-7"
          :w="null"
          :h="null"
          :is-loaded="isLoaded"
        >
          <div
            class="text-gray-500 font-body text-xs font-semibold uppercase lg:text-xl"
          >
            {{ item.tag }}
          </div>
        </skeleton>
        <skeleton
          skeleton-class="w-24 h-4 lg:h-7"
          :w="null"
          :h="null"
          :is-loaded="isLoaded"
        >
          <div class="text-gray-500 font-body text-xs lg:text-xl">
            {{ item.date }}
          </div>
        </skeleton>
      </div>
      <div class="flex flex-col">
        <skeleton
          :is-loaded="isLoaded"
          skeleton-class="w-full h-7 lg:h-9"
          class="mb-3"
          :w="null"
          :h="null"
        >
          <div class="h mb-1 font-title text-xl lg:mb-4 lg:text-4xl">
            {{ item.title }}
          </div>
        </skeleton>
        <skeleton
          class="mb-6"
          :is-loaded="isLoaded"
          skeleton-class="w-full h-3 lg:h-5"
          :w="null"
          :h="null"
          m="0 0 8px 0"
          :rep="4"
        >
          <div class="mb-6 text-gray-900 font-body text-sm lg:text-lg">
            {{ item.desc }}
          </div>
        </skeleton>
      </div>
      <div
        class="absolute bottom-0 left-0 flex items-center justify-between pb-6 px-6 w-full lg:px-8"
      >
        <div class="flex items-center text-center">
          <skeleton
            :is-loaded="isLoaded"
            skeleton-class="w-8 h-8 lg:w-11 lg:h-11"
            :w="null"
            :h="null"
            radius="100%"
            class="mr-3"
          >
            <div
              :style="{
                backgroundImage: `url(${require(`~/assets/${item.avatar}`)})`,
              }"
              class="mr-3 w-8 h-8 bg-cover bg-center rounded-full lg:w-11 lg:h-11"
            ></div>
          </skeleton>

          <skeleton
            :is-loaded="isLoaded"
            skeleton-class="w-16 h-4 lg:h-7 lg:w-28"
            :w="null"
            :h="null"
          >
            <div class="text-blue-500 text-xs font-semibold lg:text-xl">
              {{ item.author }}
            </div>
          </skeleton>
        </div>
        <skeleton
          :is-loaded="isLoaded"
          skeleton-class="w-16 h-4 lg:h-7 lg:w-28"
          :w="null"
          :h="null"
        >
          <div class="flex items-center">
            <div class="mr-1 text-blue-500 text-xs font-semibold lg:text-xl">
              Read More
            </div>
            <svg
              width="24"
              height="24"
              viewBox="0 0 24 24"
              fill="#3b82f6"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M16.17 13L12.59 16.59L14 18L20 12L14 6L12.59 7.41L16.17 11H4V13H16.17Z"
                fill="#3b82f6"
              />
            </svg>
          </div>
        </skeleton>
      </div>
    </div>
  </div>
</template>

<script>
import Skeleton from './Skeleton.vue'

export default {
  name: 'Card',
  components: {
    Skeleton,
  },
  props: {
    item: {
      type: Object,
      default: () => ({}),
    },
    isLoaded: {
      type: Boolean,
      default: true,
    },
  },
}
</script>
```

In our implementation, we mainly set `skeleton-class` props to set the height and weight of the skeleton to use the utility class in tailwind CSS, this utility class is become handy when dealing with responsive design.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eay6o38gra7jzsqiut0i.gif)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cmmm9jxk0naph2t16bbv.gif)

## Skeleton on Lazy Load Component

Lazy load component usually can be done by using `import()` function, but because it's asynchronous, we don't know when the component is finished being fetched.

```jsx
export default {
	components: {
		DialogContent: () => import('./DialogContent.vue')
	}
}
```

Luckily, Vue has a feature for this problem, we can loading components as the component is being fetched and error component if the main component is failed, you can read more [here](https://vuejs.org/v2/guide/components-dynamic-async.html#Handling-Loading-State).

```jsx
const DialogContent = () => ({
	// The component to load (should be a Promise)
  component: import('./DialogContent.vue'),
	// A component to use while the async component is loading
  loading: SkeletonDialogContent,
	// A component to use if the load fails
  error: DialogFailed,
	// The error component will be displayed if a timeout is
  // provided and exceeded. Default: Infinity.
  timeout: 3000,
})
```

Here is the end result, you can read the code in the [GitHub repo](https://github.com/akbarnafisa/skeleton-component/blob/master/components/Dialog.vue)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cyicku4kopoighl949w8.gif)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dgisgkd9if09n5ytqz7y.gif)


## Wrapping it up

We already learn how to create a skeleton component and how to implement it in Vue. Skeleton can improve UX in your site if it's implemented in the right case, you need to know the behavior of the user and the goals of the page before implementing the skeleton component.

I hope this post helped give you some ideas, please do share your feedback within the comments section, I'd love to hear your thoughts!

## Resource

- [https://www.figma.com/community/file/777205710295345433](https://www.figma.com/community/file/777205710295345433)
- [https://uxdesign.cc/what-you-should-know-about-skeleton-screens-a820c45a571a](https://uxdesign.cc/what-you-should-know-about-skeleton-screens-a820c45a571a)