Bouncing SVG Balloons
Live previewPerformance varies by device
No account needed to copy or download. Check dependencies and thelicense status before publishing.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bouncing SVG Balloons</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #f8fafc;
font-family: sans-serif;
cursor: pointer;
/* Interaction hint */
}
#container {
width: 100vw;
height: 100vh;
position: relative;
}
.balloon {
position: absolute;
transform-origin: bottom center;
will-change: transform, top, left;
}
.string {
stroke: #cbd5e1;
stroke-width: 2;
fill: none;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
const container = document.getElementById('container');
const colors = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899'];
let balloons = [];
const config = {
count: 15,
gravity: 0.15,
bounce: -0.6,
friction: 0.98
};
class Balloon {
constructor(x, y, isClick = false) {
this.x = x || Math.random() * window.innerWidth;
this.y = y || Math.random() * (window.innerHeight / 2) - window.innerHeight; // Start offscreen top
this.vx = (Math.random() - 0.5) * 4;
this.vy = isClick ? -5 - Math.random() * 5 : 0;
this.radiusX = 30 + Math.random() * 15;
this.radiusY = this.radiusX * 1.3;
this.color = colors[Math.floor(Math.random() * colors.length)];
this.element = this.createSVG();
container.appendChild(this.element);
}
createSVG() {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const width = this.radiusX * 2.5;
const height = this.radiusY * 2.5 + 60; // Extra for string
svg.setAttribute('width', width);
svg.setAttribute('height', height);
svg.setAttribute('class', 'balloon');
// Definitions for gradient
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
const gradient = document.createElementNS("http://www.w3.org/2000/svg", "radialGradient");
const id = 'grad-' + Math.random().toString(36).substr(2, 9);
gradient.setAttribute('id', id);
gradient.setAttribute('cx', '30%');
gradient.setAttribute('cy', '30%');
gradient.setAttribute('r', '50%');
const stop1 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
stop1.setAttribute('offset', '0%');
stop1.setAttribute('stop-color', '#ffffff');
stop1.setAttribute('stop-opacity', '0.6');
const stop2 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
stop2.setAttribute('offset', '100%');
stop2.setAttribute('stop-color', this.color);
gradient.appendChild(stop1);
gradient.appendChild(stop2);
defs.appendChild(gradient);
svg.appendChild(defs);
// String
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute('d', `M ${width / 2} ${this.radiusY * 2 + 5} Q ${width / 2 + 10} ${this.radiusY * 2 + 30} ${width / 2 - 5} ${height}`);
path.setAttribute('class', 'string');
svg.appendChild(path);
// Tie
const tie = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
tie.setAttribute('points', `${width / 2},${this.radiusY * 2} ${width / 2 - 6},${this.radiusY * 2 + 8} ${width / 2 + 6},${this.radiusY * 2 + 8}`);
tie.setAttribute('fill', this.color);
svg.appendChild(tie);
// Balloon Body
const ellipse = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
ellipse.setAttribute('cx', width / 2);
ellipse.setAttribute('cy', this.radiusY);
ellipse.setAttribute('rx', this.radiusX);
ellipse.setAttribute('ry', this.radiusY);
ellipse.setAttribute('fill', `url(#${id})`);
svg.appendChild(ellipse);
return svg;
}
update() {
this.vy += config.gravity;
this.vx *= config.friction;
this.x += this.vx;
this.y += this.vy;
const ground = window.innerHeight - (this.radiusY * 2.5 + 60);
// Floor collision
if (this.y > ground) {
this.y = ground;
this.vy *= config.bounce;
this.vx *= 0.9; // Extra floor friction
}
// Wall collision
if (this.x < 0) {
this.x = 0;
this.vx *= -1;
} else if (this.x > window.innerWidth - this.radiusX * 2.5) {
this.x = window.innerWidth - this.radiusX * 2.5;
this.vx *= -1;
}
// Squeeze effect based on velocity
const scaleY = 1 - Math.min(Math.abs(this.vy) * 0.02, 0.3);
const scaleX = 1 + Math.min(Math.abs(this.vy) * 0.01, 0.15);
const rotation = this.vx * 2;
this.element.style.transform = `translate(${this.x}px, ${this.y}px) rotate(${rotation}deg) scale(${scaleX}, ${scaleY})`;
}
}
function init() {
container.innerHTML = '';
balloons = [];
for (let i = 0; i < config.count; i++) {
setTimeout(() => {
balloons.push(new Balloon());
}, i * 200); // Staggered spawn
}
}
function animate() {
for (let i = 0; i < balloons.length; i++) {
balloons[i].update();
}
requestAnimationFrame(animate);
}
window.addEventListener('click', (e) => {
// Spawn 3 balloons on click
for (let i = 0; i < 3; i++) {
balloons.push(new Balloon(e.clientX - 30, window.innerHeight, true));
}
// Keep array size manageable
if (balloons.length > config.count + 15) {
const old = balloons.shift();
old.element.remove();
}
});
init();
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
// Static frame: once the staggered balloons have spawned,
// fast-forward the physics so they settle into view, then stop.
setTimeout(() => {
for (let k = 0; k < 400; k++) balloons.forEach(b => b.update());
}, config.count * 200 + 100);
} else {
animate();
}
</script>
</body>
</html>About this animation
Gravity-affected SVG balloons that bounce and settle interactively on the bottom of the screen.
Under the hood
Gravity and Elastic Collision Dynamics
This script simulates basic Newtonian physics on dynamically generated SVG elements. Variables for
gravity (0.15) and friction pull the nodes downward until they hit the bottom of the window innerHeight, at which point an inverse bounce scalar ricochets them realistically back upwards.Related