Loaders✨ Premium

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.

SVGProgress

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

Uploading a video file — the percentage sits inside the arc — Circular Progress example 1

① Uploading a video file — the percentage sits inside the arc

WhenA media library or file-sharing tool receives a heavy file (a 940 MB 4K video): the browser knows how much has already been sent (the upload's progress event) and displays it for the 30 to 90 s the transfer takes, next to the throughput and the time left.
WhyIt is the only loader in the set that can display a value: the arc traces the share already sent and leaves its centre empty for the number — “63%” and an arc filled to 63% occupy the same point of gaze, something no spinning spinner or row of dots can offer. The clockwise fill from 12 o'clock (rotate(-90deg)) is the universal visual code for a download, and at 120 px the 8 px stroke stays legible from a distance, where a 20 px spinner in a button no longer reads.
Settingswidth: 120px; height: 120px on .loader-circular-progress (radius 25 unchanged: stroke-dasharray: 157 and stroke-width: 4 kept, i.e. an 8 px stroke on screen); determinate mode from the FAQ: animation: none on .progress, transition: stroke-dashoffset .4s ease, and in JS on each progress event: circle.style.strokeDashoffset = 157 * (1 - percent / 100); the <svg> lives in a 120 px position: relative wrapper and the percentage is centred over it (position: absolute; inset: 0; display: flex; align-items: center; justify-content: center); original colours (#6366f1, 20% track).
PDF export of a 12-page report — “Page 3 of 12” — Circular Progress example 2

② PDF export of a 12-page report — “Page 3 of 12”

WhenA document editor renders the high-definition PDF of a long report (vector charts, 300 dpi images): the export advances page by page, each rendered page is confirmed to the client, and the whole job takes 15 to 30 s — too long for a plain spinner, too short for a job queue.
WhyThe export is a sequence of discrete steps: each completed page advances the arc by one twelfth of a turn, and the share already done reads geometrically — an arc reaching 6 o'clock says “halfway” before anyone reads the text. A spinning spinner could never say nine pages remain; a linear bar could, but without a free centre for the percentage or the compactness of a 120 px disc in a narrow panel. The rounded caps (stroke-linecap: round) soften each 8.3% step, and the transition smooths the jumps between pages.
Settingswidth: 120px; height: 120px; stroke-width: 3 (viewBox units: 6 px on screen, finer than the default stroke in a busy panel); animation: none on .progress + transition: stroke-dashoffset .4s ease; in JS, on each finished page: circle.style.strokeDashoffset = 157 * (1 - pagesDone / 12) (steps of 157/12 ≈ 13.1 px); radius 25 and stroke-dasharray: 157 unchanged; the “Page N of 12” label under the arc and the percentage in the centre share the same counter; original colours.
Widget refresh — card in a dashboard — Circular Progress example 3

③ Widget refresh — card in a dashboard

WhenA dashboard widget (chart, KPI) reloads after a filter change; the indicator is centered over the data area with a semi-transparent overlay.
WhyThe two-layer design (track + animated arc) clearly signals loading without fully hiding the partial data still visible below. The 2 s cycle is noticeable without being anxious for a typical 1–3 s query.
Settingsposition: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%) over an overlay with background: rgba(10,10,15,.7) covering the widget; stroke-width: 3 for a lighter look in a dense interface.

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. Add role="status" to the <svg> with a hidden <title> (Loading), or a sibling element with aria-live="polite" that announces when the operation completes.
  • aria-busy is not set on the loading content container. Apply aria-busy="true" to the affected region during loading and remove it when done.
  • Contrast: #6366f1 on #0a0a0f measures 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.

Chrome 53+✓ Full
Firefox 49+✓ Full
Safari 9.1+✓ Full
Edge 15+✓ Full
Mobile iOS 9.3+✓ Full
Android Chrome 53+✓ Full

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):

index.html — structure
<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>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
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

Indeterminate. The 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.
Gradient Ring is a <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).
No. At 100%, 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).