Glassmorphism✨ Premium

Superellipse Progress Ring

Three 110 px progress rings drawn as superellipses: on hover the path shifts in 0.8 s from a slightly squarish round (exponent 2.5) to a soft-cornered square (exponent 5), like an app icon, with a glow in the ring's color — and the arc keeps its exact percentage while the shape changes.

SVGAnimationProgress

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Glassmorphism has 47 effects, including 6 free. Explore the category →

3 usage examples

Knee rehabilitation goals — a physiotherapy app — Superellipse Progress Ring example 1

① Knee rehabilitation goals — a physiotherapy app

WhenIn week 4, the patient sees three rings: knee flexion (115° of 130°, 88%), home exercises (9 of 12, 75%) and daily walking (4,200 of 6,000 steps, 70%).
WhyThe rings give progress at a glance, with no numbers to compare. The superelliptic shape, close to phone icons, makes the screen familiar; on hover the ring “squares up” and lights, showing which one is being read.
SettingsOriginal colors (purple #8b5cf6, cyan #06b6d4, green #10b981), --dash-offset at 38.4, 80 and 96 (320 × (1 − share)). Translated labels; 156 px columns with the detail under each ring.
Free spaces in park & ride car parks — a city app — Superellipse Progress Ring example 2

② Free spaces in park & ride car parks — a city app

WhenBefore leaving, the driver compares three park & ride car parks: 82%, 97% and 45% full, with the number of free spaces under each.
WhyOne ring per car park, filled by occupancy: the comparison is visual, and the color tells the decision (green, space left; red, don't bother). The number of free spaces, large under the ring, doubles the information for those who cannot tell the colors apart.
SettingsColors switched to a fill code: orange #f59e0b, red #ef4444, green #22c55e (stroke and --ring-color, so the glow follows); --dash-offset 57.6, 9.6 and 176. City-map background in dark gradients.
Audiobook progress — a spoken-word library app — Superellipse Progress Ring example 3

③ Audiobook progress — a spoken-word library app

WhenThe listener sees where they are in their three current books: 62%, 18% and 91%, with chapter and time left.
WhyThe superelliptic ring matches the square cover thumbnails: on hover it almost takes the shape of the cover it surrounds. The fill around the cover reads as “left to listen”.
SettingsThe central label holds the cover (58 px, 14 px radius) instead of the percentage, moved underneath; colors taken from the covers (#f97316, #14b8a6, #e879f9); --dash-offset 121.6, 262.4 and 28.8.

How it works

Each .se-progress (110 × 110 px) holds an SVG viewBox="0 0 110 110" rotated −90° so the arc starts at twelve o'clock, and two paths: the track .se-bg (6 px stroke, white at 8%) and the arc .se-fill (6 px, round caps, color in style). The arc is a dash: stroke-dasharray: var(--dash-total) (320) and stroke-dashoffset: var(--dash-offset). With pathLength="320" on the arc, the path counts as 320 long whatever its shape: the filled share is therefore (320 − offset) ÷ 320 — 64 gives 80%, 176 gives 45%. In the center, .se-label shows the value at 20.8 px weight 800 and an 8.8 px capitalized label at 60% opacity.

The shape comes from JavaScript: superellipsePath(cx, cy, rx, ry, n, segments) computes 72 segments of the curve |x|^n + |y|^n = 1. Exponent 2: a circle; 2.5: a slightly squarish round (the resting shape); 5: a square with very rounded corners, close to an app icon (the hover shape). On load the script sets shape 2.5 on every ring; mouseenter switches to 5, mouseleave back to 2.5. The CSS transition: d .8s ease does the morph: measured in Chromium 151 and Firefox 153, the real path length goes from 320.7 to 352.5 in 0.8 s (325, 333, 341, 346, 350, 351, 352 in 100 ms steps), without a jump. On hover the arc also gets drop-shadow(0 0 6px var(--ring-color)). Changing --dash-offset in JavaScript animates the fill over 1 s with an overshoot of about 10% of the travel (cubic-bezier(.34, 1.56, .64, 1): measured, from 64 to 0 the offset dips to −6 before settling at 0).

Three settings keep the morph and the value right. The script sets the resting shape on load, in 72 segments like the hover shape: a ring drawn with Bézier curves would not interpolate towards 72 straight segments, and the first hover would jump at once (307.8 → 352.5, measured), with an arc starting at nine o'clock instead of twelve. pathLength="320" pins the dash scale: the paths are 307.8 to 352.5 long depending on the shape, and without that attribute the “80%” ring would show 83% at rest and 73% on hover. No decorative animation on the <svg>: varying its border-radius would change nothing, as it has neither background nor border.

Accessibility

  • Screen readers: the SVG has no text and the label is a plain div (“80% Storage”): the value is read, but nothing says it is a progress. Add role="progressbar", aria-valuenow="80", aria-valuemin="0", aria-valuemax="100" and an aria-label on .se-progress, and aria-hidden="true" on the SVG. The scenes on this page put a role="img" and a full sentence on each column.
  • Keyboard: the rings are not focusable (measured: Tab does not stop on them) and the morph is only triggered by the mouse. As it carries no information — the value is the same in both shapes — nothing is lost with the keyboard. If the rings become clickable, make them links or buttons and call the same shape change on focus / blur.
  • Contrast: the white value measures 19.8:1 on #0a0a0f and the 60% label 7.3:1. The purple (4.7:1), cyan (8.1:1) and green (7.8:1) arcs exceed the 3:1 required for graphics; the 8% track (1.2:1) is only a guide. The effect is designed for a dark background: on a light one the white label disappears — then set a dark --text-primary on .demo-preview.
  • Motion: a 0.8 s morph on hover and a 1 s fill when the value changes, no loop. To honor the preference: @media (prefers-reduced-motion: reduce) { .se-progress .se-fill { transition: none } } — shape and value then change at once.

Browser compatibility

SVG, the CSS d property and its transition, pathLength, stroke-dasharray/stroke-dashoffset, CSS variables, filter: drop-shadow(). JavaScript: Math.sign, Math.pow, querySelectorAll().forEach, no library. Measured pasted into a blank page: Chromium 151 and Firefox 153 interpolate the path (progressive morph over 0.8 s); WebKit 26.5 (Safari) does not know the CSS d property (getComputedStyle(path).d empty): the path does change shape on hover, but at once, with no transition. In all three engines the filled share stays right (pathLength) and the animated fill works (same overshoot curve, measured).

Chrome 52+✓ Full
Firefox 97+✓ Full
Safari (all versions)✓ Partial: the shape changes on hover, without a progressive morph
Edge 79+✓ Full
Mobile iOS✓ Partial: no hover, so no morph under the finger; rings and values right
Android Chrome✓ Full with a mouse; under the finger, the shape change depends on emulated mouse events (not measured)

Without a d transition (Safari, Firefox before 97), the shape jumps straight from 2.5 to 5: the ring stays right, only the glide disappears. Without JavaScript, the rings keep the HTML's Bézier curves (round shape), the arc at the right value thanks to pathLength, but no morph. For a progressive morph everywhere, the exponent would have to be animated in JavaScript (requestAnimationFrame, recomputing the path each frame) instead of the CSS transition.

The code

HTML structure to paste into your page (the CSS + JS come with Premium):

index.html — structure
<div class="demo-preview">
  <div class="se-progress-wrap">
    <div class="se-progress" style="--ring-color: #8b5cf6; --dash-total: 320; --dash-offset: 64;">
      <svg viewBox="0 0 110 110">
        <path class="se-bg" d="…"></path>
        <path class="se-fill" pathLength="320" style="stroke: #8b5cf6;" d="…"></path>
      </svg>
      <div class="se-label">80%<small>…</small></div>
    </div>
    …
  </div>
</div>
Unlock the full code — $9.90/mo incl. tax

The full HTML + CSS + JS of Superellipse Progress Ring, ready to paste — plus all 840 effects. Or $299 once, lifetime access.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Value (HTML — --dash-offset in the style of .se-progress) 64 · 176 · 96 (80%, 45%, 70%) --dash-offset = 320 × (1 − share). Changed in JavaScript (el.style.setProperty('--dash-offset', v)), it animates the fill over 1 s with a small overshoot. Remember to change the .se-label text too.
Resting and hover shapes (JS — both morph(2.5) and morph(5)) exponent 2.5 at rest, 5 on hover 2 gives a perfect circle at rest (maximum contrast with hover); 4 or 5 at rest gives rings that are already square, like icons. Beyond 8, corners become almost sharp.
Morph duration (CSS — transition: … d .8s) 0.8 s, ease 0.4 s for a snappy response. No effect in Safari, which does not animate the d property.
Color (HTML — style="stroke: …" on the arc and --ring-color on .se-progress) #8b5cf6 · #06b6d4 · #10b981 Change both together: --ring-color colors the hover glow. Aim for 3:1 against the background.
Thickness and size (CSS — stroke-width: 6, .se-progress { width/height: 110px }) 6 on 110 px The SVG scales with its box; the stroke grows in proportion. Paths are computed with a radius of 49 in a 110 box: 6 units of margin remain, so a stroke over 12 is clipped by the SVG edge — then reduce the radius (both 49s of the superellipsePath call).
Path precision (JS — 6th argument of superellipsePath) 72 segments Resting and hover shapes must keep the same number of segments, otherwise the transition turns abrupt again. 48 is enough for 80 px; 120 for a 300 px ring.

FAQ

Because the morph relies on transitioning the CSS d property (a <path>'s geometry), which Safari does not support: measured in WebKit 26.5, getComputedStyle(path).d is empty and the path length jumps from 320.7 to 352.5 at once. Shape and value stay right; only the glide is missing. Chrome, Edge and Firefox (since 97) interpolate the path. For an identical result everywhere, animate the exponent in JavaScript frame by frame.
Yes: pathLength="320" tells the browser the path measures 320, whatever its real length (307.8 to 352.5 depending on the shape). The 320 dash therefore always represents 100%, and an offset of 64 always 80%. Without that attribute, the 80% ring would show 83% at rest and 73% on hover.
Start with --dash-offset: 320 (empty ring) in the HTML, then set the real value after the first paint, with two nested requestAnimationFrames: requestAnimationFrame(() => requestAnimationFrame(() => el.style.setProperty('--dash-offset', 64))). With only one, Firefox applies the final value directly, with no transition (measured). The sold code's transition (1 s, cubic-bezier(.34, 1.56, .64, 1)) then fills the arc, overshooting by about 10% of the travel before settling — measured in all three engines: 320 → 39 → 64.