# 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.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: Feedback
- Tags: skeleton, loading, placeholder, shimmer, pulse, micro-interaction
- Types: `SkeletonProps`
- Docs: https://fancy-ui.rama.app/docs/components/skeleton
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/skeleton

## Import

```svelte
<script lang="ts">
  import { Skeleton } from 'fancy-ui-svelte';
</script>
```

Setup (once per project): `npm install fancy-ui-svelte`, then add `@import "fancy-ui-svelte/tailwind.css";` after `@import "tailwindcss";` in your main CSS file. Full guide: https://fancy-ui.rama.app/llms.txt

React: `import { Skeleton } from 'fancy-ui-react';` (`class` becomes `className`, snippets become `ReactNode`, DOM callbacks are camel-cased; exact names on https://fancy-ui.rama.app/docs/components/react/skeleton.md).

## 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 |

## Snippets

| Snippet | Description |
| --- | --- |
| `children` | Real content to reveal once loading is false. Its mere presence (not its value) switches Skeleton from standalone to wrapping mode |

## Examples

### Basic Usage

```svelte
<script lang="ts">
	import { Skeleton } from "fancy-ui-svelte";
	import { Presence } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

	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 "fancy-ui-svelte";
</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 "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

	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>
```
