Free · Fast · Privacy-first

CSS Loading Spinner CSS

A CSS loading spinner is one of the most common UI elements in modern interfaces, appearing on submit buttons during form processing, inside content cards during data fetches, and as full-page overlays during initial application loads.

Size controls from 16px inline to 80px full-page

🔒

Colour picker for the active spinner arc

Adjustable stroke width and animation speed

Generates complete circle, border, and @keyframes rotate code

Cost
Free forever
Sign-up
Not required
Processing
In your browser
Privacy
Files stay local
FreeNo signupWhite-label

Add this Animation Builder to your website

Drop the Animation Builder into any page — blog post, product docs, intranet, school portal — with a single line of HTML. Your visitors get the full tool, processed entirely in their browser. No backend, no uploads, no signup.

  • Files stay 100% in the visitor's browser
  • Responsive — adapts to any container width
  • Free forever, no API key needed

Embed code

<iframe
  src="https://www.fixtools.io/css-tool/animation-builder?embed=1"
  width="100%"
  height="780"
  frameborder="0"
  style="border:0;border-radius:16px;max-width:900px;"
  title="Animation Builder by FixTools"
  loading="lazy"
  allow="clipboard-write"
></iframe>

Attribution-friendly: a small "Powered by FixTools" link appears in the embed footer.

How the CSS Border Trick Creates a Spinner and Why It Works at Any Size

A CSS spinner works through a combination of border-radius: 50%, which turns a square element with equal width and height into a circle, and the border transparency trick: apply a solid coloured border on one of the four sides and a low-opacity or transparent border on the other three sides, then rotate the entire circle infinitely using a CSS animation. Because the element is perfectly circular due to the border-radius, the single coloured border arc appears to move continuously around the circumference as the element rotates, creating the illusion of a spinning loading indicator. The rotate @keyframes rule is the simplest possible animation: transform: rotate(0deg) at the 0 percent stop and transform: rotate(360deg) at the 100 percent stop, with animation-timing-function set to linear so the spin speed is constant throughout each revolution. No JavaScript is needed at any point in this approach, and the spinner works even when scripts are blocked or still loading.

Sizing and colour choices determine how the spinner communicates context to the user and matches the visual weight of the surrounding interface. Small spinners at 16 to 20 pixels work inside buttons as inline loading indicators, replacing button text during an async submit or save action. Medium spinners at 32 to 48 pixels work inside card or section placeholders, indicating that content within a specific container is loading rather than the whole page. Large spinners at 64 to 80 pixels work as full-page loading overlays when the entire application state is being initialized or when a large data fetch is in progress. The coloured arc should contrast with both the page background and the surrounding element colour to remain visible across themes. The remaining three border sides should be set to a low-opacity version of the arc colour, typically 15 to 25 percent opacity, rather than fully transparent, so the circular track is subtly visible behind the moving arc.

Accessibility requires two additions beyond the visual CSS that take under a minute to add but make the loading state communicated to all users including those who cannot see the spinning arc. Add role="status" to the spinner element so screen readers announce that a loading state is in progress when the spinner appears in the DOM. Add aria-label="Loading" to give the announcement meaningful text content, since a bare spinner element with no inner text would otherwise be announced as an empty element by some screen readers. If the spinner is inside a region that will update with new content when loading completes, add aria-live="polite" to the container so screen readers also announce when the new content has arrived. Together these three attributes ensure the loading state is communicated through both visual and assistive channels.

Performance for an indefinite spinner depends on which CSS properties the animation touches. A rotate animation using transform: rotate() runs entirely on the GPU compositor thread, so even a spinner running continuously for thirty seconds has effectively zero ongoing performance cost on modern devices. The page can render other elements, accept clicks, and handle network responses while the spinner spins, with no impact on main thread work. Avoid implementing spinners using animations on background-color, box-shadow, or width, all of which trigger paint or layout on every frame and accumulate cost over long-running animations. The single border arc combined with transform: rotate() is the compositor-safe pattern, and it scales to dozens of simultaneous spinners on the same page without performance degradation.

How to use this tool

