GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Loading Spinners: Copy-Paste, Single Element

A css loading spinner is a small shape — a ring, three dots, a bar — that loops in place to tell someone a page or action is still working before there is any

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine below run from the plainest shape to the most produced. 01 through 04 lean on nothing more than a border, a transform, or a background — the shapes most sites already reach for first. 05 through 07 pull in newer CSS: a conic-gradient, an SVG stroke, and an @property-registered custom value that can genuinely tween through a number. 08 and 09 are the idea-form entries, moving the spinner into a real button and into a soft AI-thinking orb. Watch the grid above cycle through all nine on its own — every css loading spinner animation below runs inside a fixed, non-scrollable iframe with nothing to click, so it just loops.

01Basic ring spinner

What it does

A single circular border spins in place with only its top edge colored differently, and that color difference is the entire animation — no absolute positioning, no pseudo-elements underneath it.

Basic ring spinner
Open in a new tab

01 · Basic ring spinner — Measured area 1.248% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

1초 회전border 4px단색

When to use it

Reach for it as the default full-page loading state or the waiting screen right after a button click, anywhere a spinner just needs to say the page is still working.

.spinner {
  width: clamp(48px, 14vw, 72px);
  height: clamp(48px, 14vw, 72px);
  border-radius: 50%;
  border: 6px solid rgba($subject-cream, .22);
  border-top-color: $color;
  animation: spin $duration $easing infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

02Dual counter-rotating ring

What you see

An outer ring and an inner ring spin in opposite directions at two different speeds, so the shape reads as layered rather than flat even though every piece is still a plain circular border.

Dual counter-rotating ring
Open in a new tab

02 · Dual counter-rotating ring — Measured area 1.833% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

역방향1.2초 · 0.9초두 겹

Where it fits

It suits modal loading states and dashboard cards pulling in fresh data, where one flat ring feels too thin against a card that already has visual depth.

.spinner__ring--outer {
  border-top-color: $color;
  border-bottom-color: $color;
  animation: spin $duration-outer linear infinite;
}
.spinner__ring--inner {
  inset: 13px;
  border-left-color: $subject-blue;
  border-right-color: $subject-blue;
  animation: spin-rev $duration-inner linear infinite;
}

@keyframes spin-rev {
  to { transform: rotate(-360deg); }
}

033-dot bounce

The motion

Three dots bounce upward one after another on a fixed delay, so the motion reads as a small wave passing through the row rather than three dots blinking at random.

3-dot bounce
Open in a new tab

03 · 3-dot bounce — Measured area 1.1% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

지연 0.16초점 3개translateY

Good places for it

It is the shape people already read as a chat window waiting on a reply, and it fits a search box's autocomplete wait just as naturally.

.dots span {
  width: clamp(12px, 4vw, 18px);
  height: clamp(12px, 4vw, 18px);
  border-radius: 50%;
  background: $color;
  animation: bounce $duration ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: $delay; }
.dots span:nth-child(3) { animation-delay: $delay * 2; }

@keyframes bounce {
  0%, 80%, 100% { transform: translateY(0); opacity: .5; }
  40%           { transform: translateY(-14px); opacity: 1; }
}

04Indeterminate bar

What it does

A short bar sweeps from one side of a thin track to the other, scaling itself down at both ends of the sweep so it never looks like it slams flat into the track's edge.

Indeterminate bar
Open in a new tab

04 · Indeterminate bar — Measured area 1.164% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

가로 100%1.5초ease-in-out

When to use it

Use it before a real percentage exists yet — the moment right before a file upload starts, or a slim progress line pinned across the top of a page.

.bar__fill {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: $color;
  transform-origin: left center;
  animation: sweep $duration $easing infinite;
}

@keyframes sweep {
  0%   { transform: translateX(-100%) scaleX(.3); }
  50%  { transform: translateX(20%) scaleX(.6); }
  100% { transform: translateX(100%) scaleX(.3); }
}

05Conic-gradient sweep

What you see

A single conic-gradient spins on its own, with a radial-gradient mask cut into the middle so the filled circle reads as a thin ring without any separate border or pseudo-element drawing it.

Conic-gradient sweep
Open in a new tab

05 · Conic-gradient sweep — Measured area 1.473% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

conic-gradientmask 처리그라디언트

Where it fits

Conic gradients now render across every modern browser, which makes this the shape to reach for on a loading screen that has to carry an exact brand color, like a checkout wait.

.spinner {
  width: clamp(56px, 16vw, 84px);
  height: clamp(56px, 16vw, 84px);
  border-radius: 50%;
  background: conic-gradient($color 0deg 270deg, rgba($subject-cream, .18) 270deg 360deg);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
          mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
  animation: spin $duration $easing infinite;
}

06SVG stroke-dashoffset circle

The motion

An SVG circle animates only its stroke-dashoffset, so the colored arc appears to chase itself around the track while the whole element also spins slowly underneath it.

SVG stroke-dashoffset circle
Open in a new tab

06 · SVG stroke-dashoffset circle — Measured area 0.706% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

stroke-dasharraySVG1.4초

Good places for it

It suits a wait icon that has no real percentage behind it yet, such as an app's splash screen before the first network call resolves.

.spinner__arc {
  stroke: $color;
  stroke-linecap: round;
  stroke-dasharray: 113;
  stroke-dashoffset: 90;
  animation: dash $duration-dash ease-in-out infinite;
}

@keyframes dash {
  0%   { stroke-dashoffset: 100; }
  50%  { stroke-dashoffset: 20; }
  100% { stroke-dashoffset: 100; }
}

07@property percent ring

What it does

A custom property named --percent is registered with @property as a plain number, so animating it from 0 to 100 tweens smoothly through every step instead of snapping straight from empty to full.

@property percent ring
Open in a new tab

07 · @property percent ring — Measured area 1.508% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

@property0~100%conic-gradient

When to use it

Support for @property only reached Chrome 85, Safari 16.4, and Firefox 128, so ship a static fallback ring for anything older, and reach for this version wherever the number behind the ring is real, like upload progress or an onboarding completion meter.

@property --percent {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

.ring {
  --percent: 0;
  background: conic-gradient($color calc(var(--percent) * 1%), rgba($subject-ink, .18) 0);
  animation: fill $duration $easing infinite;
}

@keyframes fill {
  0%   { --percent: 0; }
  50%  { --percent: 100; }
  100% { --percent: 0; }
}

08Inline button loading state

What you see

Clicking the button does not swap it for a new element — the label fades and slides down while a small ring fades in on the same grid cell, and the button's own width stays fixed the whole time so nothing shifts around it.

Inline button loading state
Open in a new tab

08 · Inline button loading state — Measured area 0.899% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

버튼 폭 고정0.9초disabled 처리

Where it fits

Wire this into save buttons and checkout buttons, anywhere someone needs to see their own click acknowledged without the page jumping under the cursor.

.btn {
  &__label, &__spin { grid-area: 1 / 1; transition: opacity $duration, transform $duration; }
  &__spin {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    border: 3px solid rgba($subject-cream, .35);
    border-top-color: $subject-cream;
    opacity: 0;
    transform: scale(.6);
    animation: spin $spin linear infinite;
  }
  &.is-loading &__label { opacity: 0; transform: translateY(6px); }
  &.is-loading &__spin  { opacity: 1; transform: scale(1); }
}

09AI-app style pulse orb

The motion

A blurred glow sits behind a soft gradient core, and both grow and shrink on a slow two-second pulse, timed just out of phase so the whole shape reads as breathing rather than mechanically ticking.

AI-app style pulse orb
Open in a new tab

09 · AI-app style pulse orb — Measured area 8.89% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

blur 그라디언트2초 펄스::before 글로우

Good places for it

Reach for it on an AI chatbot's response wait or a voice assistant's listening indicator, anywhere a plain ring would look too literal for what is really a black box thinking.

.orb::before {
  background: radial-gradient(circle, rgba($color, .55), transparent 70%);
  filter: blur(6px);
  animation: glow $duration $easing infinite;
}
.orb::after {
  background: radial-gradient(circle at 35% 35%, $subject-cream, $color 75%);
  animation: core $duration $easing infinite;
}

@keyframes core {
  0%, 100% { transform: scale(.88); opacity: .88; }
  50%      { transform: scale(1.08); opacity: 1; }
}

Where it breaks — the traps

Two silent support failures

Two of these nine lean on browser support in a way the rest don't, and both fail silently rather than throwing an error. Skip the @property block above item 07 and the browser treats --percent as an opaque string instead of a registered number, so the ring stops tweening through the fill entirely and just jumps from empty to full partway through the loop instead of counting up. Item 05 has a quieter version of the same problem: drop the -webkit-mask line and keep only the unprefixed mask, and Safari older than 15.4 paints the full conic-gradient disc with no hole cut into the middle, so what should read as a ring shows up as a solid wedge of color instead (MDN's mask compatibility table lists 15.4 as the first unprefixed version). Both fixes already sit inside the demos above, and the whole set — every fallback included — is worth grabbing from the archive rather than retyping nine keyframe blocks by hand; unzip it with qda5pgmf typed exactly as it appears on this line, no spaces added. When the wait is long enough to sketch the shape of what's coming, the CSS skeleton loading screens take over, and when the number itself has to show, the circle gauge progress bars pick it up.

Accessibility

Reduced motion

Every one of the nine respects prefers-reduced-motion, but each drops a different amount of motion rather than all going fully still. 01 through 04, 06, and 09 stop spinning, bouncing, or pulsing outright and settle on a static dimmed shape, since a loading indicator that no longer moves still communicates "wait" through its color and position alone. 05 simply freezes its rotation in place. 07 holds --percent at a fixed 62 rather than looping through zero, so the ring shows a plausible snapshot instead of an empty or full circle that might read as done or broken. 08's button keeps its ring visible but static the instant a real click starts loading, so the disabled state still reads clearly without any spin at all. MDN documents prefers-reduced-motion and the operating-system settings that flip it on.

@media (prefers-reduced-motion: reduce) {
  .spinner,
  .spinner__ring--outer,
  .spinner__ring--inner,
  .dots span,
  .bar__fill,
  .spinner__arc,
  .ring,
  .btn__spin,
  .orb::before,
  .orb::after {
    animation: none !important;
  }
}

More css loading spinner animation variants and the rest of the CSS collection live under the CSS category hub, and what this site actually sells — and doesn't — is explained on the about page.

FAQ

Will these still work as a css loading spinner full screen overlay?

Yes — every shape here is a single element sized with clamp(), so wrapping it in a fixed, full-viewport container with a dim backdrop turns any of the nine into a full-screen overlay without changing the spinner's own markup or animation at all. Item 01's basic ring and item 05's conic sweep are the two that read cleanest at a larger size, since both are drawn from pure shape rather than several small parts.

Can I put a css loading spinner with text next to it?

Yes, and it is usually better for people using screen readers than the spinner alone. Add a sibling span with the wait message, keep the spinner itself marked aria-hidden the way each demo above already does, and use the parent's role="status" or aria-label attribute to carry the real label instead of hiding it inside decorative markup.

Do these need a build tool, a framework, or a css loading spinner generator?

No — every pattern compiles from a handful of SCSS variables into plain CSS, and nothing here depends on a specific framework or a generator to produce it. Paste the vanilla SCSS into any SCSS playground to preview one pattern on its own, and the zip's react/ folder wraps each of the nine in its own component for projects that would rather import than copy classes by hand. Once a spinner like 08's finishes and the news is good, the one-line announcement it hands off to is the CSS toast notifications; when the wait covers a whole screen instead of one button, the page loading animations carry that weight.

Enter the archive password

The password is inside this article. You will find it as you read.