How do you make a loading spinner or skeleton screen in pure CSS?

A loader tells people that something is happening while content arrives. Most are pure CSS: a border or SVG arc rotated with @keyframes, dots scaled one after another with animation-delay, or skeleton screens, grey placeholders shaped like the final content and crossed by a moving shimmer gradient.

How to choose, performance and accessibility
  • Prefer a skeleton screen such as Card Skeleton when you know the layout of what is loading, a Classic Spinner or Bouncing Dots for short and unknown waits, and a Progress Bar when progress can be measured.
  • Add role="status" with a text alternative so screen readers announce the wait, animate only transform and opacity, and slow things down for prefers-reduced-motion.
  • Classic Spinner, SVG Morphing Loaders, Bouncing Dots and Card Skeleton are free with their full code; the others come with Premium.

Classic Spinner

Spinning circle, simple and effective.

CSS Minimal
Effect page →
View source code
HTML
<div class="demo-preview">
  <div class="loader-spinner"></div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.loader-spinner {
  width: 50px;
  height: 50px;
  border: 4px solid rgba(99, 102, 241, 0.2);
  border-top-color: #6366f1;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
JavaScript
/* Pure CSS — no JavaScript required */
Blob
Drop
Star

SVG Morphing Loaders

Three organic indicators whose shapes morph in a smooth loop (native SVG path interpolation). Zero JavaScript, static fallback under prefers-reduced-motion.

💡 Perfect for: loading states, AI waits, splash screens, processing buttons.

SVG SMIL Zero-JS
Effect page →
View source code
HTML
<div class="demo-preview">
  <div class="sml-row">
    <div class="sml-item"><svg class="sml-anim" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
        <path fill="url(#smlGradA)">
          <animate attributeName="d" dur="2.2s" repeatCount="indefinite" calcMode="spline"
            keyTimes="0;.5;1" keySplines=".4 0 .2 1;.4 0 .2 1"
            values="M27 5 C41 5 49 13 49 27 C49 41 41 49 27 49 C13 49 5 41 5 27 C5 13 13 5 27 5 Z;M27 8 C44 3 51 16 46 29 C42 44 30 52 18 46 C6 41 2 26 9 16 C14 8 20 10 27 8 Z;M27 5 C41 5 49 13 49 27 C49 41 41 49 27 49 C13 49 5 41 5 27 C5 13 13 5 27 5 Z"/>
        </path>
      </svg>
      <svg class="sml-static" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true"><path fill="url(#smlGradA)" d="M27 5 C41 5 49 13 49 27 C49 41 41 49 27 49 C13 49 5 41 5 27 C5 13 13 5 27 5 Z"/></svg><span class="sml-label">Blob</span></div>
    <div class="sml-item"><svg class="sml-anim" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
        <path fill="#5a9bff">
          <animate attributeName="d" dur="1.6s" repeatCount="indefinite" calcMode="spline"
            keyTimes="0;.5;1" keySplines=".4 0 .2 1;.4 0 .2 1"
            values="M27 6 C34 16 44 24 44 33 C44 43 36 49 27 49 C18 49 10 43 10 33 C10 24 20 16 27 6 Z;M27 10 C38 10 46 18 46 28 C46 39 38 46 27 46 C16 46 8 39 8 28 C8 18 16 10 27 10 Z;M27 6 C34 16 44 24 44 33 C44 43 36 49 27 49 C18 49 10 43 10 33 C10 24 20 16 27 6 Z"/>
        </path>
      </svg>
      <svg class="sml-static" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true"><path fill="#5a9bff" d="M27 6 C34 16 44 24 44 33 C44 43 36 49 27 49 C18 49 10 43 10 33 C10 24 20 16 27 6 Z"/></svg><span class="sml-label">Drop</span></div>
    <div class="sml-item"><svg class="sml-anim" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
        <path fill="#ffd166">
          <animate attributeName="d" dur="1.9s" repeatCount="indefinite" calcMode="spline"
            keyTimes="0;.5;1" keySplines=".4 0 .2 1;.4 0 .2 1"
            values="M27 6 L33 20 L48 21 L37 31 L41 46 L27 38 L13 46 L17 31 L6 21 L21 20 Z;M27 12 L31 22 L42 23 L34 30 L36 41 L27 36 L18 41 L20 30 L12 23 L23 22 Z;M27 6 L33 20 L48 21 L37 31 L41 46 L27 38 L13 46 L17 31 L6 21 L21 20 Z"/>
        </path>
      </svg>
      <svg class="sml-static" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true"><path fill="#ffd166" d="M27 6 L33 20 L48 21 L37 31 L41 46 L27 38 L13 46 L17 31 L6 21 L21 20 Z"/></svg><span class="sml-label">Star</span></div>
    <svg width="0" height="0" aria-hidden="true"><defs><linearGradient id="smlGradA" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7c3aed"/><stop offset="1" stop-color="#db2777"/></linearGradient></defs></svg>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.sml-row {
  display: flex;
  gap: 34px;
  align-items: center;
  justify-content: center
}

.sml-item {
  display: grid;
  place-items: center;
  gap: 8px
}

.sml-item svg {
  display: block
}

.sml-label {
  font-size: .62rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #8f8fb0
}

.sml-item .sml-static {
  display: none
}

@media (prefers-reduced-motion: reduce) {
  .sml-item .sml-anim {
    display: none
  }

  .sml-item .sml-static {
    display: block
  }
}
JavaScript
/* Pure CSS — no JavaScript required */

Bouncing Dots

Three dots bouncing in cascade.

CSS Playful
Effect page →
View source code
HTML
<div class="demo-preview">
  <div class="loader-dots">
    <span></span>
    <span></span>
    <span></span>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.loader-dots {
  display: flex;
  gap: 8px;
}

.loader-dots span {
  width: 12px;
  height: 12px;
  background: #6366f1;
  border-radius: 50%;
  animation: dotBounce 1.4s ease-in-out infinite;
}

.loader-dots span:nth-child(1) {
  animation-delay: -0.32s;
}

.loader-dots span:nth-child(2) {
  animation-delay: -0.16s;
}

@keyframes dotBounce {

  0%,
  80%,
  100% {
    transform: scale(0);
  }

  40% {
    transform: scale(1);
  }
}
JavaScript
/* Pure CSS — no JavaScript required */

Pulse Ring

Rings pulsing outward.

CSS Radar

Progress Bar

Indeterminate progress bar.

CSS Linear

Cube Flip

3D cube flipping over.

CSS 3D

Wave Bars

Audio wave-shaped bars.

CSS Audio

Orbit

Dot orbiting around a center.

CSS Circular

Skeleton Screen

Shimmer placeholder for content.

CSS Content

DNA Helix

Double helix in 3D rotation.

CSS 3D

Cube Grid

3x3 grid of cubes in sequence.

CSS Grid

Morphing Shape

Shape morphing while rotating.

CSS Transform

Circular Progress

SVG circle filling progressively.

SVG Progress

Bouncing Squares

Bouncing squares in cascade.

CSS Playful

Text Loading

Text with animated dots.

CSS Text

Gradient Ring

Rotating gradient ring.

CSS Gradient

Heartbeat

Heartbeat pulse effect.

CSS Pulse

Infinity Loop

Animated infinity loop.

CSS Loop

Glitch Text

Retro glitch effect on text.

CSS Glitch

Card Skeleton

Classic card placeholder with avatar, title and text lines animated by a sliding shimmer.

CSS Shimmer Card
Effect page →
View source code
HTML
<div class="demo-preview">
  <div class="card-skel">
    <div class="card-skel-header">
      <div class="skel-bone-circle card-skel-avatar"></div>
      <div style="flex:1">
        <div class="skel-bone card-skel-title"></div>
        <div class="skel-bone card-skel-subtitle"></div>
      </div>
    </div>
    <div class="skel-bone card-skel-img"></div>
    <div class="skel-bone card-skel-line"></div>
    <div class="skel-bone card-skel-line"></div>
    <div class="skel-bone card-skel-line"></div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.skel-bone {
  background: rgba(255, 255, 255, 0.08);
  border-radius: 6px;
  position: relative;
  overflow: hidden;
}

.skel-bone-circle {
  background: rgba(255, 255, 255, 0.08);
  border-radius: 50%;
  position: relative;
  overflow: hidden;
}

.skel-bone::after,
.skel-bone-circle::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
      transparent 0%,
      rgba(255, 255, 255, 0.1) 50%,
      transparent 100%);
  background-size: 200% 100%;
  animation: shimmerSlide 1.8s ease-in-out infinite;
}

