LoadersFree

Classic Spinner

Div 50 × 50 px, radius 50%, 4 px border: top side #6366f1 opaque, three sides at 20% — @keyframes spin 1 s linear, no pseudo-element, no JS.

CSSMinimal
Hover or click the scene to interact

This effect is free — the Loaders category contains 29 effects total, including 4 free. Effect.Labs has 811 vanilla effects. Explore the category →

Usage examples

Syncing mailbox…
1–50 of 1,284
Nora LemaireSaturday's photos10:12
AccountingInvoice 2026-11809:27
Julien BarbierReview before ThursdayYesterday
Theo MarchandMonday weekly check-inYesterday
Labo WeeklyIdeas for the autumnMon
Atlas BookshopOrder on its wayMon

① Mailbox toolbar — the spinner takes the sync icon's slot

WhenEmail client: the user triggers a refresh, or a periodic sync starts. In the toolbar, the sync icon gives way to the spinner in the very same 24 px footprint — and nothing else moves: no label, no overlay, no banner.
WhyThis spinner is nothing but a border on a div: scaled to 24 × 24 px with box-sizing: border-box, it fills exactly the slot of the icon it replaces, with no wrapper or pseudo-element to fit into the toolbar. The 20% ring keeps the icon's full silhouette while the arc turns — the toolbar never "breathes", which a dots or bars loader cannot guarantee since its silhouette changes over the cycle. And a turning arc is already the "syncing" sign everyone reads without a caption: that is what makes a silent swap possible.
Settingswidth: 24px; height: 24px; box-sizing: border-box (the icon's exact footprint) and border-width: 3px — one step above the 8% ratio in the customization table, so the arc stays legible at this size. 1s duration and colors unchanged. Add role="status" and a visually hidden label on the host button, which keeps its active-state background.

② Print queue — "Sending to Office Printer…" in a system dialog

WhenA desktop app (or an app's webview) hands a document to the print spooler: a system dialog shows the printer, the document and a Cancel button until the queue acknowledges the job.
WhyA system dialog is a neutral register: a spinner is expected there, a stylized loader would clash. This effect has no gradient (unlike Gradient Ring, fx-0435, whose arc fades through a conic-gradient), no glow, no color shift — a sharp-edged arc on a faint ring, rotating linearly at 1 s, exactly the grammar of native activity indicators. Among the loaders, it is the one that blends into the system's look instead of stamping a signature on it.
SettingsCode settings kept as is: 50 × 50 px, border-width: 4px, 1s linear. On the dialog's grey (#f7f7f9), the #6366f1 arc measures 4.2:1 — above the 3:1 threshold for a graphical component (WCAG 1.4.11); the 20% ring stays perceptible. Place it with flex: none to the left of the text, vertically centered.

Two-step verification

Code sent by text to +33 6 •• •• •• 47

Verifying code…

③ One-time code check (2FA) — spinner below the 6 boxes during server validation

WhenTwo-step sign-in screen: the sixth box is filled and the form submits on its own. The spinner appears below the field while the server checks the code — 200 ms to 2 s, with no predictable duration.
WhyThe check is short and binary: no progress to show (a bar would lie), no duration to suggest. This spinner runs linear infinite with a single keyframe stop: it has no privileged phase, hence no "start" or "end" of cycle. It can appear and vanish after 300 ms without looking cut off — whereas a pulse loader (fx-0423, 2 s cycles) or sequential dots, removed mid-cycle, look interrupted. And the field stays put: the spinner takes the button's row, not the code's.
Settingswidth: 36px; height: 36px; border-width: 3px (≈ 8% of the size, as the customization table recommends), 1s kept. Center it in a fixed-height row in place of the button (height: 44px; display: flex; align-items: center; justify-content: center) so that showing and removing it shifts nothing. role="status" plus a visually hidden "Verifying code…" label.

How it works

The effect is a single <div class="loader-spinner"> with no children and no pseudo-elements. border-radius: 50% on a 50 × 50 px square (content area, default box-sizing: content-box) draws the circle; the 4 px border carries the colors. Total rendered box is therefore 58 × 58 px (50 + 2 × 4). No image, no SVG, no script runs.

The visible arc emerges from the opacity distribution across the four border sides. The rule border-color: rgb(99, 102, 241) rgba(99, 102, 241, 0.2) rgba(99, 102, 241, 0.2) uses the three-value shorthand (top / right+left / bottom): the top side is fully opaque, the three others share the same hue at 20%. The arc is not clipped or masked — only the opacity difference reveals it. border-image: none is set explicitly to cancel any inherited cascade.

@keyframes spin contains only one stop: 100% { transform: rotate(360deg) }. The 0% stop is implicit — the browser starts from the element's computed value at rest, i.e. the identity (rotate(0deg)). The full declaration animation: 1s linear 0s infinite normal none running spin sets: 1 s duration, linear easing, zero delay, infinite loop. One full revolution per second, no acceleration. The js key in the sold file is a comment (// Effet CSS pur — pas de JavaScript nécessaire): nothing runs.

Accessibility

  • prefers-reduced-motion not handled (measured): under emulated @media (prefers-reduced-motion: reduce), animationPlayState stays running — the rotation continues at full speed. Add @media (prefers-reduced-motion: reduce) { .loader-spinner { animation-duration: 8s; } } to slow it down, or animation: none if a static circle is enough to indicate waiting.
  • No ARIA attributes (measured): role, aria-label, aria-live and aria-busy are all absent — a screen reader ignores the element entirely. For a loading indicator, add role='status' aria-live='polite' and a visually hidden label: <span class='sr-only'>Loading…</span> inside the div.
  • Contrast: the opaque arc #6366f1 on background #0a0a0f yields 5.1:1 (WCAG 1.4.11 threshold 3:1, compliant). On a white background: 4.5:1, also compliant. The 20% portion is decorative — no contrast requirement applies to it.
  • Pointer events: no pointer-events: none rule is present. If the spinner is overlaid on interactive content in a blocking loading overlay, add pointer-events: none to the spinner or cursor: wait to the parent container.

Browser compatibility

Pure CSS: border-radius, border-color with rgba, @keyframes, animation, transform: rotate(). No design system CSS variables, no JS, no dependencies.

Chrome 43+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 13+✓ Full
IE 11+✓ Full
Mobile iOS / Android✓ Full

On IE 10, @keyframes and animation require the -ms- prefix — without it, the div renders as a static 58 px circle with the colored arc. On IE 9 and earlier, the same static result. The page remains functional in both cases.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="loader-spinner">
</div>
CSS
.loader-spinner  {
   width: 50px;
   height: 50px;
   border-width: 4px;
   border-style: solid;
   border-color: rgb(99, 102, 241) rgba(99, 102, 241, 0.2) rgba(99, 102, 241, 0.2);
   border-image: none;
   border-radius: 50%;
   animation: 1s linear 0s infinite normal none running spin;
   }

@keyframes spin  {
   
  100%  {
   transform: rotate(360deg);
   }
}
JavaScript (fx-0421)
// Effet CSS pur — pas de JavaScript nécessaire

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Dimensions (CSS — width and height) 50px Content area; rendered box = value + 2 × border-width. At 20 px with 2 px border: total box 24 px.
Arc thickness (CSS — border-width) 4px Keep a ratio of ≈ 8% of the size: 30 px → 2–3 px, 80 px → 6–7 px. Too thin: arc invisible; too thick: the opaque surface takes up more than a quarter of the circle and the arc disappears into a mass.
Arc color (CSS — first term of border-color) rgb(99, 102, 241) Hue of the top side (the visible arc). The three other sides use the same hue at 20% — changing this first term modifies all four sides at once.
Ghost ring opacity (CSS — alpha value in rgba) 0.2 0 = three sides invisible, the arc spins on a bare background. 0.5 = clearly visible ring, stronger contrast. 1 = solid full disc spinning with no visible arc.
Revolution duration (CSS — 1s in animation) 1s 0.6 s: fast, sense of urgency. 1.5 s: slow, implies a long operation. Going below 0.4 s: the arc looks unstable.
Timing function (CSS — linear in animation) linear ease-in-out creates an illusion of deceleration at each turn start; the rotation feels lively but less mechanical. linear is the standard choice for a spinner.
box-sizing (CSS — absent, inherited content-box) content-box (inherited) Adding box-sizing: border-box: width: 50px then includes the border, rendered box = 50 px instead of 58 px. Useful to fit inside a fixed-size container.

FAQ

Why three values in border-color rather than four?

The border-color property with three values follows the rule top / right+left / bottom: the second value applies to both right and left sides simultaneously. Four values (rgb(99,102,241) rgba(…) rgba(…) rgba(…)) would produce the same result; three terms avoid the repetition. The top side stays fully opaque, the three others at 20%.

What is the difference from Gradient Ring (fx-0435), which also produces a spinning arc?

Gradient Ring draws its arc using conic-gradient(transparent, #6366f1) on the div background and hollows the center with a ::before. The arc fades gradually from transparent to opaque. Classic Spinner has no pseudo-elements and produces a sharp-edged arc through border-color. If your component already uses ::before and ::after for an icon or badge, Classic Spinner integrates without conflict.

The spinner appears squashed or oversized inside my component.

The most common cause is box-sizing: border-box inherited from a CSS framework — the box shrinks from 58 px to 50 px (the circle stays round, just smaller). If the spinner is a direct child of a display: flex container with align-items: stretch, it may stretch; adding flex-shrink: 0; align-self: center preserves its dimensions. Wrapping the spinner in a neutral container (display: flex; align-items: center; justify-content: center) addresses both cases.