Dots Loader — Free HTML CSS Loading Snippet

Dots Loader · Loaders · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Bounce: translateY(-14px) + opacity with 0.2s staggered animation-delay
Pulse: scale(0.6→1.2) + opacity on 4 dots with staggered delay
Wave: scaleY(0.3→1) on 5 bar-shaped dots — equaliser/waveform effect
Spinner: border-top-color arc + cubic-bezier(0.6,0,0.4,1) elastic timing
Button loading: .loading hides text, shows inline dots, disables re-click
Button success: removes .loading, shows ✓ Saved!, green background for 2s
All animations: transform + opacity only — GPU compositor, no layout triggers
Zero JavaScript for the four display variants — pure CSS keyframes

About this UI Snippet

Dots Loader — Bounce, Pulse, Wave, Spinner & Button Loading State — Pure CSS Animations

Screenshot of the Dots Loader snippet rendered live

Loading dots are the most versatile and universally recognised loading indicator pattern. Unlike a progress bar (which implies a known completion time) or a skeleton loader (which implies imminent content), loading dots communicate "something is happening" in a neutral, open-ended way. This snippet provides four distinct dot animation variants plus a button loading state — all animating with pure CSS keyframes and no JavaScript for the animations themselves.

Bounce uses translateY(-14px) at the 50% keyframe to lift each dot upward. Staggered animation-delay (0s, 0.2s, 0.4s) on the three dots creates the wave-bounce sequencing. Opacity fades from 0.4 to 1 in sync with the translation, giving the dots a depth illusion as if they are closer when raised.

Pulse scales each dot from scale(0.6) to scale(1.2) with four dots at 0.2s staggered delays. The scale oscillation plus opacity creates a breathing, organic feel. This variant works well for "thinking" or processing states where there is no known end time.

Wave uses bar-shaped dots (8×32px, border-radius: 4px) and scaleY animation — each bar shrinks to 30% height and grows back to full height in sequence, mimicking an audio waveform or equaliser. The five bars with 0.1s staggered delays create a smooth rolling wave.

Elastic Spinner uses border-top-color: #6366f1 on a full-circle border to create the classic arc spinner. The cubic-bezier(0.6, 0, 0.4, 1) timing function accelerates and decelerates the rotation to give an elastic, bouncy feel — much more polished than linear spin.

Button Loading State is the most practically useful variant. Clicking the button adds .loading which hides the text span and shows three inline pulse dots. After an async operation completes (replace the setTimeout with your real fetch call), the .loading class is removed and a success state is shown. The button becomes cursor: not-allowed during loading to prevent double-submission.

All animation variants use transform (translateY, scale, scaleY, rotate) and opacity — properties that run on the GPU compositor and never trigger layout recalculation, keeping all five animations at 60fps even on low-end devices.

Choosing the right variant

Bounce works best for full-page loading overlays and large loading states where the vertical movement reads clearly. Pulse is the standard for "thinking" or processing states — three pulse dots are universally recognised as "in progress". Wave is distinctive for audio-adjacent contexts (music players, voice recorders, chat transcription). The elastic spinner is the most compact and works well in button loading states, small inline indicators, and form field validation spinners.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than reasoning through four keyframe sets on your own, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the bounce, pulse, and wave animations stagger with animation-delay on nth-child selectors instead of JavaScript timers, and why every variant only animates transform and opacity rather than width, height, or top/left. The same assistant can help optimize it, for instance checking whether the button loading state's two nested setTimeout calls in triggerLoad() could drift out of sync with a real fetch promise chain. It's also useful for extending the pattern: ask it to add a fifth loader variant with a different stagger shape, wire the button's loading state to an actual async function with proper error handling, or pause an off-screen loader's animation with IntersectionObserver to save GPU cycles. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a set of loading indicators in plain HTML and CSS using only keyframe animations, plus one interactive button loading state in vanilla JavaScript — no animation library.

Requirements:
- A bounce loader: three dot elements that each translateY upward and back with synchronized opacity change, where each dot's animation-delay is staggered (e.g. 0s, 0.2s, 0.4s) purely via nth-child CSS selectors so no JavaScript drives the timing.
- A pulse loader: four dots that scale up and down between two scale factors with staggered delays, using the same nth-child stagger technique.
- A wave loader: five bar-shaped elements (tall and narrow, not circular) that animate scaleY between a shrunken and full height with tighter staggered delays, so they read as a rolling audio-equalizer wave.
- An elastic spinner: a single circular element with a transparent track color and one differently-colored border side, rotating continuously with a cubic-bezier easing curve rather than linear, so the spin has a bouncy, elastic feel.
- Every one of the above four variants must animate only the transform and opacity CSS properties, never width, height, margin, or position offsets, so they run on the GPU compositor without triggering layout.
- A button with a loading state: clicking it swaps its visible label for an inline three-dot pulse animation and disables re-clicks while "loading" (simulated with a timeout, structured so it is obvious where a real fetch or form submission promise would plug in), then on completion shows a temporary success label and color before reverting to its original idle label and color.

