Skeleton
<div class="animate-pulse">
Animated placeholder elements that mirror the layout of incoming content. Reduces perceived load time by showing structure immediately.
Overview
Preview
Base skeleton
All skeletons use animate-pulse from Tailwind and bg-zinc-800. Adjust size with width/height utilities.
Preview
html
<!-- Text lines -->
<div class="h-4 w-3/4 rounded bg-zinc-800 animate-pulse"></div>
<div class="h-3 w-full rounded bg-zinc-800 animate-pulse"></div>
<div class="h-3 w-5/6 rounded bg-zinc-800 animate-pulse"></div>
<!-- Avatar circle -->
<div class="h-10 w-10 rounded-full bg-zinc-800 animate-pulse"></div>
<!-- Button -->
<div class="h-9 w-24 rounded-md bg-zinc-800 animate-pulse"></div>
<!-- Badge -->
<div class="h-5 w-14 rounded-full bg-zinc-800 animate-pulse"></div>Card skeleton
Preview
html
<div class="rounded-xl border border-zinc-800 bg-zinc-900/50 p-5 space-y-3">
<!-- Avatar + name -->
<div class="flex items-center gap-3">
<div class="h-9 w-9 shrink-0 rounded-full bg-zinc-800 animate-pulse"></div>
<div class="space-y-1.5 flex-1">
<div class="h-3 w-24 rounded bg-zinc-800 animate-pulse"></div>
<div class="h-2.5 w-16 rounded bg-zinc-800/60 animate-pulse"></div>
</div>
</div>
<!-- Body text -->
<div class="space-y-2">
<div class="h-2.5 w-full rounded bg-zinc-800 animate-pulse"></div>
<div class="h-2.5 w-5/6 rounded bg-zinc-800 animate-pulse"></div>
<div class="h-2.5 w-4/6 rounded bg-zinc-800/60 animate-pulse"></div>
</div>
<!-- Buttons -->
<div class="flex gap-2">
<div class="h-7 w-20 rounded-md bg-zinc-800 animate-pulse"></div>
<div class="h-7 w-16 rounded-md bg-zinc-800/60 animate-pulse"></div>
</div>
</div>Table skeleton
html
<div class="rounded-xl border border-zinc-800 overflow-hidden">
<!-- Header -->
<div class="flex gap-4 border-b border-zinc-800 bg-zinc-900/30 px-5 py-3">
<div class="h-2.5 w-20 rounded bg-zinc-800/80 animate-pulse"></div>
<div class="h-2.5 w-24 rounded bg-zinc-800/80 animate-pulse ml-auto"></div>
<div class="h-2.5 w-16 rounded bg-zinc-800/80 animate-pulse"></div>
</div>
<!-- Rows -->
<div v-for="i in 5" :key="i" class="flex items-center gap-4 border-b border-zinc-800/50 px-5 py-3">
<div class="h-7 w-7 rounded-full bg-zinc-800 animate-pulse shrink-0"></div>
<div class="h-2.5 flex-1 rounded bg-zinc-800 animate-pulse"></div>
<div class="h-4 w-16 rounded-full bg-zinc-800/60 animate-pulse ml-auto"></div>
</div>
</div>With Vue (show real content when loaded)
vue
<script setup>
const { data, pending } = await useFetch('/api/posts')
</script>
<template>
<!-- Skeleton while loading -->
<div v-if="pending" class="space-y-3">
<div v-for="i in 3" :key="i" class="rounded-xl border border-zinc-800 p-5">
<div class="flex items-center gap-3">
<div class="h-9 w-9 rounded-full bg-zinc-800 animate-pulse"></div>
<div class="space-y-2 flex-1">
<div class="h-3 w-1/3 rounded bg-zinc-800 animate-pulse"></div>
<div class="h-2.5 w-1/2 rounded bg-zinc-800 animate-pulse"></div>
</div>
</div>
</div>
</div>
<!-- Real content -->
<div v-else class="space-y-3">
<PostCard v-for="post in data" :key="post.id" :post="post" />
</div>
</template>Shimmer variant
For a more dynamic feel, replace animate-pulse with a shimmer sweep:
html
<div class="h-4 w-full overflow-hidden rounded bg-zinc-800 relative">
<div class="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-zinc-700/30 to-transparent animate-[shimmer_1.5s_ease-in-out_infinite]"></div>
</div>Add to tailwind.config.js:
js
keyframes: {
shimmer: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(200%)' },
},
},Accessibility notes
- Skeleton containers should have
aria-busy="true"while loading andaria-busy="false"when content is ready - Add
aria-label="Loading..."to the skeleton container for screen readers - Remove skeletons from the DOM (don't hide with
opacity-0) once content loads