Polyatic CSS Loader Generator
All tools

CSS Loader Generator

Pick one of 30 pure-CSS loaders — spinners, typing dots, skeletons or a progress ring — tune colour, size and speed live, and copy the HTML and CSS. No JavaScript in the output; nothing leaves your browser.

Live preview Ring

Element HTML


              
            

CSS


              
            

A long snippet scrolls inside the box; Copy always takes the whole block, not just the part you can see.

Animated SVG — for a README or docs page

Commit the file next to your README, then reference it in Markdown: <img src="loader-ring.svg" width="48" height="48" alt="Loading"> (or ![Loading](loader-ring.svg)).

No SVG for these six, and why:

  • Flip — A 3D perspective flip: SVG has no 3D transforms, and a flat squash would not look like this loader.
  • Shimmer text — Uses a CSS-only text effect (a gradient clipped to the letters), and an image would draw the word in whatever font the viewer happens to have.
  • Ellipsis — It is live text with CSS-revealed dots; as an image the word would freeze in the viewer's font and stop being readable text.
  • Spinner label — Its point is the visible Loading… label in your own page font; export the plain Ring instead and write the label beside the image.
  • Progress ring — Determinate: it shows a percentage your script sets through a CSS custom property, and a static file could only show one frozen, made-up percentage.
  • Striped bar — Determinate: its fill width is a percentage your script sets through a CSS custom property, and a static file could only show one frozen, made-up percentage.

The output is pure CSS — no JavaScript. The preview and both code snippets are built on your own device; the page's only network call is one anonymous, self-hosted page-view count, and the page keeps working offline.

Offline later: once you have opened this page with a connection, it reopens and works with none — your browser keeps a copy. Not saved: the very first Polyatic page you ever open (open it again while online), and any page or link you have never opened.

Colour
Size48px
Speed1s
Border thickness5px
Accessibility

Adds four things to what you copy — aria-hidden="true" on the loader, a visually-hidden role="status" line reading “Loading…”, the rule that hides it, and a @media (prefers-reduced-motion: reduce) block that stops the animation on a still, fully-visible frame — so untick it if you only want the bare element and rule.

A curated set of pure-CSS loaders

This is a finished gallery of thirty loading indicators, each animated entirely with CSS — a @keyframes block and a class rule, no JavaScript at all. You pick one, adjust its colour, size and speed, and paste the two snippets into your project. Because the loader is pure CSS, it appears the moment the stylesheet parses — before any script has even downloaded — which is precisely when a page most needs to signal that something is happening. It is a deliberately bounded set of thirty distinct motions, honestly counted, rather than a padded list of near-identical variants, and the filter row above the gallery narrows it to one kind — spinners, dots, bars, skeletons or progress — without touching what you have selected. Every style is driven by the same four controls and emits through the same two Copy buttons — there is no second code path and no upsell.

The thirty styles, and how they move

StyleAnimationUses thickness?
Ringa single C-shaped arc rotating 360°yes
Dual ringtwo opposing arcs sweeping in a circleyes
Pulse dotsthree dots fading and scaling in sequenceno
Barsequalizer bars rising and fallingno
Rippletwo rings expanding outward and fadingyes
Pulseone disc breathing — scaling up and down as it fadesno
Flipa square flipping on both axes in 3Dno
Orbita dot circling an invisible trackno
Bouncea single disc bouncing up and downno
Bladeseight fixed spokes; only the opacity travels round the circleyes
Squarea dot walking the four legs of a square path, easing to a stop at each cornerno
Progressan indeterminate bar: one 40%-wide segment sliding along a faint trackyes
Penduluma bob swinging ±32° about a pivot at the top edge, reversing each half-cycleyes
Grida 3×3 lattice fading in a wave that runs along the diagonalno
Wavefive discs offset by a tenth of a cycle, so a crest travels along the rowno
Skeleton cardan image block and two text lines; a brighter band sweeps across them left to rightno
Skeleton textthree ragged lines of a paragraph with the same sweepno
Avatar rowa skeleton avatar: round photo placeholder plus a name and a detail lineno
Shimmer textthe word “Loading” with a light band passing through the lettersno
Typing dotsa chat bubble whose three dots hop one after anotherno
Ellipsis“Loading” followed by one, two, then three dots, in hard stepsno
Spinner labela small ring turning beside a visible “Loading…” labelyes
Stretchan arc that grows from 45° to 270° of the circle and back while it turnsyes
Morpha rounded square rotating and softening into a circle, then backno
Clocktwo hands on a dial, the short one twelve times sloweryes
Hourglasssand drains from the top bulb into the bottom one, then the glass turns overno
Page bara thin bar growing from the left, slowing as it nears the end, then fadingyes
Segmentsfive cells lighting one at a time, like a charging batteryno
Progress ringdeterminate: a ring and a number showing the --p you setyes
Striped bardeterminate: a bar filled to --p percent with moving stripesyes

