SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/loaders

8 CSS Loaders

7 Pure CSS1 with JSPublished 2026-10-03Free · no sign-up

Loading animations for every kind of wait: a spinner for short pauses, a skeleton for content that is on its way, a progress bar for page loads and a progress ring when you know the percentage. Each one tells screen readers that something is loading, and slows down or stops for visitors who prefer reduced motion.

01. Ring Spinner

Pure CSS

The classic loading ring: a faint full circle with one bright arc spinning around it. It is a single element with role=status and a visually hidden label, so screen readers announce 'Loading' instead of nothing. Change the size with one variable.

View code
<div class="ttl-ring" role="status"><span class="ttl-sr">Loading</span></div>
.ttl-ring {
  --size: 44px;
  width: var(--size);
  height: var(--size);
  border: 4px solid rgba(125, 163, 255, .2);
  border-top-color: #7DA3FF;
  border-radius: 50%;
  animation: ttl-spin .8s linear infinite;
}
.ttl-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
@keyframes ttl-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .ttl-ring { animation-duration: 2.4s; } }

02. Bouncing Dots

Pure CSS

Three dots that rise and fall one after another, the loader people read as 'someone is typing'. Each dot runs the same keyframes with a staggered delay, and the bounce uses transform so it never triggers layout. Fits inside chat bubbles and buttons.

View code
<div class="ttl-dots" role="status" aria-label="Loading">
  <span></span><span></span><span></span>
</div>
.ttl-dots { display: inline-flex; gap: 8px; }
.ttl-dots span {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #7DA3FF;
  animation: ttl-bounce 1s ease-in-out infinite;
}
.ttl-dots span:nth-child(2) { animation-delay: .15s; }
.ttl-dots span:nth-child(3) { animation-delay: .3s; }
@keyframes ttl-bounce {
  0%, 80%, 100% { transform: translateY(0); opacity: .5; }
  40% { transform: translateY(-12px); opacity: 1; }
}
@keyframes ttl-fade { 50% { opacity: .3; } }
@media (prefers-reduced-motion: reduce) {
  .ttl-dots span { animation: ttl-fade 1.4s ease-in-out infinite; }
}

03. Skeleton Loading Card

Pure CSS

A placeholder that has the shape of the content that is coming: avatar, two text lines and a block, with a soft shimmer passing over them. Skeletons make waiting feel shorter than a spinner and stop the page jumping when the real content arrives, as long as they match its size.

View code
<div class="ttl-skel" role="status" aria-label="Loading content">
  <div class="ttl-skel-row">
    <span class="ttl-skel-av"></span>
    <div class="ttl-skel-lines"><span></span><span></span></div>
  </div>
  <span class="ttl-skel-block"></span>
