Jelly Button
On hover the button squashes and stretches like a jelly being poked, wobbling a few times before it settles.
Create a jelly wobble button in pure CSS. On :hover and :focus-visible, run a 700ms keyframe animation once that alternates non-uniform scale: 0% scale(1,1), 30% scale(1.2,0.8), 40% scale(0.8,1.2), 50% scale(1.1,0.9), 65% scale(0.95,1.05), 75% scale(1.03,0.97), 100% scale(1,1). Use ease timing so each wobble is smaller than the one before, which reads as elastic. Keep the label centred and avoid overflow clipping. Under prefers-reduced-motion, replace the wobble with a background colour change. The label reads "Play now".
Source
'use client'
import s from './demo.module.css'
export default function JellyButton({ labels }: { labels: Record<string, string> }) {
return (
<button type="button" className={s.button}>
{labels.cta}
</button>
)
}
.button {
height: 52px;
padding: 0 32px;
border: 0;
border-radius: var(--radius-sm);
font: 500 var(--text-base) / 1 var(--font-body);
white-space: nowrap;
cursor: pointer;
border-radius: 999px;
background: var(--stage-accent);
color: var(--stage-accent-ink);
font-weight: 600;
}
.button:hover,
.button:focus-visible {
animation: jelly 700ms ease;
}
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 4px; }
@keyframes jelly {
0%, 100% { transform: scale(1, 1); }
30% { transform: scale(1.2, 0.8); }
40% { transform: scale(0.8, 1.2); }
50% { transform: scale(1.1, 0.9); }
65% { transform: scale(0.95, 1.05); }
75% { transform: scale(1.03, 0.97); }
}
@media (prefers-reduced-motion: reduce) {
.button:hover, .button:focus-visible { animation: none; background: var(--stage-accent-hover); }
}