💡

Set the spinner diameter, choose the arc colour and background colour of the remaining border, then set animation duration. The preview spins with your settings applied. Copy the code when the spinner looks right.

How It Works

Step-by-step guide to css loading spinner css:

  1. 1

    Set spinner size

    Enter the diameter in pixels. Use 18px for button spinners, 40px for card-level placeholders, and 64px for full-page loading overlays.

  2. 2

    Choose arc and track colours

    Set the arc colour to the primary brand colour or a high-contrast indicator colour. Set the track border to the same colour at 15 to 20% opacity for a visible circular guide.

  3. 3

    Set stroke width and speed

    Set the border-width to 10 to 15% of the diameter for balanced proportions. Set animation-duration to 0.7s for fast, 1s for standard, and 1.5s for slow and deliberate.

  4. 4

    Copy the CSS and add ARIA attributes

    Click Copy Code and paste the spinner CSS into your stylesheet. Add role="status" and aria-label="Loading" to the HTML element for accessibility.

Real-world examples

Common situations where this approach makes a real difference:

Form submit button loading state

A checkout form developer replaces the Submit button text with a 20px spinner when the payment is processing. The button is disabled immediately on click to prevent duplicate submissions. The spinner inherits the button text colour, maintaining the button's visual style. On success or failure, the spinner is replaced with the appropriate message and the button is re-enabled or left disabled based on the outcome.

Dashboard card content placeholder

A SaaS analytics dashboard shows 40px spinners inside each metric card while the API data loads. Each card has a fixed minimum height set so the layout does not reflow when the spinner is replaced by the actual data. The spinners use the dashboard's primary blue on a light grey track, matching the brand palette. All spinners disappear together when the batch fetch resolves.

Full-page application loading screen

A single-page application shows a 64px spinner centred on a white overlay during the initial JavaScript bundle load and authentication check. The overlay uses position: fixed with a high z-index to sit above all content. Once the app state is ready, the overlay fades out with a 200ms opacity transition before being removed from the DOM so the transition is smooth rather than a hard cut.

Infinite scroll list loader

A content feed triggers a next-page fetch when the user scrolls near the bottom. A 32px spinner appears at the end of the list during the fetch. The spinner uses a muted colour so it does not visually compete with the content above it. On fetch completion, the spinner is removed and the new items are appended. If the fetch returns an empty array, the spinner is replaced with an "All items loaded" message.

When to use this guide

Use this when you need a pure CSS spinner for a button loading state, a full-page overlay, or an inline content placeholder, and want to skip writing the border and keyframe setup by hand.

Pro tips

Get better results with these expert suggestions:

1

Use a CSS custom property for spinner size to resize in one edit

Set --spinner-size on the element and use it for width, height, and border-width together. Resizing then requires one variable change. Without this pattern, adjusting spinner size means hunting across three separate property values in the declaration.

2

Set border-color with alpha for the track, not a lighter hue

Use the same colour as the active arc but at 15 to 20% opacity for the three non-active border sides. This ensures the track colour adjusts automatically if the arc colour changes, without needing a separate colour value to maintain. A separate light hue often drifts from the brand colour over time as the design evolves.

3

Lock button min-width before replacing text with a spinner

Read the button's offsetWidth in JavaScript and set it as a style.minWidth before swapping the text for a spinner. This prevents layout shift when the spinner is narrower than the original button label. Remove the inline style after the loading state ends.

4

Test spinner animation under CPU throttle to catch jank

Open DevTools Performance panel and set CPU slowdown to 6x. A CSS rotate animation should stay smooth since transform runs on the compositor thread. If the spinner stutters under throttle, check whether any layout-triggering styles are being applied to the spinner element or its parent during the loading state.

FAQ

Frequently asked questions

