Skeleton Loader

Free

Loading 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 demo

More examples

Text lines

example.htmlAngular
<sv-skeleton-loader preset="text-block" animation="pulse" />

Custom bones + label

example.htmlAngular
<sv-skeleton-loader [lines]="3" [shortLastLine]="true" ariaLabel="Loading report" />

Escape a load that never finishes

example.htmlAngular
<!-- 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

NameTypeDefaultDescription
presetSkeletonPreset'custom'High-level composed preset. Set to `'custom'` (or omit) to control individual bone inputs.
shapeSkeletonShape'line'Shape of the skeleton bone.
widthstring'100%'CSS width applied to the bone element. Accepts any valid CSS value: `'100%'`, `'8rem'`, `'w-1/2'` (Tailwind class).
heightstring'1rem'CSS height applied to the bone element.
roundedstring''Border-radius override. Accepts Tailwind rounded-* class suffix: `'none'` `'sm'` `'md'` `'lg'` `'xl'` `'full'`. Shape defaults apply when left empty.
linesnumber1Repeat this bone N times stacked vertically. Gaps between repeated lines are automatic.
shortLastLinebooleantrueWhen `lines > 1`, make the last line shorter (like a real paragraph ending).
animationSkeletonAnimation'wave'Shimmer animation style.
size'sm' | 'md' | 'lg''md'Overall size multiplier that scales preset bone heights.
customClassstring''Additional CSS classes merged onto the root wrapper.
ariaLabelstring'Loading content…'Accessible label announced to screen readers while content loads.
announcebooleantrueWhether this skeleton announces the wait itself.
slowAfterMsnumber0Say "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.
stalledAfterMsnumber0Escalate to a stuck message (with a retry offer) after this many ms. Measured from the same start, not from `slowAfterMs`. `0` = never.
slowMessagestring'This is taking longer than usual…'Message shown at the `slow` stage.
stalledMessagestring'This content could not be loaded.'Message shown at the `stalled` stage.
retryLabelstring | nullnullLabel 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

EventPayload typeDescription
retryvoidThe user pressed the retry offer on a stalled skeleton.
escalatedSvWaitStageFires 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.

Loads giscus.app and GitHub — nothing is fetched until you click.