Shape Shifter
A block cut out with clip-path that reshapes on every click, from circle to square, triangle, hexagon then star, in 0.8 s: all five shapes are 40-point polygons, so every point glides to its match, and the gradient changes hue at the same time. The cutout applies to everything inside the block, photos included; a real button for the keyboard, and the shape is set without morphing when the user reduces motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Liquid has 31 effects, including 6 free. Explore the category →
3 usage examples



How it works
The .shape-shifter-btn button wraps a 140 × 140 px block, .shape-shifter, whose data-shape attribute picks the shape: circle, square, triangle, hexagon or star. Each value has its own CSS rule, a clip-path: polygon(…) and two gradient colors. On click (or Enter, Space), the script moves to the next shape and writes its name under the block, in .shape-label, reading it from the button's data-names attribute ("Circle,Square,Triangle,Hexagon,Star"). The morph itself is a CSS transition: transition: clip-path .8s cubic-bezier(.68, -.2, .27, 1.2), with a slight back-up at the start and a slight overshoot at the end.
A browser can only interpolate two clip-path values when they use the same function with the same number of points: between circle() and polygon(), or between a 4-vertex square and a 10-vertex star, it jumps in one go. That is why the five shapes are written as 40-point polygon() values, all starting at the top center and going the same way around: the circle is a 40-sided polygon (less than 0.3 px away from a true circle at 140 px), and each side of the square, triangle, hexagon and star carries its in-between points. Measured frame by frame in Chromium 151, Firefox 153 and WebKit 26: on each of the five steps, the clipped area changes on every frame (from circle to square, the area goes from 78% to 64% of the block through 83 to 86 distinct values in Chromium and Firefox, 24 in WebKit, which draws fewer frames in headless mode).
The two gradient colors, --fx0403-c1 and --fx0403-c2, are declared with @property as <color> values: the browser can then blend them over 0.8 s, together with the shape, while a hard-coded linear-gradient() would switch at once. A click during the morph starts from the shape on screen, as the CSS transition retargets by itself. The cutout applies to .shape-shifter and everything inside it: a photo, an illustration or text placed inside takes the shape, like the portrait on the judo card in the examples.
Accessibility
- Keyboard and screen readers: the shape sits in a real
<button>named "Next shape". Measured in Chromium and Firefox: Tab reaches it, Enter and Space move to the next shape. The.shape-labeltext hasaria-live="polite": the new shape's name is announced. In Safari, Tab only stops on buttons if the keyboard setting asks for it (or with Option+Tab). - Focus ring: the cutout is on the
.shape-shifterchild, not on the button, so the focus ring (2 px cyan, 6 px offset) stays whole around the shape. - Reduced motion: under
prefers-reduced-motion: reduce,clip-pathhas no transition, only the colors blend over 0.4 s. Measured: the new shape is in place from the first frame. - The shape must not be the only information: if it means something (a chosen frame, a pawn), the label says it in text too, as in the examples.
Browser compatibility
CSS (clip-path: polygon(), @property) + 1 KB of JavaScript, no dependencies. Measured frame by frame in Chromium 151, Firefox 153 and WebKit 26: same morph on all five steps, same blending colors, same stop under reduced motion. clip-path: polygon() animates since Chrome 55, Firefox 54 and Safari 13.1. @property exists since Chrome 85, Safari 16.4 and Firefox 128; without it, colors switch at once but the shape still morphs. Cost: a 140 px block repainted for 0.8 s, negligible.
Without JavaScript, the block stays a circle. Without @property, the gradient jumps from one hue to the next while the shape morphs. A browser without clip-path shows the 140 px square block with its gradient.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0403-root">
<div class="shape-shifter-container">
<button type="button" class="shape-shifter-btn" aria-label="Next shape"
data-names="Circle,Square,Triangle,Hexagon,Star">
<span class="shape-shifter" data-shape="circle"></span>
</button>
<span class="shape-label" aria-live="polite">Circle</span>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Size (CSS — width / height of .shape-shifter) |
140 × 140 px | The polygons use percentages: the shape follows the block's size. Keep the block square, or the circle becomes an ellipse. |
Colors per shape (CSS — --fx0403-c1 / --fx0403-c2 in each [data-shape=…] rule) |
five gradients: cyan, violet, pink, amber, green | Put the same two colors in all five rules for a fixed background (a mirror, a photo on a plain color). The properties stay animatable as long as both @property blocks are there. |
Duration and bounce (CSS — transition: clip-path .8s cubic-bezier(.68, -.2, .27, 1.2)) |
0.8 s, slight back-up then slight overshoot | .5s ease-in-out gives a crisper morph without bounce. Change the duration of the two colors too, or they finish after the shape. |
Labels (HTML — the button's data-names) |
"Circle,Square,Triangle,Hexagon,Star" (English version) | Five comma-separated names, in shape order. The script rereads the attribute on every click: you can change it on the fly (language, added price). |
Shape order and choice (JS — var shapes = ['circle', 'square', 'triangle', 'hexagon', 'star']) |
five shapes, looping | Remove a value to skip a shape (keep data-names in the same order). A new shape needs a [data-shape=…] rule with a 40-point polygon(), starting at the top center, going clockwise. |
Shadow that follows the shape (CSS — on .shape-shifter-btn) |
none (the block's box-shadow is cut off with it) |
filter: drop-shadow(0 10px 14px rgba(0,0,0,.35)) on the button draws a shadow that hugs the shape throughout the morph. |
FAQ
circle() to polygon(), the shape jumps at the end, with no in-between frame. Both sides need the same function and the same number of points. 40 points are enough for a 140 px circle to look smooth (less than 0.3 px off) and split cleanly across the sides: 4 points per star arm, 10 per square side. More points would smooth the circle further on a very large block, at the cost of longer CSS.polygon() in percentages, whose first point is at the top center (50% y%) and which goes clockwise, like the other five. Spread the points along the sides, vertices included: a shape with 5 equal sides gets 8 points per side. If the starting points do not match, the shape twists during the morph instead of transforming cleanly.