Skeleton
A placeholder that mimics the shape of content while it loads. Showing a skeleton in place of the eventual layout makes loading feel faster and avoids the jolt of content popping in — the page keeps its shape while data arrives.
Skeleton is deliberately minimal: it's an animated box you shape yourself with utility classes. Size it, round it, and arrange a few together to match whatever you're standing in for — a line of text, an avatar, a card.
Usage
Shape each skeleton with class — height, width, and radius. A few stacked lines stand in for a paragraph.
<script setup lang="ts">
import { Skeleton } from "@dlbcodes/ui";
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-2">
<Skeleton class="h-4 w-3/4" />
<Skeleton class="h-4 w-full" />
<Skeleton class="h-4 w-5/6" />
</div>
</template>
Shapes
The same component becomes a circle, a line, or a block depending on the classes you give it.
<script setup lang="ts">
import { Skeleton } from "@dlbcodes/ui";
</script>
<template>
<div class="flex items-center gap-4">
<!-- circle (avatar) -->
<Skeleton class="size-12 rounded-full" />
<!-- line -->
<Skeleton class="h-4 w-32" />
<!-- block -->
<Skeleton class="h-16 w-24 rounded-xl" />
</div>
</template>
Composing a card
Combine skeletons to mirror the layout you're loading — here, an avatar and a few lines of text, the classic "loading card."
<script setup lang="ts">
import { Skeleton } from "@dlbcodes/ui";
</script>
<template>
<div
class="w-full max-w-sm rounded-2xl border border-border-subtle bg-bg-raised p-5"
>
<div class="flex items-center gap-3">
<Skeleton class="size-12 shrink-0 rounded-full" />
<div class="flex flex-1 flex-col gap-2">
<Skeleton class="h-4 w-1/2" />
<Skeleton class="h-3 w-1/3" />
</div>
</div>
<div class="mt-4 flex flex-col gap-2">
<Skeleton class="h-3 w-full" />
<Skeleton class="h-3 w-full" />
<Skeleton class="h-3 w-2/3" />
</div>
</div>
</template>
In practice
Show skeletons while data is loading, then swap in the real content. Mark the loading container aria-busy="true" so assistive tech knows content is on its way.
<script setup lang="ts">
import { ref } from "vue";
import { Skeleton, Button } from "@dlbcodes/ui";
const loading = ref(true);
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<Button
variant="outline"
size="sm"
class="w-fit"
@click="loading = !loading"
>
{{ loading ? "Show content" : "Show skeleton" }}
</Button>
<div v-if="loading" class="flex flex-col gap-2" aria-busy="true">
<Skeleton class="h-5 w-1/2" />
<Skeleton class="h-4 w-full" />
<Skeleton class="h-4 w-5/6" />
</div>
<div v-else class="flex flex-col gap-2">
<div class="text-lg font-semibold text-text-primary">
Loaded title
</div>
<div class="text-sm text-text-secondary">
This is the real content that appears once loading finishes.
</div>
</div>
</div>
</template>
Appearance
A skeleton is filled with the theme's --accent color and pulses. It has the theme's medium corner radius by default, so it follows --radius. Pass your own radius to change it, such as rounded-full for an avatar placeholder. On a very light page the fill is subtle. If you need more contrast, pass class="bg-border". See Theming.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | — | Classes that shape the skeleton — height, width, radius (e.g. h-4 w-32, size-12 rounded-full). |
Skeleton intentionally takes only class. You control its shape entirely with utilities, so it can stand in for any element.
Accessibility
- The skeleton is
aria-hidden="true"— it's a purely visual placeholder, so it's hidden from assistive technology (a screen reader has nothing meaningful to announce for an empty placeholder). - Mark the container that's loading with
aria-busy="true"while the skeletons are shown, so assistive tech knows real content is coming. Remove it once the content has loaded.