Pure TailwindMIT licensed

Tailwind Jelly Bounce Button with Elastic Squash

Squash, stretch, and a settling wobble — seven keyframe steps of jelly physics. Nothing about the timeline can become a utility, and that is the point: the port keeps the keyframe verbatim and reduces the CSS to a single trigger, hover:animate-tab09-jelly.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tab-09 w-full min-h-svh grid place-items-center bg-tab09-bg font-tab09-sans px-6 py-10">
  <div class="flex flex-col items-center gap-8 py-6">
    <p class="text-[0.78rem] tracking-[0.14em] uppercase text-tab09-mut m-0">Hover for jelly effect</p>
    <div class="flex flex-wrap justify-center gap-5">
      <button type="button" class="min-h-11 px-9 text-[1.05rem] font-bold tracking-[0.03em] text-white bg-tab09-pink rounded-full cursor-pointer origin-center will-change-transform shadow-[0_4px_16px_oklch(0_0_0/0.15)] hover:animate-tab09-jelly focus-visible:animate-tab09-jelly focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab09-pink motion-reduce:hover:animate-none motion-reduce:focus-visible:animate-none motion-reduce:hover:scale-[1.04]">Subscribe!</button>
      <button type="button" class="min-h-11 px-9 text-[1.05rem] font-bold tracking-[0.03em] text-white bg-tab09-green rounded-full cursor-pointer origin-center will-change-transform shadow-[0_4px_16px_oklch(0_0_0/0.15)] hover:animate-tab09-jelly focus-visible:animate-tab09-jelly focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab09-green motion-reduce:hover:animate-none motion-reduce:focus-visible:animate-none motion-reduce:hover:scale-[1.04]">Let's Go!</button>
      <button type="button" class="min-h-11 px-9 text-[1.05rem] font-bold tracking-[0.03em] text-white bg-tab09-sky rounded-full cursor-pointer origin-center will-change-transform shadow-[0_4px_16px_oklch(0_0_0/0.15)] hover:animate-tab09-jelly focus-visible:animate-tab09-jelly focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab09-sky motion-reduce:hover:animate-none motion-reduce:focus-visible:animate-none motion-reduce:hover:scale-[1.04]">Join Now!</button>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tab09-bg: #fdf2ff;
  --color-tab09-bg: oklch(0.97 0.03 320);
  --color-tab09-mut: #a855f7;
  --color-tab09-mut: oklch(0.63 0.24 303);
  --color-tab09-pink: #db2777;
  --color-tab09-pink: oklch(0.59 0.22 354);
  --color-tab09-green: #16a34a;
  --color-tab09-green: oklch(0.58 0.16 149);
  --color-tab09-sky: #0284c7;
  --color-tab09-sky: oklch(0.56 0.15 240);
  --font-tab09-sans: 'Space Grotesk', system-ui, sans-serif;
  --animate-tab09-jelly: tab09-jelly 0.7s cubic-bezier(0.4, 0, 0.2, 1) both;
}

@keyframes tab09-jelly {
  0% {
    scale: 1 1;
  }

  10% {
    scale: 1.2 0.85;
  }

  25% {
    scale: 0.85 1.15;
  }

  40% {
    scale: 1.1 0.92;
  }

  55% {
    scale: 0.95 1.05;
  }

  70% {
    scale: 1.03 0.98;
  }

  85% {
    scale: 0.99 1.01;
  }

  100% {
    scale: 1 1;
  }
}

.tab-09 {
  width: 100%;
  min-height: 100svh;
}

.tab-09 ::selection {
  background: var(--color-tab09-pink);
  color: #fff;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tab09-bg: #fdf2ff;
  --color-tab09-bg: oklch(0.97 0.03 320);
  --color-tab09-mut: #a855f7;
  --color-tab09-mut: oklch(0.63 0.24 303);
  --color-tab09-pink: #db2777;
  --color-tab09-pink: oklch(0.59 0.22 354);
  --color-tab09-green: #16a34a;
  --color-tab09-green: oklch(0.58 0.16 149);
  --color-tab09-sky: #0284c7;
  --color-tab09-sky: oklch(0.56 0.15 240);
  --font-tab09-sans: 'Space Grotesk', system-ui, sans-serif;
  --animate-tab09-jelly: tab09-jelly 0.7s cubic-bezier(0.4, 0, 0.2, 1) both;
}

@keyframes tab09-jelly {
  0% {
    scale: 1 1;
  }

  10% {
    scale: 1.2 0.85;
  }

  25% {
    scale: 0.85 1.15;
  }

  40% {
    scale: 1.1 0.92;
  }

  55% {
    scale: 0.95 1.05;
  }

  70% {
    scale: 1.03 0.98;
  }

  85% {
    scale: 0.99 1.01;
  }

  100% {
    scale: 1 1;
  }
}

.tab-09 {
  width: 100%;
  min-height: 100svh;
}

