Squircle Image Masks
Four 90 px portraits masked as an n = 4 superellipse that, on hover, really tightens towards n = 5 over 0.6 s (two 64-point clip-path: polygon() interpolated by the browser), while a colored ring and the first name appear: a team shown without a caption under each face.
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 .sq-mask (90 × 90 px) holds an image .sq-mask__img — in the sold code, a placeholder SVG portrait: gradient background, head and bust in 30% white —, a ring .sq-mask__ring and a first name .sq-mask__name. The image is cut by clip-path: var(--sqm-squircle), an n = 4 superellipse of 64 points in percentages. On hover it switches to var(--sqm-squircle-tight), n = 5, with transition: clip-path 0.6s cubic-bezier(0.34, 1.56, 0.64, 1): both polygons have 64 points in the same order, so the browser interpolates them. At the 45° point, the edge moves closer to the corner, from 7.2 to 5.8 px on each axis: the mask “tightens” towards a square. Measured during the transition, at 120 ms: 31.5% for a point going from 34.3% to 30.3%, in Chromium 151, Firefox 153 and WebKit 26.
At the same time, over 0.5 s: the block grows 10% (scale(1.1), with a bounce), the ring — 4 px around the image, 2 px border, border-radius: 28% — goes from opacity 0 and scale(0.9) to 1, in the color of its position (violet, pink, cyan, green by :nth-child); the first name (9.6 px, weight 600, #a1a1aa) appears 18 px below the portrait over 0.3 s. The ring is not clipped: it is a sibling of the image, it stays whole around the mask. No JavaScript.
The two masks, --sqm-squircle and --sqm-squircle-tight, are CSS variables with the same structure, with no SVG <clipPath>: that is what lets the hover “tightening” exist. Two url(#…) pointing to missing SVGs would leave square portraits, and even when present they would not interpolate into each other. The placeholder portraits' gradients carry prefixed ids, sqm-grad-1 to 4, so they never clash with a generic id on your page.
Accessibility
- The first name only appears on hover: with the keyboard it never shows — the
.sq-maskblocks aredivs with notabindex(measured: Tab never stops on them). It stays in the DOM atopacity: 0, so screen readers read it. If the portraits lead to a profile, make.sq-maska link and add.sq-mask:focus-visible .sq-mask__name { opacity: 1 }plus the same rule for the ring, which then serves as the focus indicator (it is not clipped). - Images: replace the demo SVGs with
<img class="sq-mask__img" src="…" alt="Dr Hélène Marchal, GP">—object-fit: coveris already in the CSS andclip-pathapplies to the image. The code's SVGs have noroleor title: if they stay decorative, addaria-hidden="true". - First-name contrast:
#a1a1aaat 9.6 px is 7.7:1 on the dark#0a0a0fbackground, but 2.6:1 on a white page — and the size is small. On a light background, set.sq-mask__nameto#334155and 12 px. The ring, the only carrier of color, must not be the only way to read a profession: double it with a word. Noprefers-reduced-motion: the 10% grow stays active under the preference (measured).
Browser compatibility
CSS only: clip-path: polygon() on an <svg> (or <img>) element, a clip-path transition between two 64-point polygons, custom properties, transform, opacity. Zero JavaScript, no external image. Mask and tightening measured in Chromium 151, Firefox 153 and WebKit 26.
Without clip-path (Firefox before 54, Safari before 13.1 without prefix), the portraits are square; the ring, the grow and the first name still work.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="demo-preview">
<div class="sq-masks-wrap">
<div class="sq-mask">
<svg class="sq-mask__img" viewBox="0 0 90 90">…</svg>
<span class="sq-mask__ring"></span>
<span class="sq-mask__name">…</span>
</div>
<div class="sq-mask">…</div>
<div class="sq-mask">…</div>
<div class="sq-mask">…</div>
</div>
</div>
The full HTML + CSS + JS of Squircle Image Masks, 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 |
|---|---|---|
Start and hover shapes (CSS — --sqm-squircle, --sqm-squircle-tight) |
n = 4, then n = 5 on hover | For a more visible tightening, replace the start shape with an n = 3 superellipse (rounder): recompute its 64 points keeping the same order. A polygon with a different point count would break the animation. |
Image (HTML — .sq-mask__img) |
demo SVG portrait, sqm-grad-N gradient |
Replace the <svg> with an <img> of the same class, with an alt. If you keep SVGs and paste the effect twice, rename the ids of the second copy. |
Size (CSS — width / height of .sq-mask) |
90 × 90 px | The mask is in percentages: it follows. Move .sq-mask__name further down (bottom: -22px) as the size grows. |
Ring (CSS — .sq-mask__ring) |
4 px around, 2 px border, border-radius: 28% |
Colors by :nth-child(1) to (4). For a superelliptic ring, replace the border with a background and clip-path on two layers. |
Durations (CSS — transition) |
mask 0.6 s, grow and ring 0.5 s, name 0.3 s | Keep the mask longer than the rest: it is the least visible change, it should finish last. |
FAQ
polygon()s: two url(#…) pointing to SVG <clipPath>s would not interpolate.<img class="sq-mask__img" src="portrait.jpg" alt="…"> instead of the <svg>. width: 100%, height: 100% and object-fit: cover are already in the CSS: the photo fills the square, then the mask cuts it. Use centered faces, since the top corners are removed.clip-path is set on the image only; the ring is a sibling element, placed 4 px around, with a 28% border-radius. If it were cut by the same shape, its border, which overflows the mask, would be cut off.