Buttons✨ Premium

Morph

On hover, the purple button grows 10% and goes from 12 px corners to a pill (50 px) in 500 ms on a bouncy curve; its solid color snaps to a purple → fuchsia gradient — pure CSS, no press state.

CSSHoverTransform

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Buttons has 27 effects, including 2 free. Explore the category →

3 usage examples

Play — album or playlist card — Morph example 1

① Play — album or playlist card

WhenMusic player card: cover art, title, artist, one play button.
WhyTurning into a pill evokes the play button of streaming apps, and the 500 ms bounce gives tactile feedback to an otherwise still card. Purple → fuchsia suits colorful cover art.
SettingsCompact button (padding: 11px 22px, 14 px), scale(1.06) so it doesn't bite into the cover, .4s duration.
Create my first design — creative tool landing page — Morph example 2

② Create my first design — creative tool landing page

WhenHero of a creation app (design, video, slides): title, promise, a single button.
WhyThe square → pill morph stages the transformation the tool promises. On an isolated, generous button, the 10% overshoot has room to play without covering a neighbor.
Settings17 px text, padding: 18px 40px, cubic-bezier(.68, -.6, .32, 1.6) curve for a stronger bounce, #7c3aed → #c026d3 gradient for contrast.
Subscribe — email field and inline button — Morph example 3

③ Subscribe — email field and inline button

WhenNewsletter block: an input field and the submit button side by side.
WhyThe button rounding into a pill echoes the rounded field next to it; it's visible feedback on the block's only action. The scale(1.1) makes it overflow 5 px onto the field: leave a gap of at least 10 px.
Settingsgap: 12px between field and button, scale(1.05), field border-radius = 50px for an identical final shape.

How it works

Only two states: rest (background: #8b5cf6, border-radius: 12px, scale(1)) and :hover (border-radius: 50px, transform: scale(1.1), background: linear-gradient(135deg, #8b5cf6, #d946ef)). No @keyframes, no :active rule: all the motion is the transition between these two states.

The transition: .5s cubic-bezier(.68, -.55, .265, 1.55) on all properties. The control points outside [0, 1] push the curve beyond the start → end range. Measured in Chromium on scale: pull-back to 0.9925 around 60–100 ms (the button contracts before growing), overshoot to 1.107 around 400 ms, then settling at 1.1 at 500 ms. border-radius rides the same curve: 12 → 50 px with a peak at ≈ 52.7 px.

On a 50 px tall button, border-radius: 50px is a full pill (the radius exceeds half the height, the browser caps it). The change of shape and size is simultaneous: that combination, more than scale alone, is what makes the material seem to deform. The button goes from 113 × 50 px to 124 × 55 px on screen without touching layout (transform doesn't affect neighbors) — it overlaps them by 5 px on each side.

The color, however, doesn't glide: background-image is not an animatable property, and the hover's background shorthand resets background-color to transparent. Measured at +30 ms: the gradient is already in place. So the color switches instantly on hover in and out, while shape and size bounce over 500 ms — see the FAQ for a fade.

Accessibility

  • Insufficient contrast as shipped: white text on #8b5cf6 = 4.23:1, just under the 4.5:1 AA threshold for 16 px text; on hover, the fuchsia end of the gradient (#d946ef) drops to 3.46:1. Switch to #7c3aed at rest (5.7:1) and linear-gradient(135deg, #7c3aed, #c026d3) on hover (5.7:1 → 4.7:1), or raise the label to 18 px bold (3:1 threshold, which the original meets).
  • prefers-reduced-motion not handled: add @media (prefers-reduced-motion: reduce) { .btn-morph { transition: none } }. Shape, size and color still change on hover, but with no bounce or glide.
  • Keyboard: no :hover for Tab navigation. Declare .btn-morph:focus-visible { border-radius: 50px; transform: scale(1.1); background: linear-gradient(135deg, #8b5cf6, #d946ef) } to give focus the same relief, and keep the native outline — the code doesn't remove it.
  • Screen readers: the effect is purely visual, the button remains a native <button> with its label as accessible name. Nothing to add.
  • Touch: on iOS the :hover state sticks after a tap — the button stays an enlarged pill until the next touch; wrap the rule in @media (hover: hover). Target ≈ 50 px tall (55 px on hover), above the 44 px minimum.

Browser compatibility

Requires CSS transitions on transform and border-radius with cubic-bezier() — every browser since 2012, overshoot values included. No JavaScript, zero dependencies.

Chrome 26+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 12+✓ Full
Mobile iOS✓ Static (no hover, sticky state after a tap)
Android Chrome✓ Static (no hover)

Without transitions, both states apply instantly: the button jumps from its squared shape to the enlarged pill, and the gradient appears the same way. No browser animates a solid color into a gradient: that color jump is normal behavior, not a fallback.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
cubic-bezier(.68, -.55, .265, 1.55) (CSS — transition) ≈ 0.75% pull-back then 0.7% overshoot on scale Bring the 2nd and 4th parameters toward 0 and 1 to calm the bounce (.68, -.2, .265, 1.2); push them further to exaggerate it. Too far, border-radius dips below 0 for an instant: square corners.
transition-duration (CSS — .btn-morph) .5s Total length, bounce included. .3s = responsive (Gummy Elastic's value); .8s = gelatinous, for a large button only.
scale(1.1) (CSS — :hover) 1.1 Hover growth. No layout effect: the button overlaps its neighbors by 5% of its width on each side. 1.04 in a tight toolbar.
border-radius (CSS — 12px rest / 50px hover) 12 / 50 px Start and end shapes. 50px = pill for any button under 100 px tall; 24px = merely stronger rounding; 0 at rest = a square turning into a pebble.
background (CSS — :hover) linear-gradient(135deg, #8b5cf6, #d946ef) Hovered button color. Switches with no fade (not animatable). A solid color instead (#a855f7) becomes animatable again and glides with the rest.
background (CSS — .btn-morph) #8b5cf6 Resting color. For AA contrast with white text: #7c3aed or darker.
transition-property (CSS — implicit) all Restrict to transform, border-radius so the bouncy curve doesn't apply to a shadow or color added later.

FAQ

Because hover replaces a solid color with a linear-gradient, and background-image is not interpolable — no browser fades a color into a gradient. For a fade, keep background: #8b5cf6 on the button, draw the gradient in a pseudo-element (::before { inset: 0; background: linear-gradient(…); opacity: 0; transition: opacity .5s; border-radius: inherit }) and set its opacity to 1 on hover — opacity animates, and border-radius: inherit makes it follow the morph.
The code has no :active rule. Add one after :hover: .btn-morph:active { transform: scale(.96); border-radius: 30px }. Declared later, it wins while the pointer is down, and release returns to the pill through the same 500 ms curve — exactly Gummy Elastic's construction, only slower.
transform doesn't change layout: the hovered button overflows 5% per side onto its surroundings, and a neighbor painted after it covers it. Give the button position: relative; z-index: 1 (or isolation: isolate on its parent) so it paints on top, and leave a gap of at least 10% of its width if you want no overlap at all.