CSS WOOSH animations


cartoony css effects

Answering the question, What happens when you tell an LLM to follow the 12 principles of animation to create a button animation? (see demo link)

run demo


Tutorial: Building the Woosh Effect

The demo applies Disney's 12 Principles of Animation to a button click — specifically anticipation, squash and stretch, follow-through, and secondary action. Here's how each piece was built.


1. The Hover Anticipation

Before the click even happens, the button subtly grows on hover. This primes the viewer that something is about to happen.

.woosh:hover {
    transform: scale(1.06);
    transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}

The cubic-bezier (0.34, 1.56, 0.64, 1) is a spring curve — the control points go past 1.0 on the y-axis, which makes the scale overshoot and snap back, giving it that bouncy cartoon feel.


2. The Launch Keyframe: Squash, Wind-Up, and Streak

The core animation runs on a class toggle (.woosh--go). It's a single @keyframes block that hits five distinct beats:

@keyframes woosh-fire {
    0%   { transform: translateX(0)    scaleX(1)    scaleY(1)    rotate(0deg);  opacity: 1;    }
    6%   { transform: translateX(0)    scaleX(1.15) scaleY(0.85) rotate(0deg);  opacity: 1;    }
    25%  { transform: translateX(-80px) scaleX(0.8) scaleY(1.2)  rotate(-6deg); opacity: 1;    }
    32%  { transform: translateX(-90px) scaleX(0.75) scaleY(1.25) rotate(-7deg); opacity: 1;   }
    45%  { transform: translateX(250px) scaleX(1.8)  scaleY(0.5)  rotate(4deg); opacity: 0.85; }
    65%  { transform: translateX(700px) scaleX(2.0)  scaleY(0.4)  rotate(3deg); opacity: 0.4;  }
    100% { transform: translateX(1800px) scaleX(2.2) scaleY(0.3)  rotate(1deg); opacity: 0;    }
}
Beat % What's happening
Squash 0–6% Widens and flattens slightly — the button "loads up"
Wind-up 6–32% Pulls back left, stretches tall and thin (opposite of launch direction)
Launch 32–45% Reverses hard to the right, now wide and flat like a speeding car
Streak 45–65% Keeps accelerating off-screen, stretching further
Gone 65–100% Way off-screen, fades to nothing

The opposing scaleX/scaleY values maintain the squash-and-stretch illusion — total area stays roughly constant while the shape distorts.


3. Particle System: Clouds and Speed Lines

Particles are created in JavaScript and injected into <body> as absolutely-positioned divs, then removed after their animation completes. This keeps the HTML clean and avoids clutter.

Cloud puffs are pure CSS shapes — no images. Each cloud is a div with two pseudo-elements (::before, ::after) and two child <span>s, all circles styled with border-radius: 50% and a soft radial gradient.

.woosh-cloud::before { width: 22px; height: 22px; top: -11px; left: -11px; }
.woosh-cloud::after  { width: 16px; height: 16px; top: -14px; left: 6px;   }

Four clouds are spawned with staggered setTimeout delays (300ms–410ms) so they appear one-by-one as the button passes through the launch origin, not all at once.

Speed lines are just thin divs with a linear-gradient that fades right-to-transparent, scaled from 0 to full width via @keyframes woosh-streak. They're positioned around the launch point at slightly random Y offsets to break the mechanical regularity.

@keyframes woosh-streak {
    0%   { transform: scaleX(0);                    opacity: 1;   }
    30%  { transform: scaleX(1);                    opacity: 0.8; }
    100% { transform: scaleX(1.8) translateX(40px); opacity: 0;   }
}

4. Wiring It Together with JavaScript

The JS is minimal: find every .woosh element, bind a click handler, and toggle the class.

function wooshFire(btn) {
    if (btn.classList.contains('woosh--go')) return;  // guard against double-clicks
    btn.classList.add('woosh--go');
    // ... spawn particles ...
    btn.addEventListener('animationend', () => {
        btn.dispatchEvent(new CustomEvent('woosh:done'));
    }, { once: true });
}

pointer-events: none is set on .woosh--go so the button can't be clicked mid-flight. After the animation completes, a woosh:done custom event fires — giving consumers a clean hook to reset or replace the element.

The reset helper forces a reflow between removing and restoring the animation, which is the standard trick to restart a CSS animation without removing and re-adding the element to the DOM:

window.wooshReset = (btn) => {
    btn.classList.remove('woosh--go');
    btn.style.animation = 'none';
    btn.offsetHeight;   // force reflow
    btn.style.animation = '';
};

Key Takeaways