Bezier Elastic
On hover the button grows to 1.08 and rises 3 px along a curve that overshoots its target by 9.8% before settling (peak at 1.088 around 230 ms); on click it squashes to 0.95 in 100 ms. Three states, one transition, no JavaScript.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Buttons has 27 effects, including 2 free. Explore the category →
3 usage examples



How it works
Three states and no @keyframes: rest (cyan gradient #06b6d4 → #0891b2, white text), :hover (transform: scale(1.08) translateY(-3px) and box-shadow: 0 15px 35px rgba(6,182,212,.4)) and :active (transform: scale(.95)). The motion is only the transition between them: transition: .4s cubic-bezier(.34, 1.56, .64, 1) — all properties, 400 ms.
The curve is a “back-out”: its second control point has a y of 1.56, above 1. The value reaches the target at 37% of the time (≈ 150 ms), overshoots it by 9.8% of the distance at 57% (≈ 230 ms), then comes back to settle. Measured in Chromium: the scale goes from 1 to a 1.088 peak at 226 ms, then 1.08 at 400 ms; the translation peaks at −3.58 px before −3.24 px. Two details: translateY is written after scale, so it gets multiplied by 1.08 — the button rises 3.24 px, not 3; and the shadow follows the same curve, its offset and blur overshoot too.
On click, :active sets transition: .1s: the shorthand replaces the whole transition — 100 ms, default ease curve, no overshoot. The press is a snap, not a bounce. The rule omits translateY: the button also drops back 3 px. It doesn't touch box-shadow: the hover shadow stays under a button that is nonetheless squashed. Declared after :hover, :active wins for as long as the mouse button is down.
On release, the :active rule goes away and the hovered state's transition plays the way back: 400 elastic ms from 0.95 to 1.08. Since the overshoot is proportional to the distance (0.13 instead of 0.08), it's more visible: measured peak at 1.0927 around 245 ms. The release rebound, more than the hover, is what makes the effect. When the cursor leaves the button, back to 1 along the same curve, with a fleeting dip to about 0.992.
Accessibility
- Insufficient contrast: white text on
#06b6d4= 2.43:1 and on#0891b2= 3.68:1 — below the 4.5:1 AA threshold across the whole gradient, and below 3:1 on its light half. Keep the hue and darken:#0e7490 → #155e75gives 5.4:1 and 7.3:1. Dark text (#083344) wouldn't be enough: 5.5:1 on the light end, 3.6:1 on the dark end. The growth changes nothing about contrast. - prefers-reduced-motion not handled:
@media (prefers-reduced-motion: reduce) { .btn-bezier-elastic { transition: none } }— the states apply instantly. To remove the motion itself as well, add.btn-bezier-elastic:hover { transform: none }in the same rule: the shadow alone signals the hover. - Keyboard:
:activeapplies while Space is held in most browsers — the squash exists on keyboard — but there is no:hoveron focus. Add.btn-bezier-elastic:focus-visible { transform: scale(1.08) translateY(-3px); box-shadow: 0 15px 35px rgba(6,182,212,.4) }. The nativeoutlineis kept and follows the transform. - Footprint: at the peak, a 160 px button is 174 px wide and 55 px tall, and its shadow extends 35 px of blur below. Leave at least 8 px between two buttons, and a
z-indexon hover if the button overlaps a neighbor. Target ≈ 51 px tall at rest, above the 44 px minimum. - Touch:
:activefires on touch — the squash is good mobile feedback; on iOS the hovered state sticks after a tap (@media (hover: hover)). Screen readers: nothing generated, accessible name = label of the native<button>.
Browser compatibility
Requires CSS transitions on transform and box-shadow with a cubic-bezier() whose y values leave [0, 1] — allowed by the specification and supported everywhere since 2012. No JavaScript, zero dependencies.
Without transitions, the three states apply instantly: the button grows, squashes and returns at once — usable, without the bounce.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<button class="demo-btn btn-bezier-elastic">Elastic</button>
The full HTML + CSS + JS of Bezier Elastic, 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 |
|---|---|---|
| cubic-bezier(.34, 1.56, .64, 1) (CSS — transition) | 9.8% overshoot | The 2nd parameter sets the bounce: 1.2 ≈ 1%, 1.4 ≈ 5%, 1.56 ≈ 10%, 1.8 ≈ 18%, 2 = 25%. Beyond that the overshoot turns cartoonish and the shadow “jumps”. |
| .4s (CSS — transition-duration) | .4s | Total length, bounce included. .25s: snappy, the overshoot is barely felt; .6s: the button floats. |
| scale(1.08) translateY(-3px) (CSS — :hover) | 1.08 / −3 px | Growth and lift. Order matters: translateY after scale is multiplied by the scale (3 × 1.08 = 3.24 px). Write translateY(-3px) scale(1.08) for exactly 3 px. |
| scale(.95) + transition: .1s (CSS — :active) | .95 in 100 ms | Depth and speed of the press. scale(.92): deeper; .15s: softer. Add box-shadow: 0 4px 12px rgba(6,182,212,.3) so the shadow closes in with the button instead of staying 15 px away. |
| box-shadow: 0 15px 35px rgba(6,182,212,.4) (CSS — :hover) | cyan shadow, 15 px below the button | Colored shadow in the gradient's hue — more a glow than a shadow. rgba(0,0,0,.3): neutral drop shadow; 0 8px 20px: closer, subtler. |
| linear-gradient(135deg, #06b6d4, #0891b2) (CSS — background) | light cyan → cyan | For AA contrast with white text: #0e7490 → #155e75. Change the shadow color at the same time. |
| transition-property (CSS — implicit) | all | Restrict to transform, box-shadow: a color or background change (theme, disabled state) won't bounce along the same curve. |
FAQ
transition: .1s is a shorthand: rewriting it resets every component to its default, hence transition-timing-function: ease. The press happens in 100 ms with no overshoot — intended, a click should feel crisp. For an elastic press, write transition-duration: .1s alone: the hover curve is preserved.z-index: 1 on :hover (the button is already position: relative) so it rises above following elements, and a container without overflow: hidden, which would clip the overshoot and the shadow.