The six from blades to wave were added because they move on axes the first nine never used, not because the catalogue looked short. Blades is the only style where nothing moves at all — each of the eight spokes holds a fixed rotate() and the animation touches only opacity, one eighth of a cycle apart, so the bright spot walks the circle while every element stands still. Square and orbit both send one dot round a closed loop, but square's path is rectilinear: four straight legs with an eased stop at every corner, against orbit's constant angular sweep. Progress is the one loader here that is not a spinner — a single segment translating along a track at fixed width, which is what a long download wants and what three equalizer bars cannot express. Pendulum is the only rotation that reverses: alternate plus ease-in-out about a pivot at the top edge reads as gravity rather than as a motor. Grid takes the sequential fade of pulse dots onto two axes — the delay is a function of row + column, so the wave crosses diagonally. Wave moves five discs on the Y axis by translation rather than scaling them in place, which is the difference between a crest travelling along a row and three bars changing height where they stand.

The last fifteen rows were added for the jobs front-end developers most often reach for a loader to do and that a spinning shape does badly. A skeleton stands in for the content itself, so the page does not jump when the real card or paragraph arrives; here each one is a single <div>: the shapes are a CSS mask of gradients sized in percentages of an aspect-ratio box, and the shimmer is a 300%-wide background sliding behind that mask, so the highlight only ever lights the shapes and the whole placeholder scales with the size slider. Typing dots is the chat “someone is writing” bubble — its three dots are three background layers, lifted in turn by one keyframes block. The spinner label puts a real word next to the motion for the places a bare shape is ambiguous, like a submit button. The two progress styles are the only determinate loaders here: they show a number you supply, so they belong to uploads and multi-step jobs where you actually know how far along you are.

