🎨 CSS

CSS Loader Generator

Pick a loader style, customize it, and copy the HTML + CSS.

About CSS Loaders

Spinner, bouncing dots, and bars all run on plain CSS @keyframes, so nothing needs to execute at runtime beyond the stylesheet itself. Copy the markup and styles into your project and the animation just works.

Why a loading indicator specifically shouldn't depend on JavaScript

A loading spinner exists to reassure someone that the page is doing something while they wait, which is exactly the moment JavaScript execution is most likely to be busy, blocked, or delayed, the main thread might be occupied parsing a large script, waiting on a slow API response, or running expensive computation. A loader that itself depends on JavaScript to animate is self-defeating: it can freeze at the worst possible time, right when the user most needs visual confirmation that something is still happening. That's why all three styles here are pure CSS @keyframes animations with no JavaScript driving the motion, the browser's own compositor handles the animation independently of whatever the main thread is doing.

Why every animation here uses transform, never width or position directly

The spinner rotates via transform: rotate(), the dots scale via transform: scale(), and the bars scale via transform: scaleY(), none of them animate a raw layout property like width, height, or top. Transform-based animations can be handled entirely by the GPU compositor without forcing the browser to recalculate the page's layout on every animation frame, which is both smoother and considerably cheaper than animating box-model properties directly. For an animation that's expected to loop continuously for as long as content is loading, potentially several seconds or more, that efficiency difference actually matters for battery life and frame consistency, especially on mobile devices.

The spinner's faded ring is built with color-mix()

The spinner isn't drawing two separate elements for the pale ring and the bright arc, it's one element with a single circular border, where three sides use a faded version of your chosen color and the top side stays fully solid. That faded version comes from the CSS color-mix() function, blending your chosen color at 20% strength against transparent, producing a subtle tinted ring without needing to precompute a separate lighter hex value. color-mix() is a comparatively recent CSS addition, gaining support across Chrome, Firefox, and Safari around 2023, so if you need to support notably older browsers, you'd want to swap it for a manually calculated rgba() value with roughly 20% opacity instead.

How the staggered "wave" effect on dots and bars works

Both the bouncing dots and the bars use the exact same keyframe animation on every one of their child elements, but each child gets a different animation-delay, 0s, 0.15s, and 0.3s for the three dots, similarly staggered for the four bars. Since all elements are technically running the identical animation loop just offset in time, the second element is always doing what the first element did a fraction of a second ago, which is what creates the visual impression of motion traveling across the group rather than every piece pulsing in unison.

Why the copied code uses literal numbers instead of CSS custom properties

Internally, the live preview actually is driven by CSS custom properties (--ld-size, --ld-speed, --ld-color), which is what lets dragging a slider update the animation instantly without rebuilding the whole stylesheet string. But the code you copy for your own project intentionally bakes those values in as plain literal numbers and colors instead of custom properties, so pasting it doesn't require you to also separately define matching CSS variables elsewhere in your codebase, it's copy-paste-ready as a fully self-contained block.

Frequently Asked Questions

Why don't these loaders use JavaScript to animate?

A loading indicator often needs to keep moving while JavaScript itself is busy or blocked, which is precisely when a JS-driven animation is most likely to stutter or freeze. Using pure CSS @keyframes lets the browser's compositor handle the animation independently of the main thread's workload.

Why do these loaders animate transform instead of width, height, or position?

Transform-based animations can run on the GPU compositor without forcing a layout recalculation on every frame, making them smoother and less costly than animating box-model properties directly, which matters for an animation that loops continuously for as long as content is loading.

How is the spinner's faded ring color created?

It uses the CSS color-mix() function to blend your chosen color at 20% strength against transparent, producing a subtly tinted ring on three sides of the border while the top side stays fully solid. color-mix() gained broad browser support around 2023.

What creates the traveling "wave" look in the bouncing dots and bars?

Every dot or bar runs the exact same animation keyframes, but each one has a different animation-delay, so they're always a fraction of a second offset from each other. That offset is what creates the appearance of motion traveling across the group rather than everything moving in unison.

Why does the copied CSS use plain numbers instead of the CSS variables the live preview uses?

The live preview uses CSS custom properties internally so sliders can update it instantly, but the copied code bakes in literal values instead, making it a self-contained block you can paste directly without needing to separately define matching variables in your own project.

What should I use instead of color-mix() if I need to support older browsers?

Replace it with a manually calculated rgba() value at roughly 20% opacity using your chosen color's RGB channels. This produces a visually similar faded ring without relying on color-mix(), which isn't supported in browser versions from before around 2023.