---
title: "CSS Loading Animations: Spinners, Dots & Skeletons"
description: "Eight free CSS loading animations with live previews: spinners, bouncing dots, progress bars, skeleton shimmers, and ripples. Pure CSS, no libraries."
url: https://frontendhelpers.com/css-loading-animations
updated: 2026-10-12
---

# CSS loading animations

Spinners, bouncing dots, skeleton shimmers and progress bars — every one pure CSS, previewed live, and free to copy.

## The code for all 8 loaders, explained

Every effect above, with the markup to use and the complete CSS. Each one is self-contained: paste the CSS once, add the class, done.

### Ring Spinner

The classic. One element, four lines of keyframes. Add role="status" and an aria-label so screen readers announce it.

Markup

```html
<div class="fh-spinner" role="status" aria-label="Loading"></div>
```

CSS

```css
.fh-spinner {
  width: 44px;
  height: 44px;
  border: 4px solid rgba(139, 92, 246, 0.2);
  border-top-color: #8b5cf6;
  border-radius: 50%;
  animation: fh-spin 0.8s linear infinite;
}
@keyframes fh-spin {
  to { transform: rotate(360deg); }
}
```

### Dual Ring

Two opposing arcs spinning together. Colour the top and bottom borders with your two brand colours.

Markup

```html
<div class="fh-dual-ring" role="status" aria-label="Loading"></div>
```

CSS

```css
.fh-dual-ring {
  width: 44px;
  height: 44px;
  border: 4px solid transparent;
  border-top-color: #8b5cf6;
  border-bottom-color: #06b6d4;
  border-radius: 50%;
  animation: fh-spinDual 1s linear infinite;
}
@keyframes fh-spinDual {
  to { transform: rotate(360deg); }
}
```

### Bouncing Dots

The “someone is typing” pattern. The staggered animation-delay on each dot creates the wave.

Markup

```html
<div class="fh-dots-bounce" role="status" aria-label="Loading">
  <span></span><span></span><span></span>
</div>
```

CSS

```css
.fh-dots-bounce {
  display: flex;
  gap: 8px;
  align-items: flex-end;
}
.fh-dots-bounce span {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #06b6d4;
  animation: fh-dotBounce 0.6s ease-in-out infinite alternate;
}
.fh-dots-bounce span:nth-child(2) { animation-delay: 0.15s; }
.fh-dots-bounce span:nth-child(3) { animation-delay: 0.3s; }
@keyframes fh-dotBounce {
  from { transform: translateY(0);     opacity: 0.6; }
  to   { transform: translateY(-14px); opacity: 1; }
}
```

### Pulsing Dots

Calmer than bouncing dots — good inside buttons while a form submits.

Markup

```html
<div class="fh-dots-pulse" role="status" aria-label="Loading">
  <span></span><span></span><span></span>
</div>
```

CSS

```css
.fh-dots-pulse {
  display: flex;
  gap: 8px;
}
.fh-dots-pulse span {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #8b5cf6;
  animation: fh-dotPulse 1s ease-in-out infinite;
}
.fh-dots-pulse span:nth-child(2) { animation-delay: 0.2s; }
.fh-dots-pulse span:nth-child(3) { animation-delay: 0.4s; }
@keyframes fh-dotPulse {
  0%, 100% { transform: scale(0.6); opacity: 0.4; }
  50%      { transform: scale(1);   opacity: 1; }
}
```

### Indeterminate Bar

For when you can’t know the progress percentage. A gradient segment sweeps the track endlessly.

Markup

```html
<div class="fh-bar-loader" role="progressbar" aria-label="Loading"></div>
```

CSS

```css
.fh-bar-loader {
  width: 160px;
  height: 6px;
  border-radius: 3px;
  background: rgba(139, 92, 246, 0.15);
  overflow: hidden;
  position: relative;
}
.fh-bar-loader::before {
  content: '';
  position: absolute;
  top: 0; left: 0;
  height: 100%;
  width: 40%;
  border-radius: 3px;
  background: linear-gradient(90deg, #8b5cf6, #06b6d4);
  animation: fh-barSlide 1.2s ease-in-out infinite;
}
@keyframes fh-barSlide {
  0%   { left: -40%; }
  100% { left: 100%; }
}
```

### Skeleton Shimmer

Skeleton screens make loads feel faster than spinners because they preview the page structure. Size each bar like the content it replaces.

Markup

```html
<div class="fh-skeleton" style="width: 180px;"></div>
<div class="fh-skeleton" style="width: 120px;"></div>
```

CSS

```css
.fh-skeleton {
  width: 180px;
  height: 16px;
  border-radius: 6px;
  background: linear-gradient(90deg,
    rgba(148, 163, 184, 0.12) 25%,
    rgba(148, 163, 184, 0.28) 50%,
    rgba(148, 163, 184, 0.12) 75%);
  background-size: 200% 100%;
  animation: fh-shimmer 1.4s linear infinite;
}
@keyframes fh-shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}
```

