Scale Morph
One element changes size, roundness and color — a 100 px square into a 220 × 60 pill — on an overshooting curve that takes it to 232 px before it settles: the component itself transforms, not an exchange of contents. CSS + 0.6 KB of JavaScript, final size at once when the user reduces motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Transitions has 52 effects, including 9 free. Explore the category →
3 usage examples



How it works
The .scm-frame (280 × 200 px) centers a single shape, .scm-shape: 100 × 100 px, 20 px corners, amber to orange gradient, amber shadow. It carries transition: .7s cubic-bezier(.34, 1.56, .64, 1) on all its properties. When .active toggles, it becomes 220 × 60 px, 30 px corners, blue shadow. The curve's second value (1.56) is above 1: the shape goes past its final size, then comes back. Measured every 80 ms after the click in Chromium 151: 151, 192, 217, 229, 232, 229, 225, 221 then 220 px wide; same 232 px peak in Firefox 153 and same landing in WebKit 26.5.
A CSS gradient does not animate: going from one linear-gradient to another changes the color at once. The target color therefore lives on a ::before layer of the shape (cyan to blue gradient, z-index: -1 inside a shape set to isolation: isolate, so below the text), whose opacity rises from 0 to 1 over 0.7 s on a curve without overshoot. Read in Chromium: 0.11 at 80 ms, 0.35 at 160 ms, 0.60 at 240 ms, 1 at 640 ms. The shape thus changes color while it grows, instead of jumping to blue on the first frame.
Below the shape, two dots show the state: the first is lit in A, the second in B (rules .scm-frame:not(.active) .scm-dot:first-child and .scm-frame.active .scm-dot:last-child, no id, no script). The script adds or removes .active on click, Enter or Space on every .scm-frame of the page and keeps aria-pressed up to date; state B stays until the next click, which shrinks the shape with the same bounce. Everything is prefixed scm-, nothing is exposed on window.
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, the shape takes its final size with no transition and no bounce (measured at 60 ms: 220 px in Chromium and Firefox); only the color changes, over 0.15 s. - Keyboard and screen readers: the frame has
role="button",tabindex="0"andaria-pressed; Enter and Space toggle like a click (measured). The announced name is the shape's text; if you change its label between A and B ("Dictate" / "Recording"),aria-pressedalready gives the state, keep a stable label or set an explicitaria-label. - The bounce draws the eye: that is its job (confirm a press, flag an alert). Do not use it on an element that changes often on its own, it would distract from reading.
- Contrast: white text on bright amber or orange; check your colors' contrast at your text size; the demo's 0.85 rem bold is borderline on light amber.
Browser compatibility
CSS (transition on width, height, border-radius and box-shadow, ::before pseudo-element, isolation, inset) + 0.6 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26.5: bounce to 232 px, landing at 220, return on the second click; color rise, dots, keyboard and reduced motion measured in Chromium and Firefox. Animating width and height recalculates layout on every frame: harmless for a single button, to avoid on dozens of elements at once.
Without JavaScript, the shape stays square. Without inset (browsers before Chrome 87, Firefox 66, Safari 14.1), the color layer does not cover the shape: write top: 0; right: 0; bottom: 0; left: 0 on .scm-shape::before.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="scm-stage">
<div class="scm-frame" role="button" tabindex="0" aria-pressed="false">
<div class="scm-shape">
<span class="scm-label">MORPH</span>
</div>
<div class="scm-dots" aria-hidden="true">
<span class="scm-dot"></span>
<span class="scm-dot"></span>
</div>
</div>
<span class="scm-hint">…</span>
</div>
The full HTML + CSS + JS of Scale Morph, 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 |
|---|---|---|
Sizes A and B (CSS — .scm-shape and .scm-frame.active .scm-shape) |
100 × 100 px → 220 × 60 px | Set the target width to your longest label plus 40 px of padding; the bounce adds about 5% for 0.2 s, leave that room around it. |
Bounce (CSS — cubic-bezier(.34, 1.56, .64, 1)) |
1.56: about 12 px overshoot over a 120 px travel | 1.3 for a subtle bounce, 1.8 for an elastic feel; cubic-bezier(.4, 0, .2, 1) removes the bounce. |
Colors (CSS — background of .scm-shape and of ::before) |
amber/orange → cyan/blue | The starting color is the shape's background, the target color the ::before background; any gradient works, the fade goes through opacity. |
Duration (CSS — transition) |
0.7 s for shape and color | 0.45 s for a button that must respond fast; keep the same duration on the shape and on ::before so the color ends with the size. |
State dots (HTML/CSS — .scm-dots) |
two dots below the shape | Remove the block if the shape is enough to tell the state; they are only decorative (aria-hidden). |
FAQ
linear-gradients: the background property changes at once, on the first frame. Stacking the target gradient on a pseudo-element and animating its opacity gives a true fade between the two gradients, in every browser. @property could animate gradient colors declared as variables, but only in recent browsers; the layer works everywhere.setTimeout in your code that toggles again after 2 to 3 seconds, and announce the change with an aria-live region.