.tab-09 ::selection {
  background: var(--color-tab09-pink);
  color: #fff;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The keyframe uses the standalone scale property rather than transform: scaleX() scaleY(), so it does not clobber other transform utilities on the same element — the v4-idiomatic form.
  • Nothing else about this demo ports: the timeline is the design, and it stays CSS. One class does the whole trigger.
  • UI change from the CSS original: darker pink and green fills, because the originals ran white text at roughly 3:1 — below AA for a 17px label. The jelly is unchanged.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Jelly Bounce Button with Elastic Squash
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-jelly-bounce-button-animation/

Squash, stretch, and a settling wobble &mdash; seven keyframe steps of jelly physics. Nothing about the timeline can become a utility, and that is the point: the port keeps the keyframe verbatim and reduces the CSS to a single trigger, hover:animate-tab09-jelly.
## HTML
```html
<section class="tab-09 w-full min-h-svh grid place-items-center bg-tab09-bg font-tab09-sans px-6 py-10">
  <div class="flex flex-col items-center gap-8 py-6">
    <p class="text-[0.78rem] tracking-[0.14em] uppercase text-tab09-mut m-0">Hover for jelly effect</p>
    <div class="flex flex-wrap justify-center gap-5">
      <button type="button" class="min-h-11 px-9 text-[1.05rem] font-bold tracking-[0.03em] text-white bg-tab09-pink rounded-full cursor-pointer origin-center will-change-transform shadow-[0_4px_16px_oklch(0_0_0/0.15)] hover:animate-tab09-jelly focus-visible:animate-tab09-jelly focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab09-pink motion-reduce:hover:animate-none motion-reduce:focus-visible:animate-none motion-reduce:hover:scale-[1.04]">Subscribe!</button>
      <button type="button" class="min-h-11 px-9 text-[1.05rem] font-bold tracking-[0.03em] text-white bg-tab09-green rounded-full cursor-pointer origin-center will-change-transform shadow-[0_4px_16px_oklch(0_0_0/0.15)] hover:animate-tab09-jelly focus-visible:animate-tab09-jelly focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab09-green motion-reduce:hover:animate-none motion-reduce:focus-visible:animate-none motion-reduce:hover:scale-[1.04]">Let's Go!</button>
      <button type="button" class="min-h-11 px-9 text-[1.05rem] font-bold tracking-[0.03em] text-white bg-tab09-sky rounded-full cursor-pointer origin-center will-change-transform shadow-[0_4px_16px_oklch(0_0_0/0.15)] hover:animate-tab09-jelly focus-visible:animate-tab09-jelly focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab09-sky motion-reduce:hover:animate-none motion-reduce:focus-visible:animate-none motion-reduce:hover:scale-[1.04]">Join Now!</button>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tab09-bg: #fdf2ff;
  --color-tab09-bg: oklch(0.97 0.03 320);
  --color-tab09-mut: #a855f7;
  --color-tab09-mut: oklch(0.63 0.24 303);
  --color-tab09-pink: #db2777;
  --color-tab09-pink: oklch(0.59 0.22 354);
  --color-tab09-green: #16a34a;
  --color-tab09-green: oklch(0.58 0.16 149);
  --color-tab09-sky: #0284c7;
  --color-tab09-sky: oklch(0.56 0.15 240);
  --font-tab09-sans: 'Space Grotesk', system-ui, sans-serif;
  --animate-tab09-jelly: tab09-jelly 0.7s cubic-bezier(0.4, 0, 0.2, 1) both;
}

@keyframes tab09-jelly {
  0% {
    scale: 1 1;
  }

  10% {
    scale: 1.2 0.85;
  }

  25% {
    scale: 0.85 1.15;
  }

  40% {
    scale: 1.1 0.92;
  }

  55% {
    scale: 0.95 1.05;
  }

  70% {
    scale: 1.03 0.98;
  }

  85% {
    scale: 0.99 1.01;
  }

  100% {
    scale: 1 1;
  }
}

.tab-09 {
  width: 100%;
  min-height: 100svh;
}

.tab-09 ::selection {
  background: var(--color-tab09-pink);
  color: #fff;
}
```

How this works

A timeline is not a design token. Seven steps of paired scale values have no utility form, nor should they — so the keyframe stays in CSS and only its binding becomes a class. This is the clearest case in the collection of Tailwind's actual division of labour.

Ending at identity is what makes it reversible. The last step is scale(1), so a cursor leaving mid-animation lands the button at its resting size. both in the token's fill-mode applies the first frame immediately, with no flash of the rest state.

Reduced motion still answers. A 4% static scale confirms the hover without any deformation — a different state, not a slower one.

Make it yours

  • Intensity: push the extremes (scaleX(1.35) squash, scaleY(1.2) stretch) in the keyframe.
  • Tighter settle: delete the 70% and 85% steps for a five-step wobble.
  • Stagger a row by adding [animation-delay:0.1s] / [animation-delay:0.2s] to successive buttons.

Gotchas — read before shipping

  • Text can blur momentarily during the squash on low-DPI screens — a sub-pixel artefact of scaling rendered glyphs, not a bug you can fix in CSS.
  • Inside a container with overflow-hidden the stretch phase clips. Give the row padding.
  • will-change-transform helps here and hurts if you sprinkle it everywhere; it is justified because the animation is large, frequent and transform-only.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Multi-step transform keyframes are universally supported; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Search CodeFronts

Loading…