### Ripple

Two expanding rings offset by half the duration. Feels organic, like drops hitting water.

Markup

```html
<div class="fh-ripple" role="status" aria-label="Loading"></div>
```

CSS

```css
.fh-ripple {
  position: relative;
  width: 56px;
  height: 56px;
}
.fh-ripple::before,
.fh-ripple::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 3px solid #06b6d4;
  border-radius: 50%;
  animation: fh-rippleWave 1.6s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.fh-ripple::after {
  animation-delay: 0.8s;
}
@keyframes fh-rippleWave {
  from { transform: scale(0.1); opacity: 1; }
  to   { transform: scale(1);   opacity: 0; }
}
```

### Orbit

A satellite circling a dashed orbit. The dot is a pseudo-element, so it is still just one div.

Markup

```html
<div class="fh-orbit" role="status" aria-label="Loading"></div>
```

CSS

```css
.fh-orbit {
  position: relative;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 2px dashed rgba(139, 92, 246, 0.35);
  animation: fh-spinOrbit 2.4s linear infinite;
}
.fh-orbit::before {
  content: '';
  position: absolute;
  top: -7px;
  left: 50%;
  width: 12px;
  height: 12px;
  margin-left: -6px;
  border-radius: 50%;
  background: #06b6d4;
}
@keyframes fh-spinOrbit {
  to { transform: rotate(360deg); }
}
```

## Choosing the right loader for the wait

The type of loader should match how long the wait will be and whether you can measure it. For waits under about a second, showing nothing at all is often best — a spinner that flashes for 300ms reads as a glitch rather than progress.

For short indeterminate waits, a **spinner** or **bouncing dots** works. For anything you can measure, use a **progress bar**: knowing that a task is 70% done is far less stressful than watching an endless spin. And when you're loading content into a known layout, a **skeleton screen** beats both.

## Why skeleton screens feel faster

Skeletons outperform spinners for the same wait because they set expectations. A spinner says "something is happening somewhere"; a skeleton shows the shape of what's arriving, so the eye already knows where the headline and the image will land. When the content appears, it slots into place instead of replacing an unrelated animation.

The rule that makes them work: size each placeholder bar like the content it stands in for. Skeletons that don't match the final layout cause a jump when the real content loads — which is exactly the layout shift you were trying to avoid.

## Accessibility: loaders need to be announced

A purely visual loader is invisible to a screen reader user, who gets silence and no indication anything is happening. Give the element `role="status"` and an `aria-label` such as "Loading results", and assistive technology will announce it politely without interrupting.

Also respect `prefers-reduced-motion`. Continuously spinning elements can trigger discomfort for people with vestibular disorders — a static or fading indicator is a reasonable substitute, and the reduced-motion snippet in our [how-to guide](https://frontendhelpers.com/css-animations#guide-howto) handles this globally.

## Keeping loaders cheap

Every loader here animates only `transform`, `opacity` or `background-position`, which browsers composite on the GPU. That matters more than usual for loaders: they run precisely when the main thread is busy fetching and parsing, so an animation that requires layout work will stutter exactly when it's most visible.

## Frequently asked questions

### When should I use a skeleton screen instead of a spinner?

Use a skeleton whenever you are loading content into a layout you already know — lists, cards, articles. It sets expectations about what is arriving and reduces perceived wait. Use a spinner for short indeterminate waits where the resulting layout is unknown.

### Do CSS loading animations need JavaScript?

No. Every loader here is pure CSS and animates as soon as it is in the DOM. JavaScript is only needed to add or remove the element when your request starts and finishes.

### How do I make a loading spinner accessible?

Give it role="status" and an aria-label such as "Loading results" so screen readers announce it. Also honour prefers-reduced-motion, since continuous spinning can cause discomfort for people with vestibular disorders.

## Related categories

- [CSS Loading Spinners: Light, Smooth and Accessible](https://frontendhelpers.com/css-loading-spinner): Build CSS loading spinners from one element, a conic gradient or three dots. Size them in em, delay them 300ms and make them accessible in buttons.
 
- [Skeleton Loading Screens in CSS: Shimmer Done Right](https://frontendhelpers.com/css-skeleton-loader): Build CSS skeleton screens that match your layout, shimmer on the compositor, respect reduced motion and announce loading to screen readers.
 
- [UI Feedback](https://frontendhelpers.com/css-ui-feedback-animations): Toasts, modals, tooltips and progress bars
 
- [Entrances](https://frontendhelpers.com/css-entrance-animations): Fades and slides for content arriving on screen
 
- [Images & Carousels](https://frontendhelpers.com/css-image-animations): Hover zooms, Ken Burns and blur-up reveals
