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.
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
Sending to Office Printer…
September-report.pdf · 3 pages · Two-sided
Two-step verification
Code sent by text to +33 6 •• •• •• 47
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),animationPlayStatestaysrunning— the rotation continues at full speed. Add@media (prefers-reduced-motion: reduce) { .loader-spinner { animation-duration: 8s; } }to slow it down, oranimation: noneif a static circle is enough to indicate waiting. - No ARIA attributes (measured):
role,aria-label,aria-liveandaria-busyare all absent — a screen reader ignores the element entirely. For a loading indicator, addrole='status' aria-live='polite'and a visually hidden label:<span class='sr-only'>Loading…</span>inside the div. - Contrast: the opaque arc
#6366f1on background#0a0a0fyields 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: nonerule is present. If the spinner is overlaid on interactive content in a blocking loading overlay, addpointer-events: noneto the spinner orcursor: waitto 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.
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.
<div class="loader-spinner">
</div>
.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);
}
}
// Effet CSS pur — pas de JavaScript nécessaire
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
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.