Every app makes users wait — for an API response, a page navigation, a file upload, an AI answer. What separates a polished product from a janky one isn't the wait itself, it's what's on screen during it. A blank white page and a well-built skeleton screen can cover the exact same 800 ms, and one feels instant while the other feels broken. Below are 8 free loading state snippets — shimmer skeletons, a YouTube-style top bar, animated progress rings, a drag-and-drop upload with real per-file percentages — you can preview live and copy in seconds. Pure HTML, CSS and a few lines of JS, no framework required.
As with every roundup in this series, this isn't just a copy-paste dump. For each snippet you'll find how it actually works under the hood, when to reach for it (and when it's the wrong call), and a quick tip for customising or keeping it accessible. By the end you'll know the small toolkit — background-position shimmers, stroke-dashoffset maths, the NProgress "trickle" model, staggered animation-delay — behind nearly every loading state you've ever admired.
Every snippet below is a live, running preview — watch it right in the article. When you find one you like, hit View / Edit Code to grab the HTML/CSS/JS or export it to React, Vue, Angular or Tailwind.
The one rule of loading states: match the wait
Before copying anything, it helps to know which kind of loading state a situation calls for. Get this wrong and even a beautiful animation feels off:
- Under ~300 ms — show nothing. A spinner that flashes for a tenth of a second reads as flicker, not feedback.
- Unknown duration, known layout — use a skeleton screen. It previews the structure the user is about to receive, which is why Facebook, LinkedIn and YouTube all use them.
- Unknown duration, unknown layout — use an indeterminate spinner or top bar. It only says "we're working", which is all you honestly know.
- Known progress (upload percentage, steps completed) — use a determinate bar or ring. If you have a real number, show the real number.
One more rule that applies to all eight: when you drop any of these into production, give the loader role="status" and an aria-label so screen readers announce that content is loading, and wrap the animation in a prefers-reduced-motion media query for users who opt out of motion. It's two lines, and it's the difference between decorative and accessible.
1. Skeleton Loader — the shimmer everyone recognises
How it works: every placeholder shares one class with a horizontal linear-gradient — grey, brighter in the middle, grey again — stretched to background-size: 200% 100% so only half is visible at once. A single keyframe animates background-position from 200% 0 to -200% 0, sweeping the bright band across the element. That's the entire shimmer: no JavaScript, no extra elements, and because background-position doesn't trigger layout or paint, you can animate dozens of skeletons at once with no performance cost.
When to use it: any card, list or profile whose layout you know before the data arrives. The snippet ships composable shape utilities — a circle for avatars, a tall block for images, thin strips of varying widths for text lines — so you can mirror your real component in a few divs. Tip: vary the line widths (60%, 40%, 80%…) — uniform full-width bars look like a wireframe, varied ones read as "text is coming".
Try it: Skeleton Loader
2. Skeleton Card Grid — the skeleton-to-content swap, done right
How it works: this one demonstrates the part most skeleton tutorials skip — the handoff. A product grid renders six shimmering placeholder cards, then after the simulated fetch resolves, swaps in the real cards with a short fade-and-rise animation (opacity plus a 6px translateY). Because the skeleton cards have the same dimensions as the real ones, the grid doesn't jump when content lands — no layout shift, which is exactly what Google's CLS metric punishes.
When to use it: product listings, search results, dashboards — anywhere a grid of items loads from an API. Tip: render the same number of skeletons as the items you typically expect. Showing 3 placeholders and delivering 20 results (or the reverse) breaks the illusion that the skeleton was a preview.
Try it: Skeleton Card Grid
3. Top Loading Bar — the YouTube/GitHub trickle
How it works: the thin bar that slides across the top of the page during navigation solves an honest problem: you can't know how long a request will take, so a real percentage is impossible. The trick — popularised by the NProgress library — is the trickle model. On start, the bar jumps to 8%, then a timer nudges it upward by a shrinking amount every 400 ms, easing toward but never passing 90%. It races early and slows near the top, conveying activity without ever pretending to be done. When the real work finishes, it snaps to 100% and fades out. The width itself is animated with requestAnimationFrame easing so the motion stays silky, and a glowing "peg" via box-shadow on the leading edge gives it that signature comet look.
When to use it: route changes in single-page apps and background fetches that shouldn't block the UI. It occupies zero layout space, so the page stays fully usable underneath. Tip: wire startBar() to your router's navigation-start event and finishBar() to its complete event — the trickle handles everything in between.
Try it: Top Loading Bar
4. Loading Button — the loading state users trigger themselves
How it works: the most common loading state isn't a page load — it's the moment after a user clicks Save. This button swaps its label to "Saving…", reveals a tiny inline spinner (a circle with a semi-transparent border and one solid white segment, rotated by a single keyframe), blocks double-clicks with a class guard, then lands on a green "✓ Saved!" confirmation before resetting. Three states — idle, loading, done — driven by two CSS classes and a few lines of JS.
When to use it: every form submit, save action and checkout button you ship. Feedback at the point of interaction beats a global spinner because the user's eye is already there. Tip: the min-width on the button matters more than it looks — it stops the button resizing when the label changes, which would otherwise shift everything around it mid-click.
Try it: Loading Button
5. Dots Loader — four variants from one technique
How it works: bounce, pulse, wave and an elastic ring — all built on one idea: give every dot the same animation, then offset each with animation-delay: 0.2s × its index. The stagger is what turns identical keyframes into a travelling wave. The bounce variant pairs translateY with an opacity dip so dots "light up" at the top of their arc; the wave variant stretches bars instead of moving circles; and a bonus button shows the dots working as an inline busy indicator.
When to use it: chat "typing…" indicators, inline waits inside cards and buttons, anywhere a full skeleton is overkill. Dots are the smallest polite loader there is. Tip: keep the loop between 1.2 and 1.5 seconds — faster reads as frantic, slower reads as stalled.
Try it: Dots Loader
6. SVG Progress Ring — the stroke-dashoffset classic
How it works: the circular progress ring you see in Lighthouse scores and fitness apps is one SVG trick. A circle's stroke-dasharray is set to its exact circumference (2πr ≈ 251.2 for a radius of 40), which makes the entire outline one dash. stroke-dashoffset then hides part of that dash — offset by the full circumference and the ring is empty; offset by circumference × (1 − pct/100) and exactly pct percent is drawn. A CSS transition on the offset animates the sweep, the SVG is rotated −90° so progress starts at 12 o'clock, and a small JS counter animates the percentage label in sync.
When to use it: scores, completion percentages, step progress — any determinate value where the number is the story. Tip: stroke-linecap: round is the difference between "developer drew a circle" and "designer made a gauge" — it rounds the sweep's ends so the ring feels finished.
Try it: SVG Progress Ring
7. CSS Loader Gallery — eight spinners, zero JavaScript, click to copy
How it works: a grid of eight pure-CSS loaders — classic spinner, bouncing dots, pulse ring, shimmer skeleton, orbit, indeterminate progress bar, square grid, typing cursor — each one a self-contained few lines of CSS. The classic spinner is the oldest trick in the book: a circle with a light border, one segment recoloured via border-top-color, rotated forever. The pulse ring scales up while fading out. The square grid is nine cells sharing one fade keyframe with delays incremented by 0.1s so a lit wave sweeps across. Click any card and its minified, standalone CSS lands on your clipboard with a toast confirmation.
When to use it: the "I just need a spinner, now" moments — no library, no SVG asset, no dependency audit. Because every loader is pure CSS, each costs a single element and keeps animating even when the main thread is busy. Tip: the border-top spinner is three lines of CSS; if you're about to install a spinner package, paste this instead.
Try it: CSS Loader Gallery
8. Upload Progress — real percentages, per file
How it works: the one loading state where you do have real numbers, done properly. A dashed drop zone accepts drag-and-drop or click-to-browse, then renders a row per file — an icon picked by extension, the filename, a formatted size, and a thin progress bar whose fill animates via a CSS transition on width, so each JS update glides instead of jumping. Every file tracks its own percentage while a summary bar at the bottom aggregates them all into one overall figure, and finished files flip to a green "done" state. Removing a file mid-upload cancels its ticker — a small detail most implementations forget.
When to use it: any multi-file upload — avatars, attachments, media libraries. Users tolerate long uploads when they can see per-file movement; a single frozen spinner over a 40 MB upload is where trust goes to die. Tip: the demo simulates progress with timers — swap those for XMLHttpRequest's upload.onprogress event and the same rendering code shows real network percentages unchanged.
Try it: Upload Progress
The techniques you just collected
Eight snippets, but really five reusable ideas — the same handful that power almost every loading state on the modern web:
- The background-position shimmer — an oversized gradient swept across an element. Powers both skeletons and the gallery's skeleton card, and it's compositor-friendly, so it scales to dozens of elements.
- Staggered
animation-delay— identical keyframes offset per element to create waves. Powers the dots loader and the gallery's square grid. stroke-dasharray/stroke-dashoffset— draw exactly n percent of any SVG path. Powers the progress ring and every animated checkmark you've ever seen.- The trickle model — fake confident progress toward 90%, then complete on real success. The honest way to show progress you can't measure.
- Transition-smoothed JS updates — set
widthfrom JavaScript, let a CSStransitiondo the easing. Powers the upload bars and the loading button's state changes.
Final Thought
Users forgive waiting; they don't forgive wondering whether anything is happening. Pick the state that matches what you actually know — skeleton for known layouts, trickle bar for unknown durations, ring for real percentages — and the same app suddenly feels faster without a single backend change.
All eight snippets live in the free FWD Tools UI snippet gallery — preview them live, edit the code in the browser, and export to React, Vue, Angular or Tailwind. And if you missed the earlier roundups in this series, they cover copy-paste button snippets, hover card snippets and text effect snippets.