.card-skel {
  width: 280px;
  padding: 20px;
  background: rgba(255, 255, 255, 0.03);
  border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.06);
}

.card-skel-header {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 20px;
}

.card-skel-avatar {
  width: 48px;
  height: 48px;
  flex-shrink: 0;
}

.card-skel-title {
  height: 14px;
  width: 65%;
  margin-bottom: 8px;
}

.card-skel-subtitle {
  height: 10px;
  width: 45%;
}

.card-skel-img {
  height: 120px;
  width: 100%;
  margin-bottom: 16px;
  border-radius: 10px;
}

.card-skel-line {
  height: 10px;
  width: 100%;
  margin-bottom: 10px;
}

.card-skel-line:nth-child(2) {
  width: 92%;
}

.card-skel-line:last-child {
  width: 60%;
  margin-bottom: 0;
}

@keyframes shimmerSlide {
  0% {
    background-position: -200% 0;
  }

  100% {
    background-position: 200% 0;
  }
}
JavaScript
/* Pure CSS — no JavaScript required */

List Skeleton

List of elements with avatars and text lines in progressive loading.

CSS Shimmer Liste

Image Grid Skeleton

2x2 grid of image placeholders with staggered shimmer for a cascade effect.

CSS Grid Images

Profile Skeleton

Profile page loading: large avatar, name, bio and stats row.

