Displays a shimmer animation over any element while a reactive expression is truthy. When the expression becomes falsy, the skeleton fades out and reveals the real content underneath.
<!-- Basic text skeleton (active while `loading` is truthy) -->
<h2 skeleton="loading">Article Title</h2>
<p skeleton="loading">This paragraph appears after loading completes.</p>
<!-- Circle avatar placeholder -->
<div skeleton="loading" skeleton-type="circle" skeleton-size="64"></div>
<!-- Multi-line text placeholder -->
<div skeleton="loading" skeleton-lines="3"></div>
<!-- Rectangular card placeholder -->
<div skeleton="loading" skeleton-type="rect" skeleton-size="200"></div>| Attribute | Type | Default | Description |
|---|---|---|---|
skeleton |
expression | required | Reactive expression — shows the skeleton placeholder while truthy |
skeleton-type |
"text" | "circle" | "rect" |
"text" |
Shape of the placeholder. text renders full-width rounded bars, circle renders a circular placeholder, rect renders a rectangular placeholder |
skeleton-lines |
number | — | Number of simulated text lines to generate. Each line is a shimmer bar; the last line is 60% width for a natural look |
skeleton-size |
number | string | — | Explicit dimension (width and height) for circle or rect types. Appends px if the value is a bare number |
The skeleton directive is fully reactive. It watches the expression via $watch and toggles automatically:
<div state="{ loading: true, user: null }">
<!-- Skeleton activates immediately, disappears when `loading` turns false -->
<div skeleton="loading" skeleton-lines="2" style="width: 300px; min-height: 48px;">
<h3 bind="user.name"></h3>
<p bind="user.email"></p>
</div>
<button on:click="loading = true; fetch('/api/user').then(r => r.json()).then(u => { user = u; loading = false })">
Load User
</button>
</div>When loading is true:
- Real children become invisible (
opacity: 0,pointer-events: none) - A shimmer animation plays over the element via
::after - The element's original dimensions are maintained
When loading becomes false:
- The
.nojs-skeletonclass is removed - A
.nojs-skeleton-fadetransition class fades the skeleton out - Real content is revealed
The default type. Useful for paragraphs, headings, and any text content.
<!-- Single element — shimmer covers the full element area -->
<p skeleton="loading" style="min-height: 1.2em; width: 200px;">
Real text content here
</p>
<!-- Multi-line placeholder (generates line divs inside the element) -->
<div skeleton="loading" skeleton-lines="4" style="width: 400px;"></div>Each generated line (.nojs-skeleton-line) is 0.8em tall with 0.6em bottom margin. The last line is 60% width to create a realistic paragraph shape.
<div state="{ loadingAvatar: true }">
<!-- 64px circular placeholder -->
<div skeleton="loadingAvatar" skeleton-type="circle" skeleton-size="64"
style="border-radius: 50%;">
<img src="" bind:src="avatarUrl" alt="Avatar" />
</div>
</div>The circle type adds .nojs-skeleton-circle, which forces a circular border-radius: 50% on the ::after overlay. The skeleton-size attribute sets both width and height.
Combine multiple skeleton elements for card layouts:
<div state="{ loading: true, post: null }">
<div class="card" style="padding: 16px; max-width: 400px;">
<!-- Avatar -->
<div skeleton="loading" skeleton-type="circle" skeleton-size="48"
style="border-radius: 50%; margin-bottom: 12px;">
<img bind:src="post.authorAvatar" alt="" />
</div>
<!-- Title -->
<h3 skeleton="loading" style="min-height: 1.5em;">
<span bind="post.title"></span>
</h3>
<!-- Body text -->
<div skeleton="loading" skeleton-lines="3" style="min-height: 72px;">
<p bind="post.body"></p>
</div>
</div>
</div>No.JS Elements automatically injects these classes via <style data-nojs-skeleton>:
| Class | When applied |
|---|---|
.nojs-skeleton |
On the element while the skeleton is active |
.nojs-skeleton > * |
Children become invisible (opacity: 0, pointer-events: none) |
.nojs-skeleton::after |
Shimmer animation overlay (linear-gradient sweep) |
.nojs-skeleton-circle |
Applied with skeleton-type="circle" — forces border-radius: 50% on the overlay |
.nojs-skeleton-fade |
Transition class added during deactivation (opacity 0.3s ease) |
.nojs-skeleton-line |
Generated text line placeholders (0.8em height, 0.6em margin) |
.nojs-skeleton-line:last-child |
Last line is 60% width for a natural paragraph look |
The shimmer uses @keyframes nojs-shimmer — a linear-gradient that sweeps from right to left over 1.5s with ease-in-out timing, repeating infinitely. The gradient uses color-mix(in srgb, currentColor %, transparent) for automatic dark mode compatibility: the shimmer adapts to the element's inherited color without extra configuration.
No.JS Elements automatically adds:
aria-busy="true"on the element while the skeleton is activearia-busyis removed when the skeleton deactivates- Children are hidden from interaction via
pointer-events: nonewhile loading
Previous: Stepper