Skeleton Loader
FreeLoading placeholders with wave/pulse animations and preset layouts.
</>sv-skeleton-loader skeletonloadingplaceholder
import { SvSkeletonLoaderComponent } from '@styloviz/skeleton-loader';Demo & usage
Basic usage
Usage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Text lines
example.htmlAngular
1 lines
<sv-skeleton-loader preset="text-block" animation="pulse" />
Custom bones + label
example.htmlAngular
1 lines
<sv-skeleton-loader [lines]="3" [shortLastLine]="true" ariaLabel="Loading report" />
Escape a load that never finishes
example.htmlAngular
10 lines
<!-- A stuck skeleton lies in the exact place the data was going: it says "the shape of your content is arriving" forever if the request failed silently. Both thresholds run from the moment the skeleton appears. --> <sv-skeleton-loader preset="table" [slowAfterMs]="8000" [stalledAfterMs]="25000" stalledMessage="We couldn't load these rows." retryLabel="Try again" (retry)="reload()" />
When to use
- Filling space while content loads, preserving the eventual layout.
- Cards, lists, tables and profiles where you know the shape ahead of time.
- When you can't predict the layout, a Spinner is simpler.
Async states
- retry
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| preset | SkeletonPreset | 'custom' | High-level composed preset. Set to `'custom'` (or omit) to control individual bone inputs. |
| shape | SkeletonShape | 'line' | Shape of the skeleton bone. |
| width | string | '100%' | CSS width applied to the bone element. Accepts any valid CSS value: `'100%'`, `'8rem'`, `'w-1/2'` (Tailwind class). |
| height | string | '1rem' | CSS height applied to the bone element. |
| rounded | string | '' | Border-radius override. Accepts Tailwind rounded-* class suffix: `'none'` `'sm'` `'md'` `'lg'` `'xl'` `'full'`. Shape defaults apply when left empty. |
| lines | number | 1 | Repeat this bone N times stacked vertically. Gaps between repeated lines are automatic. |
| shortLastLine | boolean | true | When `lines > 1`, make the last line shorter (like a real paragraph ending). |
| animation | SkeletonAnimation | 'wave' | Shimmer animation style. |
| size | 'sm' | 'md' | 'lg' | 'md' | Overall size multiplier that scales preset bone heights. |
| customClass | string | '' | Additional CSS classes merged onto the root wrapper. |
| ariaLabel | string | 'Loading content…' | Accessible label announced to screen readers while content loads. |
| announce | boolean | true | Whether this skeleton announces the wait itself. |
| slowAfterMs | number | 0 | Say "this is taking longer than usual" after this many ms. `0` = never. A STUCK SKELETON IS INDISTINGUISHABLE FROM A FAILED REQUEST, AND IT LIES IN THE EXACT PLACE THE DATA WAS GOING. It says "the shape of your content is arriving" — permanently, if the host's request failed silently. There was no timeout, no error and no retry: the placeholder simply outlived the thing it was standing in for. Off by default, because the honest threshold depends on what is loading and because turning it on adds text to every existing layout. |
| stalledAfterMs | number | 0 | Escalate to a stuck message (with a retry offer) after this many ms. Measured from the same start, not from `slowAfterMs`. `0` = never. |
| slowMessage | string | 'This is taking longer than usual…' | Message shown at the `slow` stage. |
| stalledMessage | string | 'This content could not be loaded.' | Message shown at the `stalled` stage. |
| retryLabel | string | null | null | Label for the retry offer shown once the wait is `stalled`. `null` = none. AN OUTPUT CANNOT BE ASKED WHETHER ANYONE IS LISTENING, so the offer is an input: a placeholder that renders "Try again" and does nothing when it is pressed is worse than one that never offered. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| retry | void | The user pressed the retry offer on a stalled skeleton. |
| escalated | SvWaitStage | Fires on each escalation: `'slow'`, then `'stalled'`. |
Customization & theming
- Preset layouts (text-block, card, list, table, dashboard…).
- Wave or pulse animation.
- Custom shapes via composition; `customClass` and `ariaLabel`.
- Escalation: `[slowAfterMs]` says "this is taking longer than usual", `[stalledAfterMs]` says the content could not be loaded and can offer a `retryLabel` button wired to `(retry)`; `(escalated)` reports each step. Off by default, and shared with the Spinner — `createWaitEscalation` in `@styloviz/core`.
Best practices
- Do: Match the skeleton's shape to the real content so the swap isn't jarring.
- Do: Decide who announces the wait — the skeleton (default) or your own region via `[announce]="false"` — and let only one of them do it.
- Do: Put `aria-busy` on the region you are swapping, not on the skeleton, and clear it when the content lands.
- Do: Give any skeleton that can hang a `[stalledAfterMs]` and a `retryLabel`. Without one it is indistinguishable from a failed request — permanently, and in the exact place the content was going to be, so the page still looks like it is working.
- Don't: Don't show skeletons for sub-second loads — the flash is more distracting than helpful.
Accessibility
- `role="status"` with a configurable `ariaLabel`, which is SAID once through the shared `SvLiveAnnouncer` when the skeleton first renders (politely, browser only, de-duplicated across skeletons) — a status region born with its name and no content is usually never read. ⚠️ Deliberately NOT `aria-busy`: that attribute tells assistive technology to stop reporting changes from the subtree, so putting it on this element silenced the very region it is here to speak from — and it could never flip back, because the skeleton is destroyed when content arrives rather than updated.
- `aria-busy` belongs on the region whose contents are CHANGING — yours, around the swap — not on the placeholder, which IS the busy indicator.
- Exactly one owner announces the wait. Leave `announce` on and say nothing yourself, or mark your own region busy, announce the transition, and set `[announce]="false"` so the skeleton becomes `aria-hidden` decoration. Two voices for one wait is worse than one.
- Both wave and pulse animations stop under `prefers-reduced-motion`.
- An escalation message renders INSIDE the existing `role="status"` wrapper rather than in a region of its own — one wait, one voice, and a region that already existed when the text appeared is what makes the change audible. With `[announce]="false"` the host owns the announcement of the stall too, along with everything else about the wait — but once a stalled skeleton offers its `retryLabel` button the wrapper stops being `aria-hidden`, because a focusable control inside a hidden subtree is reachable by Tab and unnamed to a screen reader.
Feedback on Skeleton Loader
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.