
CSS Spinning Ring Loader
Published
A CSS loader signals "something is happening" during the 200ms-3s gap between a click and a result — the difference between a snappy app and one that feels broken. These 20 hand-coded loaders cover every production loading-indicator pattern: spinning rings, dot chase, skeleton screens (Facebook/LinkedIn pattern), progress bars, bouncing balls, DNA helix, glitch flicker, heartbeat pulse, circular progress, liquid fill, neon arc spinners, cube flip 3D, audio wave bars, morphing squares, orbit planets, typing dots (chat indicator), staircase steps, infinity loops, gradient conic spinners, and particle burst loaders. All 100% pure CSS — zero JavaScript, zero library dependencies (no react-loading, no spinkit, no css-loaders.com snippet copy). Every demo respects prefers-reduced-motion, uses scoped .ld-NN__* class names so multiple loaders coexist on the same page, ships with proper role="status" + aria-live markup for screen reader accessibility, MIT-licensed.
Related25 CSS Spinners22 CSS Skeleton Loaders20 CSS Loading Buttons

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published

Published
<Skeleton width height count /> API. Great DX, but adds a React dependency you may not need. If you're NOT on React (Astro, Vue, Svelte, Rails ERB, vanilla, WordPress): all the React-specific options force you to bring React into your bundle. SpinKit / loaders.css / epic-spinners: framework-neutral CSS-only loader libraries (~2-5kb gzipped). These are essentially what this collection IS — except this collection is MIT-licensed copy-paste with 20 hand-picked patterns instead of 30-100 mediocre ones. loading.io: web tool to generate loaders, often paywalls the source code or watermarks SVGs unless you pay. This collection's 20 loaders: 0 bytes of JavaScript, ~1-3kb of CSS per demo (only the ones you use), zero framework lock-in, MIT-licensed, modify-and-resell allowed. Best fit for: marketing sites, Astro/Eleventy static sites, Rails or Django ERB templates, WordPress themes, any non-React project, and React projects where you want to avoid framework-coupled loading state libraries.background-color: rgba(0,0,0,0.06) on light themes or rgba(255,255,255,0.06) on dark themes. (2) Add the shimmer effect via a linear-gradient sweep: background: linear-gradient(90deg, var(--skeleton-bg) 0%, var(--skeleton-shimmer) 50%, var(--skeleton-bg) 100%); background-size: 200% 100%; animation: shimmer 1.5s ease-in-out infinite; where the keyframe shifts background-position from 200% 0 to -200% 0. (3) Swap the skeleton for real content by setting display: none on the skeleton element + display: block on the real content when your data arrives. Three production-grade details most online tutorials skip: (a) Match dimensions exactly. If your real headline is 28px tall, the skeleton line must be 28px tall — not 16px. If the dimensions drift, the user sees layout shift when real content swaps in (terrible CLS score, Core Web Vital penalty). (b) Don't animate skeleton lines individually. 20 separate animation declarations = 20 animation frames per second × N elements. Put ONE animation on a single pseudo-element that overlays the whole skeleton card — or use a single shared keyframe via animation-delay: var(--i). (c) Respect prefers-reduced-motion. Some vestibular-sensitive users can't tolerate the shimmer sweep — fall back to a static gray rectangle. All 20 demos in this collection respect this.background: conic-gradient(var(--primary) calc(var(--progress) * 1%), var(--track) 0) where --progress is a CSS custom property updated via JS (or pure CSS @property animation for indeterminate). Add a circular mask via ::before { background: var(--bg); border-radius: 50%; inset: 6px; } to create the ring shape. Inside, position a <span> with position: absolute; inset: 0; display: grid; place-items: center; showing the percentage. SVG version: two stacked <circle> elements, the second with stroke-dasharray="circumference" and stroke-dashoffset animated from circumference to 0. Add transform-origin: center + transform: rotate(-90deg) so the stroke starts from 12 o'clock instead of 3 o'clock. Cost comparison: react-circular-progressbar ~5kb minified + requires React. This demo: ~25 lines of CSS, zero JS, no library. Browser support for conic-gradient: Chrome 69+, Safari 12.1+, Firefox 83+ — universally supported. The CSS transition: --progress 0.6s ease-out (with @property) animates smoothly between step changes — but it's an INP-safe transition (just paint, no layout).animation but staggered: .dot:nth-child(1) { animation-delay: 0s } .dot:nth-child(2) { animation-delay: 0.2s } .dot:nth-child(3) { animation-delay: 0.4s }. (3) The animation: a 1.4s loop that translates the dot upward by ~6px at the 30%-40% keyframe, then back. animation: typing-bounce 1.4s ease-in-out infinite;. Three production-grade details: (a) Use a chat-bubble container around the dots — a rounded rectangle with the message-tail SVG/CSS — so it matches the rest of your chat UI. (b) Animate transform: translateY(-6px) not top: -6px — translate is GPU-accelerated. (c) Add a fade-in/out wrapper for when the typing state toggles — the dots shouldn't pop into existence. opacity: 0; transition: opacity 0.2s then opacity: 1 when the typing flag flips. Production usage: Discord, WhatsApp, iMessage, Slack, Telegram, Microsoft Teams all use variants of this pattern. The pattern is part of the conversational UI lexicon — users immediately recognize it as "the other side is composing."role="status" on the loader's outer wrapper. Tells assistive tech that the element conveys status information. Screen readers (NVDA, JAWS, VoiceOver, TalkBack) announce the content politely without interrupting the user's current task. (2) aria-live="polite" in addition to role="status" (technically the role implies it, but explicit doubles up nicely for buggy older assistive tech). Polite = announce when the user is idle. Avoid aria-live="assertive" for loaders — assertive interrupts the user, which is jarring for routine "loading" announcements. (3) Visually-hidden text inside the loader: <span class="sr-only">Loading…</span> with .sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }. The spinning ring (Demo 01) means nothing to a blind user — the visually-hidden "Loading…" text IS the announcement. (4) Update the text when state changes. When loading completes, swap the inner text to "Loaded" or remove the loader from the DOM entirely. aria-live will announce the change. (5) prefers-reduced-motion: reduce — for vestibular-sensitive users (people who experience motion sickness from continuous motion), pause or simplify the animation. Don't just remove it (then no loading indication at all) — replace with a static "Loading…" text or a single-frame icon. Common mistake most online tutorials make: animating only with CSS without any aria/role markup. Blind users have no idea your app is loading. The 20 demos here ship the markup correctly out of the box.transform and opacity only. Every loader in this collection follows this — spinning ring uses transform: rotate(360deg), not border-rotation; bouncing balls use transform: translateY(), not top; skeleton shimmer uses background-position (paint-only, not layout). All transforms and opacity changes are GPU-accelerated on the compositor thread — they DON'T trigger layout recalculation, so they DON'T affect INP (Interaction to Next Paint), the Core Web Vital that replaced FID in March 2024. Five loader-specific gotchas: (a) Don't animate border-radius on a continuous loop — repaints the entire element every frame. (b) Don't animate box-shadow on a continuous loop — same problem, plus shadows are GPU-expensive on mobile. (c) Don't use backdrop-filter on a loader that overlays a long scrollable page — backdrop-filter re-evaluates on every scroll frame, tanking scroll FPS. (d) Avoid SVG stroke-dashoffset animation for high-frequency loops — it's repaint-heavy. Use conic-gradient instead (Demo 19). (e) Limit concurrent loaders — three skeleton cards animating simultaneously is fine, but 50 stacked into a virtual-scroll list will saturate the compositor on low-end Android. Use animation-play-state: paused on off-screen skeletons via IntersectionObserver. All 20 demos in this collection: 95+ Performance score on Pixel 5 baseline. Skeleton screens actually IMPROVE perceived performance (lower CLS, lower TTI feel) compared to spinners — Nielsen Norman Group ~22% reduction in perceived wait time.const t = setTimeout(() => setLoading(true), 200); fetch(...).finally(() => { clearTimeout(t); setLoading(false); }). Operations under 200ms = no loader shown. Operations over 200ms = loader appears after 200ms and stays until done. (2) Minimum visible duration. Once shown, the loader stays visible for at least 500ms even if the operation finishes sooner. Prevents flash from "loader appears at 195ms, disappears at 220ms" (25ms flash, terrible). Combine both for best UX: 200ms grace period + 500ms minimum visible duration. Three patterns for the loader fade: (a) Instant on, fade out — when the operation finishes, fade the loader's opacity 1→0 over 200ms then remove. Feels polished. (b) Fade in + fade out — fade in over 100ms when shown, fade out when removed. Smoothest. (c) Replace with content immediately — when content's ready, swap loader for content with no fade. Snappiest, best for skeleton screens (the skeleton IS the layout placeholder, replacing it with real content feels natural). React: useDebounce(loading, 200). Vue: <Transition appear>. Astro: CSS transitions on a class toggle. None of this is loader-design-specific — it's wrapping logic that applies to all 20 demos.20 hand-coded CSS animated buttons — neon glow, ripple, 3D press, liquid fill, jelly bounce, shine sweep, animated border, moving gradient CTA, text flip, submit success state, add-to-cart progress, download icon, hamburger-to-close, toggle switch, loading spinner inside button, next/prev arrow nav, and ghost button background reveal. Half pure CSS, half lightweight JS for production interactions.
11 pure CSS animation-timeline demos built on the W3C scroll() and view() spec — reading progress bar, view-timeline fade & reveal, shrinking sticky header, stacking cards, horizontal scroll section, parallax hero, container shadow indicators, reverse-scroll columns, text scrim reveal, cover card to sticky header, and image zoom / clip-path wipe. Zero JS, off the main thread, Core Web Vitals safe. Copy-paste ready.
25 hand-coded CSS background animations for SaaS hero sections, developer tool documentation, agency portfolios, gaming and Web3 landing pages, seasonal e-commerce campaigns, and dark-mode dashboards. Covers full-screen gradient shifts, animated mesh gradients, aurora borealis glow, cursor spotlight follow, interactive water ripples, dot grid and diagonal stripe patterns, SVG grain and noise texture overlays, topographic contour lines, layered SVG waves, plasma and lava lamp fluids, bokeh light blur, particle constellation networks, matrix digital rain, synthwave 3D perspective grids, starfield warp speed, CRT scanline overlays, fog and mist drift, morphing organic blobs, floating glassmorphism orbs, halftone dot patterns, starry night skies, floating geometric shapes, rising bubbles, and falling snow. 20 are pure CSS with zero JavaScript; the 5 that need it use vanilla canvas or pointer tracking with no dependencies. Every background animates compositor-only properties so it never blocks the main thread, is scoped under a .bga-NN prefix for no-collision pasting, guards prefers-reduced-motion, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.