Feedback Core Stable
Skeleton
Placeholder bones — a block, one or more text lines, or a circular avatar — with a phase-synced shimmer sweep or opacity pulse, that swaps to real content once loading finishes while keeping aria-busy semantics correct throughout
Preview
svelte
<script lang="ts">
import { Skeleton } from 'fancy-ui-svelte';
</script>
<Skeleton variant="text" lines={3} class="w-72 text-base" />Installation
pnpm add fancy-ui-svelte
import { Skeleton } from 'fancy-ui-svelte'
Usage
svelte
<script lang="ts">
import { Skeleton } from 'fancy-ui-svelte';
</script>
<Skeleton />Examples
Basic Usage
svelte
<script lang="ts">
import { Skeleton } from "$lib/fancy-ui/skeleton";
import { Presence } from "$lib/fancy-ui/presence";
import { Button } from "$lib/fancy-ui/button";
let loading = $state(true);
</script>
<div class="flex w-full flex-col items-center gap-4 p-6">
<div class="bg-card border-border flex w-full max-w-sm items-start gap-3 rounded-xl border p-4">
<!-- Avatar bone: label="" so only the text bone below announces "Loading" —
two Skeletons in one card shouldn't produce two live regions. -->
<Skeleton variant="circle" {loading} label="" class="size-10 shrink-0" />
<Skeleton variant="text" lines={3} {loading} class="w-full" />
<!-- Presence stays mounted the whole time with `open={!loading}`; that's what
lets its fade actually play once loading flips (a Presence created already
`open` never plays its own intro — see Presence's own doc comment). -->
<Presence open={!loading} preset="fade" class="flex w-full items-start gap-3">
<div
class="bg-primary/15 text-primary flex size-10 shrink-0 items-center justify-center rounded-full text-sm font-semibold"
aria-hidden="true"
>
MC
</div>
<div class="flex flex-col gap-1">
<p class="text-foreground text-sm font-medium">Maya Chen</p>
<p class="text-muted-foreground text-sm">
Approved the design — ship it whenever you're ready.
</p>
</div>
</Presence>
</div>
<Button size="sm" variant="outline" onclick={() => (loading = !loading)}>
{loading ? "Load message" : "Reset"}
</Button>
</div>Variants
Rect, text, and circle bones across all three animations.
svelte
<script lang="ts">
import { Skeleton } from "$lib/fancy-ui/skeleton";
</script>
<div class="flex w-full flex-col gap-8 p-6">
<div class="flex flex-wrap items-end gap-6">
<div class="flex flex-col items-center gap-2">
<Skeleton variant="circle" class="size-12" />
<span class="text-muted-foreground text-xs">circle</span>
</div>
<div class="flex min-w-32 flex-1 flex-col items-center gap-2">
<Skeleton variant="rect" label="" class="h-12 w-full" />
<span class="text-muted-foreground text-xs">rect</span>
</div>
<div class="flex min-w-40 flex-1 flex-col items-center gap-2">
<!-- Every bone below already sits under one "Loading" announcement (the
circle above); the rest stay silent with label="" rather than each
opening a second, third… live region for the same demo card. -->
<Skeleton variant="text" lines={3} label="" class="w-full" />
<span class="text-muted-foreground text-xs">text (lines=3)</span>
</div>
</div>
<div class="flex flex-wrap items-end gap-6">
<div class="flex min-w-32 flex-1 flex-col gap-2">
<Skeleton variant="text" lines={2} animation="shimmer" label="" class="w-full" />
<span class="text-muted-foreground text-xs">animation="shimmer"</span>
</div>
<div class="flex min-w-32 flex-1 flex-col gap-2">
<Skeleton variant="text" lines={2} animation="pulse" label="" class="w-full" />
<span class="text-muted-foreground text-xs">animation="pulse"</span>
</div>
<div class="flex min-w-32 flex-1 flex-col gap-2">
<Skeleton variant="text" lines={2} animation="none" label="" class="w-full" />
<span class="text-muted-foreground text-xs">animation="none"</span>
</div>
</div>
</div>With Children
Wrapping mode: real content swaps in once loading is false.
svelte
<script lang="ts">
import { Skeleton } from "$lib/fancy-ui/skeleton";
import { Button } from "$lib/fancy-ui/button";
let loading = $state(true);
</script>
<div class="flex w-full max-w-sm flex-col items-start gap-4 p-6">
<!-- Wrapping mode: `children` is always supplied, so Skeleton itself decides
what to show — bones + aria-busy while loading, the real content once it
isn't. No `{#if}` needed around the swap; that's the whole point of this
mode over the standalone one used in BasicUsage. -->
<Skeleton {loading} variant="text" lines={2} class="w-full">
<p class="text-foreground text-sm">
Your invoice for August is ready — total due <strong>$128.40</strong>, payable by the 30th.
</p>
</Skeleton>
<Button size="sm" variant="outline" onclick={() => (loading = !loading)}>
{loading ? "Show content" : "Show loading"}
</Button>
</div>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "rect" | "text" | "circle" | "rect" | Bone shape: a block, one or more text lines, or a circular avatar |
lines | number | 1 | Number of text lines to render; only read when variant is "text". The last line renders at 60% width so a paragraph placeholder doesn't read as a perfect rectangle |
animation | "shimmer" | "pulse" | "none" | "shimmer" | Shimmer sweep, opacity pulse, or a static muted bone (still a valid loading cue on its own) |
loading | boolean | true | Whether the placeholder is currently showing. In wrapping mode (children supplied) drives the swap to real content; in standalone mode drives whether anything renders at all |
label | string | "Loading" | The one screen-reader announcement. Pass "" to silence it entirely |
class | string | - | Additional CSS classes; also the sizing hook, since rect/text bones have no intrinsic size |
ref | HTMLDivElement | null | null | Bindable reference to the root element; null whenever nothing is rendered |
Slots
| Slot | Description |
|---|---|
children | Real content to reveal once loading is false. Its mere presence (not its value) switches Skeleton from standalone to wrapping mode |
Links
Related components
An
AnimatedTooltip
Avatar row with animated tooltips that follow mouse movement
Feedback Stable
St
StatusMorph
A 1em SVG status icon that morphs between idle, loading, success, and error, closing its spinning ring into a drawn check or cross with a portalled live region announcing each state
Feedback Stable