Transitions✨ Premium

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.

CSSMorphPremium

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

Mic button to recording bar — a dictation app for doctors — Scale Morph example 1

① Mic button to recording bar — a dictation app for doctors

WhenBetween two patients, Dr Hamel presses the mic to dictate the visit report: the square button stretches into a "Recording 00:12" bar that counts the seconds.
WhyThe touched object changes shape: the doctor knows the button has become the recorder, and the slight bounce confirms the press.
SettingsSlate shape in A, red layer in B, target width raised to 250 px for the label; two labels inside the shape: the mic icon fades out, B's text only appears after 0.3 s, once the shape is wide enough; pagination dots hidden.
Chosen bike to unlock bar — a bike-sharing app — Scale Morph example 2

② Chosen bike to unlock bar — a bike-sharing app

WhenIn front of the Place du Marché station, a student in a hurry taps bike 214 on the map: the badge stretches into a "Bike 214 unlocked" bar, and he can ride off.
WhyThe badge stretching into a pill keeps the position of the touched object: the confirmation comes out of the gesture, not out of a dialog.
Settings84 px green badge ringed in white on the map, 260 × 60 px teal bar with an open padlock; the frame is transparent over the map.
Green badge to alert banner — a market gardener's greenhouse monitoring — Scale Morph example 3

③ Green badge to alert banner — a market gardener's greenhouse monitoring

WhenAt 2:20 pm, in full sun, the greenhouse 3 sensor goes over 32 °C: on the grower's tablet, the "Greenhouse 3 OK" badge widens into an orange "34 °C – open the vents" banner.
WhyThe change in size and color on an overshooting curve draws the eye to the alert without popping up a window.
Settings96 px green badge, 290 × 64 px orange banner with a wider orange shadow; drawing of the greenhouse tunnel in the frame, threshold top right, the other greenhouses below the frame.

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" and aria-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-pressed already gives the state, keep a stable label or set an explicit aria-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.

Chrome 87+✓ Full
Firefox 66+✓ Full
Safari 14.1+✓ Full
Edge 87+✓ Full
Mobile iOS 14.5+✓ Full
Android Chrome 87+✓ Full

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):

index.html — structure
<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>
Unlock the full code — $9.90/mo incl. tax

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 / propertyDefaultEffect
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

Because a browser cannot interpolate between two 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.
Here there is only one element: it keeps its place, its identity and its keyboard focus, only its shape changes. It is the right choice when the touched object becomes something else (a button turning into a progress bar, a badge turning into a banner). To replace one content with another, prefer a fade or a push between two layers.
That is not the default: the shape stays in B until the next press, so information is not taken away from the user. If state B really is temporary ("Copied!"), add a setTimeout in your code that toggles again after 2 to 3 seconds, and announce the change with an aria-live region.