Most loading spinners on the web are just a keyframe animation sitting in the stylesheet. Nobody needs a GIF or an animation library to draw one.

The classic version is a circle with a border, rotated through 360 degrees by a transform. It usually shows up on form submits and API calls, and sometimes as a full-page preloader.

For a sense of how common this is, the 2022 Web Almanac (HTTP Archive) found the animation property on 77% of mobile pages. Rotate animations, which includes anything named spin, were the most popular identifiable type at 13%.

What Are CSS Loaders and How Do They Differ From GIF and Lottie Loaders?

See the Pen
CSS Animated Loader
by Christofer Vilander (@c_vilander)
on CodePen.

A loader written in CSS draws its pending state with keyframes, transforms, borders and gradients. There’s no image request and no library to load.

People use spinner and loading indicator for the same thing, and preloader too. The preloader is really the full-page version, sitting on top of the content until the page is ready.

A GIF is fixed pixels in fixed colors, costs an extra file request and can’t read a theme variable. Lottie is JSON-based animation that needs a player library at runtime, which is worth it for branded motion and hard to justify for a plain ring.

SVG stays sharp at any size and can be animated through SMIL or CSS. The CSS route is one rule set with no file at all, and a custom property recolors it.

You can write a CSS loader in pure CSS, an empty div styled into a ring, a row of dots or a bar. You can also keep the shape in inline SVG markup and let CSS supply only the motion. In both cases a little JavaScript can toggle a class that decides when the loader shows, while CSS still does the drawing and animating.

Tailwind’s documentation puts its animate-spin utility on an inline SVG with a 24 by 24 viewBox. That leaves the shape in the markup and the motion in CSS.

Temani Afif’s css-loaders.com collection builds every loader from a single HTML element. The css-loader package on cdnjs does the same thing with one div and pure CSS.

Which CSS Loader Type Fits Which Wait?

How long the wait runs matters more than how the loader looks. Anything under 1 second doesn’t need a loader at all. From 1 to 10 seconds a spinner or skeleton screen does the job, and once the wait passes 10 seconds a progress bar becomes necessary at the latest.

Where is web design headed next?

Discover the latest web design statistics: industry growth, design trends, technology adoption, and insights defining the future of the web.

Explore the Data →

Those thresholds trace back to Jakob Nielsen’s response-time limits (Nielsen Norman Group, 1993). At 0.1 seconds a response feels instant. 1 second is about as long as the user’s train of thought stays intact, and by 10 seconds focused attention has run out.

Loader typeBest fitProgressMain drawback
SpinnerForm submits, API callsUnknownShows no sign of progress
DotsButtons, inline statusUnknownConfusing on sub-second waits
BarsCompact data fetchesUnknownLooks like a progress bar but is not one
PulsePlaceholders, small status marksUnknownWeak signal on a busy layout
Progress barUploads, downloads, multi-step tasksMeasuredNeeds a real value from a script
Skeleton screenCards, lists, data tablesUnknownMust match the final layout

My rule is simple. If the app can report real progress, use a bar. If it can’t, use a spinner for actions and a skeleton screen for content.

Spinners, dots, bars and pulse loaders

See the Pen
Vivid CSS3 Spinner
by Kevin Jannis (@kevinjannis)
on CodePen.

None of these report progress. They show that something is happening and say nothing about how long it will take.

The spinner is the default for short blocking actions like a form submit or an API call. Dots are light enough for buttons and inline text, but Workday’s Canvas design system advises against them under 1 second, because the user can’t register the switch from indicator to content.

Bars, usually a row of scaling ones, fit tight spaces. The motion looks a lot like measurement though, so keep them away from places where users expect real progress.

Pulse loaders are a fade or scale loop that works as a quiet status mark (Tailwind’s animate-pulse dips opacity to 0.5 and back in a 2-second loop).

Progress bars and skeleton screens

See the Pen
CSS Dash Loader
by Cassidy (@cassidoo)
on CodePen.

A progress bar reports a measured percentage, so it’s called determinate. A spinner only says activity is happening. That makes it indeterminate.

IBM’s Carbon Design System draws the line early. A loading indicator shows no progress, so a process that takes more than a moment or two should get a progress indicator instead. Nielsen’s limits put the same switch at 10 seconds.

The sources disagree on the threshold, not on the principle. Show a bar as soon as the app can report real progress (uploads and downloads, or any multi-step flow) and keep the spinner for waits with no measurable value.

On the CSS side, a progress bar built with CSS is a track plus a fill, and the fill value comes from a script.

