#ConfettiButton
View Compiled
@import url("https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap");

body {
  font-family: "Fredoka One", cursive;
  font-size: 16px;
  background-color: #e6d6ff;
  width: 100vw;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
}

.confetti-button {
  font-family: inherit;
  font-size: 32px;
  display: inline-block;
  > button {
    --scale-x: 1;
    --scale-y: 1;
    --translate-x: 0%;
    --translate-y: 0%;
    color: #dec7ff;
    background-color: #7c1fff;
    font-family: inherit;
    font-size: inherit;
    white-space: nowrap;
    text-shadow: 0 -0.025em #4b02b3;
    padding: 0.5em;
    border: solid 0.025em #4b02b3;
    margin: 0;
    outline: 0;
    border-radius: 0.25em;
    box-sizing: border-box;
    transform:
      scaleX(var(--scale-x))
      scaleY(var(--scale-y))
      translateX(var(--translate-x))
      translateY(var(--translate-y))
    ;
  }
}

.confetti-container {
  --x: 0;
  --y: 0;
  width: 0;
  height: 0;
  position: absolute;
  top: 50%;
  left: 50%;
  transform:
    translateX(var(--x))
    translateY(var(--y))
  ;
}

.confetti {
  width: var(--width);
  height: var(--height);
  position: absolute;
  top: calc(var(--height) / -2);
  left: calc(var(--width) / -2);
  transform-style: preserve-3d;
  animation: spinning var(--spin-time) linear infinite;
  &:before, &:after {
    content: "";
    background-color: var(--background-color);
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
    transform-style: preserve-3d;
  }
  &:after {
    filter: brightness(0.75);
    transform: translateZ(-1px);
  }
}

@keyframes spinning {
  0% {
    transform: rotateY(0deg);
  }
  100% {
    transform: rotateY(360deg);
  }
}
View Compiled
import React, { useState } from "https://cdn.skypack.dev/react@17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom@17.0.1";

// GSAP timeline
const tl = gsap.timeline();

// Timeout (ms) to clear the array of confetti
const CLEAR_TIME = 5000;

// Confetti settings
const CONFETTI_COUNT_MIN = 25;
const CONFETTI_COUNT_MAX = 50;

// Confetti settings : Computed by em (See SCSS class)
const CONFETTI_WIDTH_MIN = 0.2;
const CONFETTI_WIDTH_MAX = 0.6;
const CONFETTI_HEIGHT_MIN = 0.2;
const CONFETTI_HEIGHT_MAX = 0.6;

// Confetti settings : Animations
const CONFETTI_SPIN_TIME_MIN = 128;
const CONFETTI_SPIN_TIME_MAX = 256;

// Confetti settings : Background colors
const CONFETTI_BG_COLORS = [
  "#8c3bff",
  "#9347ff",
  "#a15eff",
  "#aa6ffc",
  "#b885ff",
  "#6718d6",
  "#5e16c4",
  "#5310b3",
  "#490aa3",
  "#3c018f"
];

// Button clear timeout
let confettiClearTimeout;

/**
 * Generate random number
 */
function random(min, max) {
  return Math.round(Math.random() * (max - min)) + min;
}

/**
 * Animation is a job of GSAP. I'm letting GSAP to touch the virtual DOM directly
 */
function animateConfettiButton(elm, confettiStartCallback, animationEndCallback) {
  // Pull animation
  tl.to(elm, {
    "--scale-x": 0.5,
    "--scale-y": 1.75,
    "--translate-y": "25%",
    ease: "power3.out",
    duration: 1.5
  });
  
  // Release animation
  tl.to(elm, {
    "--scale-x": 1,
    "--scale-y": 1,
    "--translate-y": "0%",
    ease: "elastic.out(1.75, 0.2)",
    duration: 1.5,
    onStart: () => {
      confettiStartCallback();
    }
  });
  
  // Animation ending delay (There is no particular animation here)
  tl.to(elm, {
    duration: 0.5,
    onComplete: () => {
      animationEndCallback();
    }
  });
}

/**
 * Add confetti JSX element into an array
 */
function addConfetti(confettiArray, setConfettiArray) {
  // Clone the array from state
  const tempConfettiArray = [...confettiArray];
  
  for (let i = 0; i < random(CONFETTI_COUNT_MIN, CONFETTI_COUNT_MAX); i++) {
    const backgroundColor = CONFETTI_BG_COLORS[random(0, CONFETTI_BG_COLORS.length)];
    const width = random(CONFETTI_WIDTH_MIN * 100, CONFETTI_WIDTH_MAX * 100) / 100;
    const height = random(CONFETTI_HEIGHT_MIN * 100, CONFETTI_HEIGHT_MAX * 100) / 100;
    const spinTime = random(CONFETTI_SPIN_TIME_MIN * 100, CONFETTI_SPIN_TIME_MAX * 100) / 100;
    const style = {
      "--background-color": backgroundColor,
      "--width": `${width}em`,
      "--height": `${height}em`,
      "--spin-time": `${spinTime}ms`
    }
    
    // Add confetti🎉
    tempConfettiArray.push(
      <div className="confetti-container">
        <div className="confetti" style={style} onAnimationStart={evt => {
           animateConfetti(evt.target.parentElement);
        }}></div>
      </div>
    );
  }
  
  setConfettiArray(tempConfettiArray);
}

/**
 * Start of animation of each confettis
 */
function animateConfetti(elm) {
  const duration = random(40, 90) / 100;
  const randomY = random(50, 700) / 100;
  const randomX = random(0, 1000) / 100;
  const negativeX = random(0, 1) === 0 ? -1 : 1; 
  
  const confettiTl = gsap.timeline();
  
  confettiTl.to(elm, {
    "--y": `-${randomY}em`,
    ease: "power2.out",
    duration: duration
  });
  
  confettiTl.to(elm, {
    "--y": `${randomY / 2}em`,
    ease: "power1.in",
    duration: duration
  });
  
  confettiTl.to(elm, {
    "--x": `${randomX * negativeX}em`,
    ease: "power1.out",
    duration: duration * 2
  }, "-=" + (duration * 2));
  
  confettiTl.to(elm, {
    opacity: 0,
    ease: "power2.out",
    duration: duration / 2,
    onComplete: () => {
      confettiTl.set(elm, {
        width: 0,
        height: 0
      });
    }
  }, "-=" + (duration * 0.5));
}

/**
 * The confetti button react component. I used hooks to save the state.
 */
function ConfettiButton(props) {
  // Flag if the button is animating
  const [isAnimating, setIsAnimating] = useState(false);
  
  // Array to render the confetti
  const [confettiArray, setConfettiArray] = useState([]);
  
  clearTimeout(confettiClearTimeout);
  
  confettiClearTimeout = setTimeout(() => {
    setConfettiArray([]);
  }, CLEAR_TIME);
  
  return (
    <div className="confetti-button">
      <button onClick={evt => {
        evt.stopPropagation();

        if (isAnimating === true) {
          return false;
        }

        setIsAnimating(true);

        // Animate button by GSAP
        animateConfettiButton(evt.target, () => {
          // Add confetti JSX element into an array
          addConfetti(confettiArray, setConfettiArray);
        }, () => {
          // Reset the animation to beginning
          tl.progress(0);
          tl.clear();
          
          setIsAnimating(false);
        });
      }}>{props.text}</button>
      {confettiArray}
    </div>
  );
}

// Confetti🎉 button
ReactDOM.render(<ConfettiButton text="🎉Click Me!🎉" />, document.querySelector("#ConfettiButton"));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.co/gsap@3/dist/gsap.min.js