HTML Formatterhtmlformatteronline.com

Loaders and skeletons

Six loading indicators in pure CSS, plus the skeleton pattern that usually beats all of them.

cssloaderspinnerskeletonanimation
<div class="shelf">
  <figure><div class="spinner"></div><figcaption>Spinner</figcaption></figure>
  <figure><div class="dots"><i></i><i></i><i></i></div><figcaption>Dots</figcaption></figure>
  <figure><div class="bar"></div><figcaption>Indeterminate bar</figcaption></figure>
  <figure><div class="pulse"></div><figcaption>Pulse</figcaption></figure>
  <figure><div class="ring"></div><figcaption>Progress ring</figcaption></figure>
  <figure><div class="bars"><i></i><i></i><i></i><i></i></div><figcaption>Bars</figcaption></figure>
</div>

<h3>Skeleton screen</h3>
<div class="skeleton" aria-busy="true" aria-live="polite" aria-label="Loading article">
  <div class="sk sk-img"></div>
  <div class="sk sk-line w70"></div>
  <div class="sk sk-line w90"></div>
  <div class="sk sk-line w50"></div>
</div>

How it works

The skeleton is usually the right answer. A spinner says "something is happening". A skeleton says "here is the shape of what is coming", which makes the wait feel shorter and stops the page jumping when the content lands. Use skeletons for content you are fetching, spinners for actions you have triggered.

The spinner is one element. A border on all four sides, a different colour on one of them, and a rotation. No SVG, no images, no extra markup.

The progress ring uses conic-gradient and a mask. The gradient draws a filled pie; the radial mask punches out the middle to leave a ring. The fill is one custom property, so style="--p: 68" from your own code is all it takes to drive it — no SVG stroke-dasharray arithmetic.

Stagger with animation-delay, not separate keyframes. The dots and bars share one animation and differ only by delay. Three lines instead of three animations.

The shimmer moves a gradient, not the element. transform: translateX on a pseudo-element runs on the compositor. Animating background-position instead — the common version of this effect — forces a repaint on every frame.

Reduced motion is not "no feedback". Under prefers-reduced-motion the animations stop but the indicators remain visible, with the bar shown as a static filled track. Removing the indicator entirely would leave those users with no sign that anything is loading at all.

Accessibility notes

A loading indicator with no text is invisible to a screen reader. The skeleton block here carries aria-busy="true", aria-live="polite" and an aria-label, so the state is announced once and again when it changes.

Do not use aria-live="assertive" for loading. It interrupts whatever is being read. polite waits for a pause, which is what you want for a status update.

Avoid anything that flashes more than three times a second — that threshold is a seizure risk and it is in WCAG for a reason. None of these cross it, but fast custom animations easily can.

Making it yours

Size everything from the element's own width and height. All six scale correctly because the internals are proportional or in em-like ratios.

For a determinate bar, drop the animation and set the width from your own progress value. Add role="progressbar" with aria-valuenow, aria-valuemin and aria-valuemax so the figure is announced.

Related templates

Check your version

Once you have edited this, the HTML validator will catch any tag you left unclosed, and the formatter will tidy the indentation. Both run in your browser.