Morphing Corner Shapes
Four 100 px pieces — superellipse, bevel, notch, scoop — that, on hover, really turn into the next shape over 0.6 s with a slight bounce and a 5° rotation: four clip-path: polygon() of 68 points each, in the same order, which the browser interpolates point by point.
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 .morph-shape (100 × 100 px, 135° gradient, white uppercase 10.4 px label) carries four variables: --msh-squircle (superellipse n = 4), --msh-bevel (24% cut corners), --msh-notch (18% square notch) and --msh-scoop-concave (24% scooped quarter circle). Each one is a polygon() of 68 points in percentages, 17 per corner, in the same order: from the left edge to the top edge, then the other three corners by rotation. When :hover swaps one variable for the next, transition: clip-path 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) moves each point towards its counterpart: the superellipse stretches into a bevel, the bevel digs into a notch, the notch rounds into a scoop, the scoop swells into a superellipse.
Measured while hovering the first piece, on the vertical coordinate of the first point (50% → 24%): 42.4% at 40 ms, 28.8% at 150 ms, 21.7% at 300 ms — the bounce curve overshoots the target —, 22.4% at 450 ms, 24% at 800 ms in Chromium 151; 39.5, 27.3, 21.5, 22.7 and 24% in Firefox 153; 32.3% at 120 ms in WebKit 26. The piece also rotates ±5° and grows 5% (0.4 s). The background switches to the next shape's gradient too, but a gradient does not interpolate: the color changes at once, when the hover starts. Leaving the piece plays the transition backwards. No JavaScript.
The four shapes are polygon()s with the same structure, not url(#squircle), url(#bevel)… pointing to SVG <clipPath>s, for two reasons. A <clipPath> must exist on the page with its id: when missing, the pieces stay squares and only the rotation and color change show (measured: corner clickable). And even with valid <clipPath>s, going from one url() to another does not animate: it would snap. Four 68-point polygons in the same order make the morph real.
Accessibility
- Hover only: the shape change only exists with a mouse. The pieces are
divs with no role and notabindex(Tab never stops on them, measured); with a finger, a tap triggers:hoverand the shape stays until the next touch. If the shape carries information (game, configurator), addtabindex="0"and repeat the:hoverrules as:focus-visible; sinceclip-pathclips the focus ring, mark focus with the rotation itself or an outline on the parent. - prefers-reduced-motion not in the code: measured under emulation, rotation, grow and morph all stay active. Add
@media (prefers-reduced-motion: reduce) { .morph-shape { transition: none } .morph-shape:hover { transform: none } }: the shape still changes, without motion. - Contrast of the white 10.4 px label: 4.2:1 on
#8b5cf6, 3.5:1 on#ec4899, 2.4:1 on#06b6d4, 2.5:1 on#10b981— all under 4.5:1. Darken the gradient start colors or raise the text to 14 px at weight 700 (3:1 threshold).
Browser compatibility
CSS only: clip-path: polygon() in percentages, clip-path transitions between polygons with the same number of points, custom properties, transform. Zero JavaScript, zero SVG. Morph measured in Chromium 151, Firefox 153 and WebKit 26 (intermediate values read during the transition in all three).
Without clip-path (Firefox before 54, Safari before 13.1 without prefix), the pieces are square and only rotation and color change remain. If you replace a shape with a polygon of a different point count, the browser can no longer interpolate: the change snaps again, with no error.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="demo-preview">
<div class="morph-shapes-wrap">
<div class="morph-shape morph-shape--squircle">…</div>
<div class="morph-shape morph-shape--bevel">…</div>
<div class="morph-shape morph-shape--notch">…</div>
<div class="morph-shape morph-shape--scoop">…</div>
</div>
</div>
The full HTML + CSS + JS of Morphing Corner Shapes, 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 |
|---|---|---|
Cycle order (CSS — .morph-shape--*:hover rules) |
superellipse → bevel → notch → scoop → superellipse | Change the variable called in each :hover. Any pair of shapes animates, since all four have 68 points in the same order. |
Shape depth (CSS — the four --msh-* variables) |
bevel 24%, notch 18%, scoop 24%, superellipse n = 4 | Recompute the 17 points of one corner, then rotate them: for a b% bevel, points go from (0, b) to (b, 0) in a straight line. Keep 17 points per corner, or interpolation breaks. |
Duration and bounce (CSS — transition of .morph-shape) |
clip-path 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) |
0.9s ease-in-out for a slow morph with no overshoot; the original curve overshoots the target by about 2 percentage points around 300 ms. |
Rotation and grow (CSS — transform of the :hover rules) |
±5° and scale(1.05) |
transform: none for a configurator where the piece must stay straight; ±12° for a livelier game. |
Size (CSS — width / height of .morph-shape) |
100 × 100 px | Shapes are in percentages: they follow. A non-square piece stretches the corners along the long side. |
FAQ
polygon()s that have the same number of points: it then moves each point towards the point of the same rank. Two url(#…) pointing to SVG <clipPath>s never interpolate — the shape flips at the end. The code's four shapes have 68 points, 17 per corner, in the same order: any pair animates. Measured in Chromium, Firefox and WebKit, intermediate values run through the 0.6 s.background holds a linear-gradient(), and gradients do not interpolate: the transition: background 0.4s rule has no effect, the color changes at the first instant of the hover. For a fade, put the color in a variable declared with @property --msh-c { syntax: '<color>'; inherits: false; initial-value: #8b5cf6 }, use it in the gradient and transition the variable..morph-shape--squircle:hover selectors with .morph-shape--squircle.is-next (and so on), then toggle the class in JavaScript: el.addEventListener('click', () => el.classList.toggle('is-next')). The transition stays the same.