</div>
.ttl-skel {
  width: 300px;
  max-width: 100%;
  box-sizing: border-box;
  padding: 18px;
  border: 1px solid rgba(255, 255, 255, .08);
  border-radius: 16px;
  background: #121722;
}
.ttl-skel-row { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }
.ttl-skel-lines { flex: 1; display: grid; gap: 8px; }
.ttl-skel span {
  display: block;
  border-radius: 8px;
  background: linear-gradient(90deg, #1b2232 0%, #262f44 40%, #1b2232 80%);
  background-size: 300% 100%;
  animation: ttl-shimmer 1.4s ease-in-out infinite;
}
.ttl-skel .ttl-skel-av { width: 44px; height: 44px; flex: 0 0 44px; border-radius: 50%; }
.ttl-skel-lines span { height: 11px; }
.ttl-skel-lines span:last-child { width: 60%; }
.ttl-skel-block { height: 90px; }
@keyframes ttl-shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
@media (prefers-reduced-motion: reduce) { .ttl-skel span { animation: none; } }

04. Indeterminate Progress Bar

Pure CSS

A thin bar for page or upload loading when you do not know how long it will take. A highlight slides across the track on a loop. It uses the native progress element with no value, which browsers already expose as busy, and only restyles it.

View code
<progress class="ttl-bar" aria-label="Loading"></progress>
.ttl-bar {
  appearance: none;
  -webkit-appearance: none;
  width: 280px;
  height: 6px;
  overflow: hidden;
  border: 0;
  border-radius: 999px;
  background: rgba(125, 163, 255, .15) linear-gradient(90deg, transparent, #7DA3FF, #5B8CFF, transparent) no-repeat;
  background-size: 40% 100%;
  animation: ttl-slide 1.2s ease-in-out infinite;
}
.ttl-bar::-webkit-progress-bar { background: transparent; }
.ttl-bar::-moz-progress-bar { background: transparent; }
@keyframes ttl-slide {
  from { background-position: -40% 0; }
  to { background-position: 140% 0; }
}
@media (prefers-reduced-motion: reduce) { .ttl-bar { animation-duration: 3s; } }

05. Pulse Ring

Pure CSS

A solid dot with rings that grow and fade outward, like a radar ping. It works as a loader, a 'live' indicator or a hotspot marker on a product image. Two pseudo-elements with offset delays make the rings, so the markup is one element.

View code
<span class="ttl-pulse" role="status" aria-label="Live"></span>
.ttl-pulse {
  position: relative;
  display: inline-block;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #5B8CFF;
}
.ttl-pulse::before,
.ttl-pulse::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid #7DA3FF;
  animation: ttl-ping 1.8s cubic-bezier(.2,.6,.3,1) infinite;
}
.ttl-pulse::after { animation-delay: .9s; }
@keyframes ttl-ping {
  from { transform: scale(1); opacity: .9; }
  to { transform: scale(3.2); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .ttl-pulse::before, .ttl-pulse::after { animation: none; opacity: 0; } }

06. Equalizer Bars

Pure CSS

Five bars that rise and fall like an audio equalizer. It suits media players, voice notes and anything to do with sound. Each bar scales from the bottom with transform-origin, so the motion stays smooth and the overall size never changes.

View code
<div class="ttl-eq" role="status" aria-label="Loading">
  <span></span><span></span><span></span><span></span><span></span>
</div>
.ttl-eq { display: inline-flex; align-items: flex-end; gap: 5px; height: 40px; }
.ttl-eq span {
  width: 6px;
  height: 100%;
  border-radius: 3px;
  background: linear-gradient(#7DA3FF, #5B8CFF);
  transform-origin: bottom;
  animation: ttl-eq 1s ease-in-out infinite;
}
.ttl-eq span:nth-child(2) { animation-delay: -.8s; }
.ttl-eq span:nth-child(3) { animation-delay: -.6s; }
.ttl-eq span:nth-child(4) { animation-delay: -.4s; }
.ttl-eq span:nth-child(5) { animation-delay: -.2s; }
@keyframes ttl-eq {
  0%, 100% { transform: scaleY(.25); }
  50% { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) { .ttl-eq span { animation-duration: 3s; } }

07. Progress Ring with Percentage

CSS + JS

A circular progress indicator with the percentage in the middle, for uploads and multi-step tasks where you know how far along you are. The ring is an SVG circle whose stroke-dashoffset follows the value, and the element exposes role=progressbar with aria-valuenow so assistive tech reads the number.

View code
<div class="ttl-pr" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Upload progress">
  <svg viewBox="0 0 120 120" aria-hidden="true">
    <circle class="ttl-pr-track" cx="60" cy="60" r="52"/>
    <circle class="ttl-pr-fill" cx="60" cy="60" r="52"/>
  </svg>
  <span class="ttl-pr-num">0%</span>
</div>
.ttl-pr { position: relative; width: 120px; height: 120px; }
.ttl-pr svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ttl-pr circle { fill: none; stroke-width: 10; }
.ttl-pr-track { stroke: rgba(125, 163, 255, .15); }
.ttl-pr-fill {
  stroke: #7DA3FF;
  stroke-linecap: round;
  stroke-dasharray: 326.73;
  stroke-dashoffset: 326.73;
  transition: stroke-dashoffset .3s ease;
}
.ttl-pr-num {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #F5F7FA;
  font: 800 24px Inter, system-ui, sans-serif;
}
document.querySelectorAll('.ttl-pr').forEach(function (el) {
  var fill = el.querySelector('.ttl-pr-fill');
  var num = el.querySelector('.ttl-pr-num');
  var full = 2 * Math.PI * 52;
  var value = 0;
  function set(v) {
    fill.style.strokeDashoffset = full * (1 - v / 100);
    num.textContent = v + '%';
    el.setAttribute('aria-valuenow', v);
  }
  setInterval(function () {
    value = value >= 100 ? 0 : Math.min(100, value + 7);
    set(value);
  }, 400);
});

08. Orbit Loader

Pure CSS

Two small planets orbiting a centre point at different speeds and in opposite directions. It is a decorative loader for splash screens and empty states where a little personality fits. Each orbit is a rotating ring with a dot placed on its edge.

View code
<div class="ttl-orbit" role="status" aria-label="Loading">
  <span class="ttl-orbit-core"></span>
  <span class="ttl-orbit-a"></span>
  <span class="ttl-orbit-b"></span>
</div>
.ttl-orbit { position: relative; width: 80px; height: 80px; }
.ttl-orbit > span { position: absolute; border-radius: 50%; }
.ttl-orbit-core {
  inset: 32px;
  background: linear-gradient(135deg, #7DA3FF, #5B8CFF);
  box-shadow: 0 0 18px rgba(125, 163, 255, .7);
}
.ttl-orbit-a, .ttl-orbit-b { border: 1px dashed rgba(125, 163, 255, .25); }
.ttl-orbit-a { inset: 14px; animation: ttl-spin 1.6s linear infinite; }
.ttl-orbit-b { inset: 0; animation: ttl-spin 2.6s linear infinite reverse; }
.ttl-orbit-a::after, .ttl-orbit-b::after {
  content: "";
  position: absolute;
  top: -5px; left: 50%;
  width: 10px; height: 10px;
  margin-left: -5px;
  border-radius: 50%;
  background: #8B5CF6;
}
.ttl-orbit-b::after { background: #22d3ee; width: 8px; height: 8px; top: -4px; margin-left: -4px; }
@keyframes ttl-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .ttl-orbit-a, .ttl-orbit-b { animation-duration: 6s; } }

About these CSS Loaders

Which loader to use when

Under about a second, show nothing; a flash of spinner feels slower than no feedback. For one to three seconds, a small spinner or dots is enough. For content that is loading, such as a feed or a product grid, use a skeleton in the shape of that content. When you know how far along a task is, show real progress with a bar or ring.

Making loaders accessible

A spinning circle means nothing to a screen reader unless you say what it is. Give the loader role="status" and a label such as "Loading", or use a native <progress> element. For known progress, role="progressbar" with aria-valuenow lets assistive technology read the number.

Keeping animations smooth

Every loader here animates only transform and opacity, which the browser can run on the GPU without recalculating the page layout. That keeps them smooth even while the main thread is busy loading the thing you are waiting for. Each one also has a prefers-reduced-motion rule that slows or removes the motion.

How to use these demos

Each loader is scoped with a .ttl- class prefix. Show it while your request is pending and remove it, or swap it for the content, when the request finishes. Open any loader in the editor to change its size and colours with a live preview.

Frequently asked questions

Are pure CSS loaders better than GIFs?

Usually, yes. They are a few hundred bytes instead of kilobytes, stay sharp at any size and screen density, and you can recolour them with one line of CSS.

Do these loaders affect page speed?

Barely. They are tiny and animate only transform and opacity, so they do not cause layout work. A skeleton that matches the final content size also prevents layout shift when the content arrives.

Can I change the colours?

Yes. Each loader uses two or three hex colours. Replace them with your brand colours, or open the loader in the editor and change them with a live preview.