Loading Skeletons in Vue with Tailwind
Written on: November, 2024
•4 min readLoading states are essential in frontend development, providing feedback during data fetches or complex rendering. This is especially beneficial for users with slower connections or large datasets. In this post, I will show you how to manage loading skeleton components in a Vue project using Tailwind CSS, with a mock API to simulate data fetching. Follow these steps to effectively implement loading skeletons in your projects.
Steps
- Generate Random User Data
- Set Up Mock API with MSW
- Enable MSW in Development
- State Management with Pinia
- Create UserSkeleton Component
- Create UserList Component
- Create UserView Component
- Loading State UI
- Success State UI
I created functions to generate random user data. If you don't have this data in your backend, you can implement your own functions. Find the code for these functions at the link below.
https://github.com/Aldikrasniqi/vue-tailwind-loading-skeleton/blob/main/src/utils/utils.tsMock Service Worker (MSW) allows you to intercept and mock API requests during development. Create a handlers.ts file in the src/mocks directory to define API endpoints and responses, simulating backend interactions without a server.
https://github.com/Aldikrasniqi/vue-tailwind-loading-skeleton/blob/main/src/mocks/handlers.ts1npm i msw
1import { http, HttpResponse } from 'msw'
2import type { User } from '@/interfaces/user'
3import { posts, users } from '@/utils/utils'
4import type { Post } from '@/interfaces/post'
5
6export const handlers = [
7 http.get('/users', () => {
8 return HttpResponse.json<User[]>(users)
9 }),
10
11 http.get('/posts', () => {
12 return HttpResponse.json<Post[]>(posts)
13 }),
14]
MSW intercepts API calls and returns mock data. To enable it, create a `browser.ts` file in the `src/mocks` folder and set up the MSW worker. Import this file into `src/main.ts` to initialize the mock server.
1import './assets/index.css'
2
3import { createApp } from 'vue'
4import { createPinia } from 'pinia'
5
6async function prepareApp() {
7 const { worker } = await import('./mocks/browser')
8 return worker.start()
9}
10
11import App from './App.vue'
12import router from './router'
13
14const app = createApp(App)
15
16app.use(createPinia())
17app.use(router)
18
19prepareApp().then(() => {
20 app.mount('#app')
21})
Pinia is a user-friendly state management library for Vue.js. It allows you to define stores as reactive data containers. Create a `users.ts` file in the `src/stores` directory to define the `users` store, which includes an array for users and a loading flag. The `fetchUsers` action retrieves users from the mock API.
1import type { User } from '@/interfaces/user'
2import { defineStore } from 'pinia'
3import { getUsers } from '@/services/user.service'
4export const useUsersStore = defineStore('users', {
5 state: () => ({
6 users: [] as User[],
7 loadUsers: false,
8 }),
9 actions: {
10 async fetchUsers() {
11 this.loadUsers = true
12 const resp = await getUsers()
13 if (resp?.data) {
14 this.users = resp.data
15 setTimeout(() => {
16 this.loadUsers = false
17 }, 2000)
18 } else {
19 this.loadUsers = false
20 }
21 },
22 },
23})
Build a UserSkeleton component in the src/components/ui/users folder. It accepts the number of columns and rows as props, using a `for` loop to render the skeleton. Tailwind provides classes for styling, and I use the `animate-pulse` class for animation. Find the code for the UserSkeleton component at the link below.
https://github.com/Aldikrasniqi/vue-tailwind-loading-skeleton/blob/main/src/components/ui/users/UserSkeleton.vueBuild a UserList component in the src/components/ui/users folder to display user data in a list. This component separates UI from logic, accepting user objects as props for rendering. Find the code for the UserList component at the link below.
https://github.com/Aldikrasniqi/vue-tailwind-loading-skeleton/blob/main/src/components/ui/users/UserList.vueCreate a UserView component in the src/components/ui/users folder to pass user objects to the UserList and UserSkeleton components, maintaining separation of UI and logic.
https://github.com/Aldikrasniqi/vue-tailwind-loading-skeleton/blob/main/src/components/ui/users/UserView.vue1<script setup lang="ts">
2import UserList from '@/components/ui/users/UserList.vue'
3import UsersLoadingSkeleton from '@/components/ui/users/UsersLoadingSkeleton.vue'
4import { useUsersStore } from '@/stores/users'
5import { onMounted } from 'vue'
6const store = useUsersStore()
7
8onMounted(async () => {
9 store.fetchUsers()
10})
11</script>
12
13<template>
14 <main>
15 <div>
16 <h1 class="md:py-6 px-3 text-2xl font-semibold">Users</h1>
17 <div v-if="store.loadUsers">
18 <UsersLoadingSkeleton :numberOfSkeletons="store.users.length" />
19 </div>
20 <div v-for="user in store.users" v-if="!store.loadUsers" :key="user.id">
21 <UserList
22 :id="user.id"
23 :firstName="user.firstName"
24 :lastName="user.lastName"
25 :username="user.username"
26 :email="user.email"
27 :profilePicture="user.profilePicture"
28 />
29 </div>
30 </div>
31 </main>
32</template>
While the mock API fetches data, the `UserSkeleton` component shows loading progress, providing users with feedback that the system is working.

Once data is fetched, the loading skeleton is replaced by user data in the UI, enhancing user satisfaction by indicating task completion.
