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.
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



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. Addrole="progressbar",aria-valuenow="80",aria-valuemin="0",aria-valuemax="100"and anaria-labelon.se-progress, andaria-hidden="true"on the SVG. The scenes on this page put arole="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
#0a0a0fand 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-primaryon.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).
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):
<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>
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 / property | Default | Effect |
|---|---|---|
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
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.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.--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.