20 CSS Loading Buttons
20 hand-coded CSS loading buttons for checkout flows, dashboards, upload forms and any action that waits on a server. Covers the inline spinner swap, the width jump when a label changes, double-submit guarding, the aria-busy contract screen readers need, and the states most builds skip — retry after failure, optimistic save, and an undo window instead of a confirm dialog. Scoped under .lb-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Related51 CSS Buttons20 CSS Loaders20 CSS Form Validation30 CSS Notifications20 CSS SVG Stroke Buttons
Course pickPartner: Zero to Mastery
Affiliate linkWant to build production-ready CSS layouts, animations and UI from scratch?
You just browsed 20 loading buttons. Learn the Flexbox, Grid and animation thinking behind them, so the next component starts in your head, not in a copy-paste.
Every ZTM course in one membership: $25/month, paid yearly at $299 · 30‑day money-back guarantee
The CSS Bootcamp. We may earn a commission, at no extra cost to you.
02. Fixed Width Loading Button No Layout Shift (opens in a new tab)
03. Disabled Pending State Button (opens in a new tab)
04. Accessible Loading Button with ARIA Live (opens in a new tab)
05. Debounced Minimum Spinner Duration (opens in a new tab)
06. Success Checkmark Morph Button (opens in a new tab)
07. Error State with Shake and Retry (opens in a new tab)
08. Background Progress Fill Button (opens in a new tab)
09. Percentage Progress Upload Button (opens in a new tab)
10. Streaming Response Typing Button (opens in a new tab)
11. Checkout Pay Button with Pending Lock (opens in a new tab)
12. Optimistic UI Save Changes Button (opens in a new tab)
13. Form Submit with Validation Error State (opens in a new tab)
14. Infinite Scroll Load More Button (opens in a new tab)
15. Delete Button with Undo Window (opens in a new tab)
16. Ghost Outline Loading Button (opens in a new tab)
17. Icon Only Circular Loading Button (opens in a new tab)
18. Full Width Mobile Touch Button (opens in a new tab)
19. Gradient Border Trace Loading Button (opens in a new tab)
20. Dark Mode Tokenized Loading Button (opens in a new tab)
Frequently asked questions
How do I stop a button from resizing when it shows a loading state?
grid-area: 1 / 1 and crossfade opacity between them — the button sizes to the wider of the two and never moves. Or set a min-inline-size in ch units, sized to the longest label the button will ever hold, which is simplest when the states are known up front. Or reserve the spinner's box with visibility: hidden rather than display: none, so the space exists before the spinner appears. What does not work is swapping textContent and hoping, which is the default approach and the reason so many production buttons visibly jump. The shift is not only ugly — it is a Cumulative Layout Shift event, and a button near other controls can push a mis-click onto the wrong target.How do I prevent a double submit on a loading button?
disabled property on the element itself, not a class that only changes appearance. A class with pointer-events: none stops the mouse but not the keyboard — the button still activates on Enter or Space, so a determined double-tap still fires twice. disabled makes the browser refuse activation from every input method, which is the whole point. There is a real tradeoff, though: a disabled element is removed from the tab order and silently drops focus, so a keyboard user who just pressed the button is dumped back at the top of the document with no idea what happened. When focus must persist, use aria-disabled="true" plus a guard clause that returns early from the handler — the button stays focusable and announces as unavailable, and your own code blocks the second call. Belt and braces is to also guard on the state itself: if data-state is already pending, return immediately.How do I make a loading button accessible to screen readers?
aria-busy="true" on the button while the request is in flight and clear it on resolution, which is the standard way to say "this control is working". Announce the outcome through a live region: an element with aria-live="polite" that you write "Changes saved" into on success. Use role="alert" for the failure path instead, because a failed save is urgent and polite announcements can be queued behind other speech. Beyond that, never rely on colour alone for the error state — red plus a label, or red plus an icon. And if the button shows determinate progress, it needs role="progressbar" with aria-valuenow, aria-valuemin and aria-valuemax kept in sync, or the percentage is invisible to assistive technology.Why does my loading spinner flash and look broken?
What is optimistic UI and when should I use it for a save button?
Should I use a confirmation dialog or an undo window for delete?
How do I show upload progress in a button?
button.style.setProperty('--progress', pct), with the background reading that value through a gradient stop or a scaled pseudo-element. Register the property with @property and a <percentage> syntax so it can transition smoothly between updates instead of stepping between them — an unregistered custom property is just a string to the engine and will not interpolate. If you show a number alongside the bar, set font-variant-numeric: tabular-nums so the digits stay the same width and the label does not jitter on every update. One honesty point worth making: only show a percentage when you actually know the total. If the response is streamed or the size is unknown, a percentage is invented and users notice when it stalls at 90% — an indeterminate spinner is the more truthful control.How do these port to React, Vue, Svelte and Tailwind?
.lb-NN selector and custom property is framework-agnostic, and the five pure-CSS demos need no JavaScript anywhere. The state machine is where framework idiom matters, and modern React has purpose-built tools for it: useTransition gives you an isPending flag without managing a boolean yourself, and inside a form useFormStatus exposes the parent form's pending state to a nested submit button — which is exactly the case that used to require prop-drilling. Vue reaches for a ref plus onBeforeUnmount; Svelte a plain let and the cleanup returned from onMount. One trap is universal: clear the previous timeout before setting a new one, or a rapid re-click leaves a stale timer that flips the button back to idle while a second request is still running. For Tailwind the states map to data-[state=pending]: variants, which keeps the whole machine in markup.Related collections
15 CSS 3D Tilt Hover Cards
15 mouse-aware 3D tilt hover cards — e-commerce product spotlight, glassmorphism parallax team card, interactive pricing tier, holographic NFT collectible, pop-out mascot, dark tech grid with border glow, media player album art, blog article preview, cyberpunk neon glow, dashboard KPI widget, minimalist real estate, flip-to-back tilt, mobile app showcase, course learning card, and a Pure CSS touch-friendly tilt. Vanilla JS writes --rx/--ry/--mx/--my; all rendering stays in CSS on the GPU.
24 CSS Animated Cards
24 hand-coded CSS animated card patterns organised by what triggers the motion, not by card style. Scroll and entrance triggers: staggered grid reveals, @starting-style mount-in, scroll-driven scale and fade on animation-timeline: view(), View Transitions API card-to-detail morphs, blur-to-sharp lazy image loading, and FLIP re-layout when a filter changes. State-change triggers: add-to-cart success morph, like and save particle burst, expand and collapse to intrinsic height with calc-size(), swipe-to-dismiss on Pointer Events, and skeleton-to-loaded crossfade. Data-driven triggers: count-up KPI metrics, SVG sparkline draw-on-enter, and progress ring fill on load. Ambient idle loops: breathing glow, floating drift, animated gradient mesh backgrounds, conic rotating borders, auto-cycling testimonial decks, and live status pulses. Plus four hover patterns where the effect is the topic itself: cursor spotlight, holographic foil glare, corner ribbon slide, and horizontal accordion expand. Every card is scoped under a .ac-NN prefix for no-collision pasting, guards prefers-reduced-motion, animates compositor-only properties, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.
22 CSS Avatars
22 hand-coded CSS avatars for chat apps, team dashboards, comment threads, account menus, and social profiles. Covers circular and squircle shapes, gradient and conic story rings, hexagon clip-path crops, online status dots, notification count badges, verified checkmarks, stacked facepiles with plus-N overflow, initials fallbacks for users without photos, broken-image recovery, avatar pickers, and a size scale system where the ring, badge, and status dot all scale from a single custom property.


