CSS Shimmer Profil

Article Skeleton

Blog article skeleton: hero image, title, paragraphs and author info.

CSS Shimmer Blog

Dashboard Skeleton

Mini dashboard with stat cards, chart area and table rows loading.

CSS Shimmer Dashboard

Pulse Loading

Same card structure but with a pulse/breathing animation instead of classic shimmer.

CSS Pulse Card

Wave Shimmer

Undulating shimmer that flows like water, with a fluid and organic motion.

CSS Wave Organique

Rainbow Shimmer

Skeleton with a colorful rainbow gradient shimmer sweeping across elements.

CSS Rainbow Gradient

Skeleton to Content

Click to toggle between skeleton state and real content with a smooth transition.

CSS JS Transition

Frequently Asked Questions

How to create a CSS spinner without JavaScript? +

Use a div with border-radius:50%, a transparent border except on one side, and animation: spin 1s linear infinite. The @keyframes spin animation rotates from 0 to 360deg. It's the lightest possible loader.

What is the best loader for a website? +

Use a skeleton screen for long content (cards, lists) and a simple spinner for short actions (submit button). The skeleton gives a sense of speed because it shows the page structure.

How to animate a skeleton screen in CSS? +

Create gray rectangles with border-radius that mimic the content structure. Add an animated gradient (shimmer) with background: linear-gradient and animation on background-position.

Are CSS loaders performant? +

Yes. CSS animations using transform and opacity are processed by the GPU. A CSS spinner uses fewer resources than a GIF or animated SVG. Avoid animating box-shadow or width which trigger repaints.

How to center a loader on the page? +

Use display:flex, align-items:center and justify-content:center on the container. For a fullscreen overlay, add position:fixed, inset:0, z-index:9999 and a semi-transparent background.