#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
This Pen doesn't use any external CSS resources.