Twelve styles use the thickness control and it means something slightly different in each: on ring, dual-ring, ripple, stretch, clock and spinner label it is the CSS border width (the clock's hands take 60% of it), on blades the width of each spoke, on progress and page bar the height of the bar (the striped bar is twice that), on pendulum the width of the rod, and on the progress ring the width of the ring. The other eighteen are solid shapes, dots, text or placeholders where there is no stroke to set, so the control hides itself rather than sitting there doing nothing.

Setting the percentage on the progress ring and striped bar

Both determinate styles read one custom property, --p, a whole number from 0 to 100. The copied CSS sets --p: 65 as a placeholder; you override it on the element and update it as your task advances:

<div class="loader" style="--p: 40"></div>

// later, from your upload handler:
el.style.setProperty('--p', Math.round(sent / total * 100));

The ring draws its arc with conic-gradient(colour calc(var(--p) * 1%), track 0), cuts the middle out with a radial mask the width of the thickness control, and prints the number with a CSS counter (counter-reset: p var(--p) and content: counter(p) "%") — so at --p: 40 the arc covers 144° of the circle and the centre reads “40%”. It also registers --p with @property as an <integer>: that is what lets the number count up from 0 on first paint and glide between values. Two honest consequences: a fractional value such as 42.5 is invalid for an integer property and falls back to 0, so round it first; and a browser without @property support still draws the right ring but jumps instead of gliding. The striped bar needs none of that — its fill is a ::before at width: calc(var(--p) * 1%) with a width transition. A sighted visitor sees the number; a screen-reader user only hears what the status line says, so when you know the value, write it there too (“Uploading, 40%”) or pair the visual with a native <progress>.

What the code looks like

Most styles are a single element. Stripped to the loader itself, the default ring is just this:

<div class="loader"></div>

.loader {
  width: 48px;
  height: 48px;
  border: 5px solid #4f52f0;
  border-bottom-color: transparent;
  border-radius: 50%;
  box-sizing: border-box;
  display: inline-block;
  animation: loader-ring 1s linear infinite;
}
@keyframes loader-ring {
  to { transform: rotate(360deg); }
}

The trick is border-bottom-color: transparent: three sides of the circle are painted and one is invisible, so the rotating ring reads as a moving gap. The dots and bars styles need three children, so their HTML wraps three <span> elements; all fifteen newer styles are a single element, using ::before/::after, background layers and masks instead of children, and the four text styles carry their word inside the <div>. The HTML box always shows the exact structure to paste, whatever the style.

With Include the accessible pattern left ticked — its default — the two boxes add these lines around that core, and nothing else:

<div class="loader" aria-hidden="true"></div>
<span class="loader-status" role="status">Loading…</span>

/* The status line: hidden from sight, still announced. The loader
   above is aria-hidden, so this sentence is what a screen reader
   actually says. Remove BOTH elements when loading finishes. */
.loader-status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

@media (prefers-reduced-motion: reduce) {
  /* Resting state: the C-shaped arc held still with its gap at the bottom,
     at full thickness and full colour. Nothing to restore — the arc is
     painted by the border, not by the keyframes. */
  .loader {
    animation: none;
    transform: none;
  }
}

Using a loader well

A loader runs forever by design — its animation-iteration-count is infinite — so you show it by adding the element to the page and stop it by removing that element once your content is ready (remove the status line with it). The two accessibility chores this page used to hand you as homework are now in the snippet itself. While Include the accessible pattern is ticked, the HTML box gives you the loader marked aria-hidden="true" with a visually-hidden role="status" line reading "Loading…" beside it, because a spinning shape means nothing to a screen reader; and the CSS box ends in a @media (prefers-reduced-motion: reduce) block for anyone whose OS is set to reduce motion.

That block is deliberately not a blanket animation: none. For most of the thirty styles simply stopping the animation does land on a decent frame, because their base rule already paints them at full size and full opacity — but it lands there by luck rather than by instruction, and ripple is where the luck runs out. Its two rings have no width, no height and no transform in the base rule at all — size and centring both come from the keyframes — so stopping the animation collapses each ring to its bare border box and drops it off-centre. Measured in Chromium, that is a 10×10px speck at the default 5px thickness (2×2px at the thinnest) with its corner sitting at the middle of what should be a 48px ring. Each style therefore states its own resting frame outright, annotated in the emitted CSS: the ring and dual ring hold their arcs still, the three dots sit at full size and full opacity instead of half-faded, the bars stand at their full height instead of scaleY(0.4), ripple is pinned open at width: 100%; height: 100%, pulse rests at scale(1) and opacity: 1, flip sits face-on rather than edge-on, orbit parks its dot at the top of the track, bounce rests at translateY(0) so nothing around it shifts, blades holds all eight spokes at opacity: 1 (and pointedly does not clear their transforms, which would stack all eight on top of each other), square parks its dot in the corner it starts from, pendulum hangs straight down at rotate(0deg), grid rests as a solid 3×3 block, and wave lays its five discs flat on the centre line. The progress bar is the interesting judgement call: its resting frame pins the segment back to the start of the track at its 40% width rather than stretching it to width: 100%, because a full bar would read as "finished" — a claim an indeterminate loader has no right to make. The same rule decides the newer bars: the page bar rests a third of the way across and segments rests with three of five cells lit, because their base rule starts at zero — empty, which would show nothing at all. The skeletons rest as one even tint with the highlight parked off the left edge, the ellipsis shows all three dots, stretch holds a still three-quarter arc, the clock stops at three o'clock with its hands apart, the hourglass stands upright with a full top bulb, and the two determinate styles keep showing their real --p — only the count-up and the glide are dropped, because the value is information, not decoration. Untick the box and both outputs return to the bare element and rule, byte for byte.

Using a loader in a README or docs page

GitHub, GitLab and most Markdown-based docs generators strip <style> and <div> from what you write, so the HTML + CSS snippet will not run there. Pick a loader, set its colour, size and speed, and press Download SVG: you get one self-contained file of roughly 0.5 to 1.5 KB with its animation in an internal <style> block. Commit it next to your README and reference it as ![Loading](loader-ring.svg), or as <img src="loader-ring.svg" width="48" height="48" alt="Loading"> when you want to set its size. Browsers run CSS animation inside an SVG that is shown as an image, so the loader keeps moving in the rendered page. The file has no script, no font and no link to any other host, and it carries a prefers-reduced-motion rule that stops it on a still frame wherever the viewer applies that setting. 24 of the 30 styles export; flip (a 3D effect), shimmer text, ellipsis, the labelled spinner and the two determinate progress styles do not, and the page lists why under the buttons.

Honest limits

Twenty-eight of these loaders are indeterminate: they show that work is happening, not how far along it is. That includes the progress style — a segment sliding back and forth forever that never claims a percentage. Only the progress ring and striped bar show a value, and only the value you feed them; they do not measure anything themselves. Performance-wise, sixteen of the thirty animate only transform and opacity, which the browser composites on the GPU without re-running layout — cheap even on weak hardware. The other fourteen repaint every frame: ripple animates width and height, the skeletons, shimmer text, typing dots, hourglass, segments, page bar and striped bar animate a background position or size, stretch and ellipsis animate clip-path, morph animates border-radius and the ring animates --p. One or a handful on screen is fine; a feed of fifty shimmering skeletons is not — show a few and let the real content replace them. The skeleton masks and aspect-ratio need a browser from 2021 or later, and the ellipsis's steps(4, jump-none) from 2020 or later. Two more honest limits: blades, grid and wave need 8, 9 and 5 child <span>s respectively, so they are more markup than a one-div ring, and pendulum swings to ±32° precisely so the bob stays inside the box you asked for — widen the swing by hand and it will overflow its own declared width. Finally, these are visual defaults you will likely want to match to your brand — that is what the colour, size and speed controls are for. If you need a completely bespoke animation rather than one of these presets, build it from scratch in the CSS Animation Generator, shape a custom easing curve with the cubic-bezier() Generator, or add depth with the Box-Shadow Generator.

Made with AI tools.