Carbon limits skeleton screens to container-based and data-based components, so tiles, structured lists, data tables and cards. It rules them out for modals, toasts, dropdown items and loaders, and says buttons and inputs don’t need one.

In practice, size each placeholder to match the final content and swap it out as its data arrives. On pages that load fast, skip them.

How to Build a CSS Spinner Step by Step

See the Pen
Escalade Loader
by Yoav Kadosh (@ykadosh)
on CodePen.

A CSS spinner is one empty element styled as a ring with a single colored edge. A keyframe animation then rotates it 360 degrees in a loop.

  1. Add one empty element: <div class="loader"></div>
  2. Give it equal width and height, then add border-radius: 50% to make a circle
  3. Add a light border on all sides, then set border-top-color to the accent color so the ring shows one visible arc
  4. Define @keyframes spin with an end state of transform: rotate(360deg)
  5. Apply animation: spin 1s linear infinite to the element
.loader {
width: 40px;
height: 40px;
border: 4px solid #d9d9d9;
border-top-color: #0a84ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}

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

The shorthand packs duration, timing function, delay, iteration count and name into one declaration (MDN Web Docs). The value infinite keeps the loop running.

Linear timing matters. The default function, ease, slows the motion at the start and end of every cycle, so an eased spinner appears to hitch on each turn.

The rotation sits in a CSS keyframes rule with a single end state, since the browser reads the start state from the element. Tailwind CSS v4 declares only a to state for its spin keyframes too.

Tailwind’s spinner utility, animate-spin, ships the same recipe, spin 1s linear infinite.

A dual ring uses the same border trick on ::before and ::after, with the two rotating in opposite directions. Dots are three or more circles sharing one keyframe, offset with animation-delay.

For a gradient arc, a conic-gradient paints a fading arc on one element. A radial-gradient mask or a pseudo-element then hollows out the center.

Temani Afif’s freeCodeCamp tutorial on filling loaders animates the background properties of gradients on a single element. It stores the repeated gradient in one custom property, --g.

Customizing the loader with CSS custom properties

See the Pen
Chrome Cast Loader
by Robin Brons (@bronsrobin)
on CodePen.

Put the size and color in custom properties, and the speed too if you plan to change it. Set them once on the loader and let each rule read from them.

.loader {
--size: 40px;
--color: #0a84ff;
--speed: 1s;
width: var(--size);
height: var(--size);
border: calc(var(--size) / 10) solid #d9d9d9;
border-top-color: var(--color);
border-radius: 50%;
animation: spin var(--speed) linear infinite;
}

.loader.small { --size: 16px; }

Scaling the border width from the size keeps the ring proportions when the loader shrinks or grows.

For a faster spin, the Boosted 5.1 documentation sets its spinner speed to 0.75s. For a slower one, Tailwind lets you register your own animation through a theme variable, so a spin-slow at 3s is a one-line addition.

How to Add a Loader to a Button, a Component or a Full Page

Put the loader markup where the wait happens and hide it by default. A class or data attribute switches it on while the request runs.

CSS has no way to know when a network request finishes, so the show and hide switch always comes from a script. A short JavaScript handler sets the flag before the request and clears it after.

On a button, a data-loading attribute shows a small spinner inside it, and the script disables the button to block double submits. A component container gets position: relative with the loader centered inside, and it keeps its size so nothing jumps when content arrives.

.btn .loader { display: none; }
.btn[data-loading="true"] .loader { display: inline-block; }
btn.dataset.loading = "true";
try {
await fetch(url);
} finally {
btn.dataset.loading = "false";
}

The finally block matters. If the request throws and the flag never resets, the loader runs forever.

A full-page loader is a fixed overlay that covers the screen until the page is ready. It uses position: fixed with inset: 0, which covers the entire viewport, and sits above the content with a high z-index.

Hide the overlay when the content is ready, not on a fixed timer, so it never outlasts the real wait.

Frameworks handle the switch themselves. React Suspense takes a fallback prop that renders the loader while a child component waits for data.

Why Transform and Opacity Keep CSS Loaders Smooth

Animate only transform and opacity in a loader. Those two properties stay on the compositing stage of the rendering path, so the browser skips layout and paint work on every frame (web.dev, as of October 2026).

Per web.dev’s animation performance guide, CSS animations are typically handled on the compositor thread. A spinner built that way can keep turning while the main thread is busy running scripts.

Animated propertyWork the browser repeatsLoader example
transformCompositing onlySpinner rotation, bouncing dots
opacityCompositing onlyPulse fade
background-positionPaintGradient filling loaders
width, height, top, leftLayout and paintProgress fill set through width

What loader libraries do about it

SpinKit’s README says it uses only transform and opacity animations, and its author says they don’t trigger redraws. Loaders.css limits each animation to a small subset of CSS properties to avoid expensive painting and layout calculations. web.dev names the same two properties as the safe pair.

That’s three independent sources agreeing, and a rotate keyframe is already on the safe list.

Tobias Ahlin started SpinKit while adding a loading indicator to Infinite Jekyll. He chose CSS animations over exporting two GIFs for retina screens.

Gradient loaders and will-change

See the Pen
Neumorphism Gradient Loader
by samuel garcia (@sam_garcia2)
on CodePen.

Filling loaders animate background properties, so they sit outside the safe pair. Test them on a low-end phone before shipping.

Adding will-change: transform tells the browser which property is about to animate (web.dev). Put it on the loader element only.

Layout shift around the loader

Good Cumulative Layout Shift values are 0.1 or less, measured at the 75th percentile of page loads (web.dev). A loader that changes its container’s size moves the content around it and pushes that score up.

Position overlays with fixed or absolute so they take no space in the flow, and give the container a min-height close to the expected content height.

CLS is one of the Core Web Vitals, and Lighthouse reports it for the page.

Checking frame cost in DevTools

The Chrome DevTools Performance panel shows whether a loader holds its frame rate.

  1. Open the Performance panel
  2. Record while the loader is running
  3. Read the result, where a high percentage such as 99% means few dropped frames (web.dev)

How to Make a CSS Loader Accessible

Give the loader a text equivalent that screen readers announce, and give motion-sensitive users a version that moves less. A spinning ring tells assistive technology nothing on its own.

Announcing the loading state

The role="status" attribute carries an implicit aria-live of polite and aria-atomic of true (MDN), so the text is announced without taking focus.

Visually hidden text works alongside it. That’s a “Loading…” label sighted users never see, the pattern Bootstrap’s spinner docs use. On top of that, aria-busy gets set to true on the region being filled, then back to false once the content is stable.

The ARIA spec lets assistive technology hold back changes while aria-busy is true and read them as one update afterward. Put it on the results container, not on the status message, or the announcement can be held back too.

<div id="status" role="status" class="visually-hidden"></div>
<div id="results" aria-busy="true">
<div class="loader"></div>
</div>
status.textContent = "Loading results";
// fetch and render the results, then:
status.textContent = "Results loaded";
results.setAttribute("aria-busy", "false");

Sources split on markup. Bootstrap’s docs put role="status" directly on the spinner, while HTMHell’s 2023 live-region guidance says the region should already sit in the DOM, empty, before its text changes.

The pre-existing region is the safer pattern, which is why the example above keeps it separate from the loader.

Respecting prefers-reduced-motion

The prefers-reduced-motion feature is Baseline widely available, supported across browsers since January 2020 (MDN, as of October 2026). Its reduce value asks the page to minimize motion that is not needed, so a loader needs a rule inside a media query that answers it.

One option is to remove the animation and keep a visible “Loading” label. The other is to keep the loop but slow it down or shrink the movement.

Removing the animation alone is the common mistake. A frozen ring with no label looks like a broken page.

@media (prefers-reduced-motion: reduce) {
.loader {
animation: none;
}

.loader::after {
content: "Loading";
}
}

How long a loader may keep moving

WCAG 2.2 success criterion 2.2.2 (Pause, Stop, Hide, Level A) covers moving content that starts automatically, lasts more than 5 seconds and sits next to other content. It needs a way to pause, stop or hide it, unless the motion is part of an activity where it is essential.

W3C’s Understanding document exempts moving content that is not presented in parallel with other content. So a full-page preloader that is the only thing on screen can run past 5 seconds without a control.

An inline loader beside other content is the risky case. The criterion’s Note 4 treats a preload-phase animation as essential only when interaction cannot happen for any user and hiding progress could make people think the page is frozen or broken. A loader beside live content rarely meets that bar, so a pause control or a time limit is the safer call.

A loader that a user starts directly, such as by pressing a submit button, does not count as starting automatically. One that appears on its own at page load does.

Which CSS Loader Libraries and Generators Are Worth Using?

A powerful CSS Loader Generator for creating custom loading animations with zero hassle.

This interactive generator lets you:

  • Create 12 different loader styles from spinners to pulsing hearts
  • Customize dimensions from 20px to 100px
  • Adjust animation speed between 0.5x and 2x
  • Pick any color to match your design

If you only need one loader, copying a single-element snippet from a collection is enough. Several matching styles are a better case for a small library. A framework spinner makes sense only when the project already ships that framework.

Collections and generators

css-loaders.com, Uiverse.io and CodePen hold ready-made snippets. Generators such as loading.io, or a CSS loader generator, let you tune how the loader looks and how fast it spins before you copy the code.

The upside is no dependency, copying only what you need and a live preview. The downside is uneven quality between authors. A snippet is visuals only too, so the status role and reduced-motion rule are on you.

Libraries

SpinKit ships spinners with no shared classes or animations, so any single one extracts cleanly. Loaders.css works differently, since you add an animation class such as ball-pulse and then insert the right number of child divs.

Both install from npm and give you tested styles under one naming scheme, and SpinKit exposes --sk-size and --sk-color for configuration.

There are a few catches. Loaders.css needs several child divs per loader, or its jQuery helper, to build one, and its CSS is not run through autoprefixer (per its npm README). SpinKit leans on CSS variables, so older browsers need them removed (its README).

Framework built-ins

The Bootstrap spinner comes in 2 styles, border and grow, with small-size classes and the status role plus hidden text already in its documented markup.

Tailwind goes the other way. Its animation docs list 4 example animations (spin, ping, pulse and bounce) and leave the shape to you.

Framework spinners cost nothing when the framework is already loaded. Adding one just for a single spinner is a poor reason.

When Is a CSS Loader the Wrong Choice?

CSS loaders stop being the right tool when the wait needs more than a looping shape, such as a percentage, a message, an error path or illustrated motion.

AttributeFailureFix
Loader typeSpinner on a long, measurable waitShow a progress bar
Animated propertiesWidth, top or background animated in the loopRotate or scale with transform
AccessibilityMotion is the only signalAdd status text and a reduced-motion rule
CustomizationPixel sizes copied from a snippet clash with the containerExpose size and color as custom properties
ImplementationNo error path when the request fails or hangsAdd a timeout and a retry control

When Lottie or SVG earns its weight

See the Pen
SVG ∞ loader (no JS, cross-browser, minimal code)
by Ana Tudor (@thebabydino)
on CodePen.

Illustrated or brand-specific motion, such as a character or a logo animation authored in a design tool, is where CSS gets awkward. SVG and Lottie handle those shapes.

A CSS spinner costs a few lines of stylesheet. LottieFiles’ documentation lists its web players at 35 KB (JavaScript) up to 45 KB (React), minified and gzipped.

For a simple illustrated loader, animating an SVG with CSS keeps the file light and the player out of the bundle.

A loader does not fix a slow page

A loader hides a slow request. It does not shorten it.

If users see the loader on every visit, the request is the problem. Fix that first, then check whether a loader is still needed at all.

CSS Loaders Examples To Check Out

Morning Brew Time

See the Pen
cup of coffee loader
by Elena Nazarova (@nazarelen)
on CodePen.

Spiral Serenity

See the Pen
Spiral Loading
by alphardex (@alphardex)
on CodePen.

Daily Dose of Beauty

See the Pen
Daily UI #20 | CSS loader
by Håvard Brynjulfsen (@havardob)
on CodePen.

Get Your Game On

See the Pen
Battlenet Loader
by Stix (@stix)
on CodePen.

Android Sleekness

See the Pen
Android 4.4 Kitkat Loading Screen
by Simon Clavey (@simoncla)
on CodePen.

Spinning Stories

See the Pen
Loader
by Alex Rutherford (@Ruddy)
on CodePen.

Liquid Dreams

See the Pen
Liquid loader
by Mikael Ainalem (@ainalem)
on CodePen.

Starry Corporate Nights

See the Pen
Nutanix Loading Screen
by Ken Chen (@kenchen)
on CodePen.

Burrito Magic

See the Pen
Magic Burrito
by Cody Ogden (@codyogden)
on CodePen.

Cube World

See the Pen
CSS Loader
by Glen Cheney (@Vestride)
on CodePen.

HexaVibes All Around

See the Pen
Hexagon Loading With CSS (2)
by Osama Belal (@osama-belal)
on CodePen.

Stairway to Trust

See the Pen
CSS Stairs Loader
by Irko Palenius (@ispal)
on CodePen.

Redirect with a Twist

See the Pen
Redirecting Loader
by Mr Alien (@mr_alien)
on CodePen.

For the Quirkmasters

See the Pen
Weird Loader
by Sikriti Dakua (@dev_loop)
on CodePen.

Cube Roller Alert

See the Pen
Loader css3
by Mathieu Richard (@MathieuRichard)
on CodePen.

Gearing Up

See the Pen
Cog loading animation
by Jamie Coulter (@jcoulterdesign)
on CodePen.

Swing and Sway

See the Pen
Swing Masking Loader
by Nikhil Krishnan (@nikhil8krishnan)
on CodePen.

Flipping Pancakes, Literally

See the Pen
‘Making pancake’ loader
by Pawel (@pawelqcm)
on CodePen.

Bubbling Donut

See the Pen
Pure CSS bubbles float in 🍩 loader animation
by Ana Tudor (@thebabydino)
on CodePen.

Poppin’ Bubbles

See the Pen
Bubble Gum Loader
by ilithya (@ilithya)
on CodePen.

Cyclone of Simplicity

See the Pen
CSS3 Loaders
by Siddharth Parmar (@Siddharth11)
on CodePen.

Chill Float Vibes

See the Pen
Floating Loading Animation
by Mario Duarte (@MarioDesigns)
on CodePen.

Hashtag Breakup

See the Pen
Single element Slack loader
by CrocoDillon (@CrocoDillon)
on CodePen.

That Glow Though

See the Pen
The Glowing Loader – Pure CSS Animation
by Maxime Rossignol (@Maxoor)
on CodePen.

Old School Loading

See the Pen
Awesome loading screen using only HTML & CSS
by Ahmad Emran (@ahmadbassamemran)
on CodePen.

Legos for Days

See the Pen
LEGO Loader
by Chris Gannon (@chrisgannon)
on CodePen.

Outer Space Feels

See the Pen
Pure CSS Planet Loader Animation #cpc-planets
by Rafaela Lucas (@rafaelavlucas)
on CodePen.

Ride the Wave

See the Pen
#4
by Sasha (@sashatran)
on CodePen.

Taste the Rainbow

See the Pen
Single Element Rainbow Pen Loader
by Dario Corsi (@dariocorsi)
on CodePen.

It’s Gooey

See the Pen
gooey css loader
by Shrikanth (@megatroncoder)
on CodePen.

Borderline Genius

See the Pen
CSS loader with borders
by Jesgrapa (@JesGraPa)
on CodePen.

Arrows Making Waves

See the Pen
Untitled
by Jules Forrest (@julesforrest)
on CodePen.

Bacon All the Way

See the Pen
Bacon Loader
by Chris Gannon (@chrisgannon)
on CodePen.

Slide Into The Future

CSS Loaders FAQ

Which timing function works best for a loader?

Use linear for continuous rotation and ease-in-out for loops that reverse direction, such as pulse and bounce.

Per-keyframe timing functions also work. Tailwind’s bounce animation eases the rise and the fall separately.

How fast should a spinner rotate?

See the Pen
Fancy CSS loaders / spinners
by Jenning (@jenning)
on CodePen.

One turn per second is the usual default, and 0.75 seconds is the fast end (Boosted).

Larger rings tend to look steadier at a slower turn. Keep the duration in a –speed custom property so one edit changes every loader.

How do I center a loader on the page or inside a container?

Give the parent display: grid with place-items: center, or use flexbox with justify-content and align-items set to center.

SpinKit’s sk-center class takes a shorter route. It sets the margin to auto.

Do CSS loaders work in every browser?

Basic keyframe loaders run in every current browser. Newer pieces need a check on Can I use before launch.

SpinKit’s README says to remove its CSS variables when older browsers matter.

Can I use loaders from CodePen or GitHub in a commercial project?

It depends on each snippet’s license. Look for a license file in the repository or a license note on the demo page, and keep the author’s notice if one is required.

If no license appears, ask the author or write your own. A basic spinner takes about ten lines of CSS.

How to Test CSS Loaders Before Launch

Test CSS loaders on a throttled network, with the operating system’s reduced-motion setting on, and against a request that fails on purpose. Fast office connections hide every problem a loader exists to cover.

  1. Throttle the network in DevTools until the wait outlasts the loader
  2. Turn on reduce motion and read the status text with a screen reader
  3. Force a failed request and confirm the error and retry state

Throttling comes first because it exposes layout shift and timeout gaps that the other two checks depend on.

Push the throttled wait past 5 seconds. For a loader that starts on its own beside other content, WCAG 2.2.2 asks for a pause, stop or hide control from that point, and Nielsen’s 10-second limit marks where a progress bar should replace the spinner, so the 5 to 10 second window deserves the hardest testing.

The cost is time, with three passes per page, so start with the views that load the most data.

The sequence holds as of October 2026 against WCAG 2.2. A web accessibility checklist covers the audit beyond the loader, and a newer motion criterion would add a fourth pass.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)