Step by step

How to Use

  1. 1
    Pick your variantClick the "Save changes" button to see the button loading state. All four animation variants (bounce, pulse, wave, spinner) run automatically on page load.
  2. 2
    Copy just the variant you needCopy only the HTML, CSS, and JS for the specific variant you need. Each is self-contained — bounce needs only .bounce .dot CSS rules, pulse needs only .pulse .dot, and so on.
  3. 3
    Wire the button loading state to your formIn JS, replace the setTimeout inside triggerLoad() with your real fetch or form submission. Call btn.classList.remove("loading") inside .then() or .finally() so the button always re-enables even on error.
  4. 4
    Change the loader colourReplace #6366f1 in the CSS with your brand accent colour. This updates all four variants simultaneously. For the button dots, they use rgba(255,255,255,0.8) — white on the coloured button background.
  5. 5
    Change the dot size and gapUpdate the width and height on .dot for each variant. Change gap on .loader to control spacing between dots. For the wave variant, adjust the height on .wave .dot (default 32px) to control bar height.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component where the button loading state uses useState, or "Tailwind" for a Tailwind CSS version.

Real-world uses

Common Use Cases

Form submission and async operation feedback
The button loading state prevents double-submission and communicates that the form is being processed. Wire to fetch() or axios — add .loading on click, remove in .then()/.finally(). The cursor: not-allowed prevents impatient re-clicks.
Chat typing indicators and real-time presence
The pulse dots (3-dot variant) is the universal "someone is typing" indicator in chat UIs. Position it absolutely in the chat thread below the last message when the other user is composing a reply.
Full-page loading overlays for route transitions
Place any variant inside a fixed overlay (position:fixed, inset:0, background:rgba(255,255,255,0.9)) for a full-page loading overlay. Show on route change start, remove on completion. The bounce and spinner variants work best at large sizes for overlay contexts.
Inline content-loading placeholders
Use the pulse dots inline inside a card or widget while its data loads. Replace the dots with the actual content once the fetch resolves — same container, same position. Simpler than a full skeleton loader for single-value displays.
Study CSS keyframe animation staggering technique
The staggered animation-delay pattern — giving nth-child elements increasing delay values — is the core technique behind most multi-element CSS animations. This snippet shows three variants of the same pattern: translateY stagger, scale stagger, and scaleY stagger.
Music players and audio equaliser visualisations
The wave variant bars mimic an audio equaliser — for a real signal-driven version, see the audio waveform visualizer. Change the background to your audio player accent colour, remove the label, and position it in the "now playing" indicator area. Add animation-play-state: paused when audio is paused to freeze the bars.

Got questions?

Frequently Asked Questions

CSS properties that trigger layout recalculation (width, height, top, left, margin, padding) cause the browser to recalculate the positions of all affected elements on every animation frame — expensive on any device. Properties handled by the GPU compositor (transform: translateY, scale, rotate and opacity) bypass layout and paint entirely. The GPU moves pixels directly. This is why these dot animations run at 60fps even on mobile — they never touch the main thread.

The .loading class sets cursor: not-allowed and the triggerLoad() function returns early if the button already has the .loading class (if (btn.classList.contains("loading")) return). For a more robust solution, also set btn.disabled = true on click and btn.disabled = false in your .finally() handler — the disabled attribute prevents click events at the browser level, not just visually.

Click "JSX" to download. In React: const [loading, setLoading] = useState(false). On click: setLoading(true); try { await yourApiCall(); setSuccess(true); } finally { setLoading(false); }. Conditionally render the text or dots: {loading ? <Dots /> : "Save changes"}. Add disabled={loading} to the button element. Use useRef if you need to reset button text after success.

Use IntersectionObserver: const obs = new IntersectionObserver(([entry]) => { entry.target.style.animationPlayState = entry.isIntersecting ? "running" : "paused"; }); obs.observe(document.querySelector(".wave")). This pauses the animation when the element is scrolled out of view, saving GPU resources on long pages with multiple loaders.