A CSS spinner uses three properties together. The border property applies a 3 or 4-sided border with one coloured side and three transparent or low-opacity sides. The border-radius: 50% property turns the element into a circle, making the borders appear as arcs. The animation property applies an infinite rotate @keyframes that spins the element continuously. These three declarations are the entire CSS foundation; the rest of the spinner code handles sizing and colour customization.
The CSS border approach is simpler to implement inline, requires no SVG syntax knowledge, and scales with the element size using em or percentage units. SVG spinners offer more control over the arc start and end positions and allow animated stroke-dashoffset effects for progress indicators that show partial completion. For a simple indefinite loading indicator, the CSS border approach is faster to write, easier to maintain, and produces a visually identical result in all modern browsers.
The standard approach uses a fixed-position overlay container with display: flex, align-items: center, and justify-content: center. Set the overlay to position: fixed with top: 0, left: 0, width: 100%, and height: 100%. Place the spinner element inside the overlay. For inline spinners inside a button or card, use position: absolute on the spinner and position: relative on the parent, combined with the standard centering transforms.
Use animation-timing-function: linear for a spinner. An ease or ease-in-out timing function makes the spinner visibly accelerate and decelerate each revolution, which looks like the spinner is struggling or dragging. Linear produces a constant, smooth rotation that communicates steady progress. This is one of the few cases in UI animation where linear is the correct choice, because the visual goal is uniform circular motion rather than a natural-feeling start and stop.
Use em units for the border-width instead of pixels, so the stroke thickness scales automatically with the font-size of the spinner container. Set the width and height in the parent selector using a single CSS custom property: --spinner-size: 40px. Reference that variable for width, height, and as a base for border-width calculations. Changing one value then resizes the spinner uniformly. This approach is easier to maintain than managing three separate pixel values across the spinner declaration.
Yes. Define one @keyframes spin rule at the stylesheet level and apply different animation-duration values to each spinner element. Each spinner references the same @keyframes name but plays at its own speed. Alternatively, use a CSS custom property for the duration: --spin-duration: 1s on each element selector, and reference it in the animation shorthand as animation: spin var(--spin-duration) linear infinite. Changing the duration for one spinner then requires only updating the custom property value on that element.
The standard pattern: set the spinner element to display: none by default. Before the fetch call, set display: block (or add a visible class). In the fetch .then() and .catch() handlers, set display: none again (or remove the class). Avoid removing the element from the DOM before the animation completes if you plan to reuse it. If the spinner is inside a button, also set the button to disabled: true during the fetch to prevent duplicate submissions while the spinner is visible.
Replace the button text content with the spinner element when the form is submitted. The spinner inherits the button colour context naturally. Disable the button to prevent re-submission. After the async action resolves, restore the original text content and re-enable the button. The button width will shift if the spinner is narrower than the text; prevent this by setting a fixed min-width on the button before the submit, which locks the button width regardless of its content during the loading state. For consistency across a product, define a reusable button-loading utility class that handles min-width, opacity, cursor, and the spinner swap together rather than rebuilding the pattern in each form.
For known progress (file upload, multi-step process), switch from the rotating border trick to an SVG circle with stroke-dasharray and stroke-dashoffset. Calculate the circumference as 2 * Math.PI * radius and set stroke-dasharray to that value. Update stroke-dashoffset based on percent complete: offset = circumference * (1 - percent / 100). Combine with a slow rotation animation on the parent for a polished progress ring that doubles as a spinner when progress is uncertain. This pattern is common in upload widgets and onboarding checklists where the percentage is meaningful, and it gracefully degrades to the indefinite spin when progress data is unavailable.
At sizes below 20px, the border-radius circle approximation can show subpixel rounding artifacts depending on the device pixel ratio and the browser rendering pipeline. Three fixes help. First, set the border-width to a value that divides cleanly into the diameter, such as 2px on a 16px spinner rather than 3px. Second, add transform: translateZ(0) to the spinner element to force GPU compositing, which often improves edge smoothing. Third, switch to an SVG spinner at very small sizes since SVG anti-aliasing handles tiny circles more reliably than CSS border rendering, especially on Windows Chrome at non-integer DPI scaling factors like 125 or 150 percent.

Related guides

More use-case guides for the same tool:

Ready to get started?

Open the full Animation Builder — free, no account needed, works on any device.

Open Animation Builder →

Free · No account needed · Works on any device