Circular Progress
SVG arc 60 px, stroke-dashoffset 157→0→−157 over 2 s ease-in-out: the 4 px arc fills then drains in a continuous loop on a 20%-opacity track. Zero JS.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Loaders has 29 effects, including 4 free. Explore the category →
3 usage examples



How it works
The effect is a <svg viewBox="0 0 60 60"> of 60×60 px containing two stacked <circle> elements, cx="30" cy="30" r="25". Both share fill: none, stroke-width: 4 and stroke-linecap: round. The first, .bg, gets stroke: rgba(99,102,241,.2) — an indigo track at 20% opacity. The second, .progress, gets stroke: #6366f1 and the animation. The circumference 2π×25 ≈ 157.08 px is rounded to 157 in the code.
.progress starts with stroke-dasharray: 157 (a single value: dash and gap are each 157 px) and stroke-dashoffset: 157, which hides the arc entirely. The circularProgress animation — 2s ease-in-out infinite — cycles through three states: 0% (offset 157, arc invisible), 50% (offset 0, full circle), 100% (offset −157, arc invisible again). From 0% to 50%, the dash tail catches up toward 12 o'clock: the arc grows clockwise from 12 o'clock. From 50% to 100%, the dash head advances clockwise: the arc drains from 12 o'clock. Measured: offset 156.5 px at t≈50 ms, 7.2 px at t≈1,000 ms (almost full), −118 px at t≈2,000 ms (three-quarters drained). The 100%→0% wrap is invisible: offset −157 and offset 157 render identically empty.
transform: rotate(-90deg) with transform-origin: center center shifts the stroke's start point from 3 o'clock (SVG default) to 12 o'clock, making the fill naturally top-first. The animation is entirely CSS — the sold JS is empty. Colors are hard-coded (#6366f1, rgba(99,102,241,.2)): no site CSS variable is required.
Accessibility
- prefers-reduced-motion not handled: the animation loops indefinitely regardless of the user's preference. Add
@media (prefers-reduced-motion: reduce) { .loader-circular-progress .progress { animation: none } }to stop the arc; a static full arc (offset 0) is an honest fallback. - The element is a silent
<svg>with no role or alternative text: no screen reader will announce the loading state. Addrole="status"to the<svg>with a hidden<title>(Loading), or a sibling element witharia-live="polite"that announces when the operation completes. aria-busyis not set on the loading content container. Applyaria-busy="true"to the affected region during loading and remove it when done.- Contrast:
#6366f1on#0a0a0fmeasures 3.0:1 (WCAG 1.4.11 threshold for graphic elements: 3:1) — borderline acceptable on a dark background, insufficient on a light one (1.9:1 on white). On a light background, use#4f46e5(contrast ≥ 4.5:1).
Browser compatibility
SVG stroke-dashoffset animated via CSS. No JS dependency, no external CSS variable.
Without CSS animations (very old browsers): static arc at its initial offset of 157 — invisible. Add stroke-dashoffset: 0 inside @supports not (animation-name: x) {} to display a static full arc instead.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<svg class="loader-circular-progress" viewBox="0 0 60 60">
<circle class="bg" cx="30" cy="30" r="25"></circle>
<circle class="progress" cx="30" cy="30" r="25"></circle>
</svg>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Size (width / height on .loader-circular-progress) |
60 px | Resizes the whole element. If you also change the r attribute, recalculate stroke-dasharray and all three 157 values in the animation: for r=12, circumference ≈ 75. |
Radius (r attribute on both <circle> elements) |
25 | Must stay below 30 to fit inside the 60×60 viewBox with a 4 px stroke. Any change requires recalculating stroke-dasharray = 2π×r. |
Stroke width (stroke-width) |
4 | 2 px for a fine look (mobile, tight spaces); 6 px for a bold appearance. Applies to both circles. |
Progress color (stroke: #6366f1 on .progress) |
#6366f1 | Color of the animated arc. Mirror the same hue in the track: .bg { stroke: rgba(R,G,B,.2) }. |
Track opacity (.bg { stroke: rgba(99,102,241,.2) }) |
0.2 (20%) | 0 to remove the track; 0.35 for a more prominent guide rail that improves readability on light backgrounds. |
Cycle duration (animation: 2s) |
2 s | 1 s: conveys urgency; 3 s: long, smooth wait. Change in the animation declaration on .progress. |
Easing (ease-in-out) |
ease-in-out | linear for constant speed (download feel); ease-in for acceleration toward the end (sense of completion). |
FAQ
stroke-dashoffset loops endlessly between 157 and −157 — it receives no dynamic value. For a true 0-to-100% indicator, remove the animation, add transition: stroke-dashoffset 0.4s ease and in JS: circle.style.strokeDashoffset = 157 * (1 - progress / 100). At progress = 100, offset = 0 = full circle.<div> with conic-gradient and border-radius: 50% that spins continuously — the ring is always full, it only rotates. Circular Progress is an <svg> whose arc fills then drains via stroke-dashoffset — the arc grows and disappears in a loop. Gradient Ring conveys an indefinite wait through rotation; Circular Progress simulates a restarting progression, which suits short repeated operations (form submissions, data refreshes) and can be turned into a real progress bar by simply removing the animation (see above).stroke-dashoffset = −157 renders identically to 0% (stroke-dashoffset = 157): the arc is invisible in both cases. The wrap is seamless, as confirmed by measurement (offset 156.5 px at t≈50 ms = nearly empty at the very start of each new cycle).