雨や梅雨を表現するCSS・JSアニメーションまとめ

特集

雨や梅雨を表現するCSS・JSアニメーションまとめ

投稿日2026/07/06

更新日2026/7/5

梅雨の季節感を演出したいときや、雨をテーマにしたWebサイトを制作したいときに使えるCSS・JSアニメーションをまとめました。

雨粒が落ちるアニメーションや雲の演出、水滴を表現したエフェクトなど、デザインのアクセントになる実装を掲載しています。

コピペで試せるサンプルや実装方法も紹介しているので、ぜひ制作の参考にしてみてください。

CSSやJSで作る、窓ガラスの雨粒が流れるイマーシブ背景エフェクト

<img id="custom-bg" src="https://images.unsplash.com/photo-1448375240586-882707db888b?w=1920&q=80" alt="Misty forest background">
<canvas id="bg-canvas" aria-hidden="true"></canvas>
* {
	margin: 0;
	padding: 0;
	box-sizing: border-box;
}
html,
body {
	width: 100%;
	height: 100%;
	overflow: hidden;
	background: #000;
}
/* 背景画像 - 霧の深い森のフリー素材 */
#custom-bg {
	position: fixed;
	top: 50%;
	left: 50%;
	min-width: 100%;
	min-height: 100%;
	width: auto;
	height: auto;
	transform: translate(-50%, -50%) scale(1.05);
	z-index: 0;
	object-fit: cover;
	/* 暗めのフィルターで雨を際立たせる */
	filter: blur(4px) brightness(0.45);
	will-change: filter;
}
/* 雨のエフェクトキャンバス */
#bg-canvas {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	z-index: 1;
	mix-blend-mode: screen;
	opacity: 0.95;
	pointer-events: none;
}
const canvas = document.getElementById("bg-canvas");
    const customBg = document.getElementById("custom-bg");

    // 雨のエフェクト初期化
    function initRaindrops() {
      if (typeof Raindrops === "undefined") return;
      
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      
      try {
        new Raindrops(canvas, canvas.width, canvas.height, {
          fg: customBg,
          bg: customBg,
          rainChance: 0.4,
          brightness: 1.1,
          renderDropsOnTop: true
        });
      } catch(e) {
        console.error("Rain effect failed:", e);
      }
    }

    // 外部スクリプトのロード
    function loadRainScript() {
      const script = document.createElement("script");
      script.src = "https://fyildiz1974.github.io/web/files/raindrops.js";
      script.onload = () => {
        if (customBg.complete) {
          initRaindrops();
        } else {
          customBg.onload = initRaindrops;
        }
      };
      document.head.appendChild(script);
    }

    // 開始
    document.addEventListener("DOMContentLoaded", () => {
      loadRainScript();

      window.addEventListener("resize", () => {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
      });
    });
背景画像にCSSブラー+暗転を加え、Canvas上にRaindropsライブラリで雨粒エフェクトを重ねる没入型の背景演出。

窓ガラス越しに雨を眺めるような雰囲気を、軽量な構成で実現します。

Canvas2Dで作る、文字に雨が当たって流れ落ちる梅雨エフェクト

<div class="kumonosu-font-loader">梅雨の大雨</div>
<canvas id="kumonosu-rainCanvas"></canvas>
<div class="kumonosu-vignette"></div>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@900&display=swap');
body, html {
	margin: 0;
	padding: 0;
	width: 100%;
	height: 100%;
	overflow: hidden;
	background-color: #020202;
}
#kumonosu-rainCanvas {
	display: block;
	position: absolute;
	top: 0;
	left: 0;
}
.kumonosu-font-loader {
	font-family: 'Noto Sans JP';
	font-weight: 900;
	position: absolute;
	opacity: 0;
	pointer-events: none;
}
.kumonosu-vignette {
	pointer-events: none;
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	background: radial-gradient(circle at center, transparent 0%, rgba(0, 0, 0, 0.8) 100%);
}
const canvas = document.getElementById('kumonosu-rainCanvas');
const ctx = canvas.getContext('2d', {
	willReadFrequently: true
});
const hitCanvas = document.createElement('canvas');
const hitCtx = hitCanvas.getContext('2d', {
	willReadFrequently: true
});
const cloudCanvas = document.createElement('canvas');
const cloudCtx = cloudCanvas.getContext('2d');
let width, height;
let drops = [];
let splashes = [];
let cloudLayers = [];
let lightningBolts = [];
const dropCount = 1300;
const textString = "梅雨の大雨";
const fontFamily = "'Noto Sans JP'";
const RAIN_ANGLE = 5;
const rainAngleRad = (RAIN_ANGLE * Math.PI) / 180;
const rainVelocityX = Math.sin(rainAngleRad);
const rainVelocityY = Math.cos(rainAngleRad);
let isLightning = false;
let lightningTimer = 0;
let lightningIntensity = 0;

function resize() {
	width = canvas.width = window.innerWidth;
	height = canvas.height = window.innerHeight;
	hitCanvas.width = width;
	hitCanvas.height = height;
	cloudCanvas.width = width;
	cloudCanvas.height = Math.floor(height * 0.6);
	drawCollisionMap();
	initClouds();
}

function getResponsiveFontSize() {
	return Math.min(Math.max(width * 0.16, 80), 300);
}

function drawCollisionMap() {
	hitCtx.fillStyle = 'black';
	hitCtx.fillRect(0, 0, width, height);
	let fontSize = getResponsiveFontSize();
	hitCtx.font = `900 ${fontSize}px ${fontFamily}`;
	hitCtx.textAlign = 'center';
	hitCtx.textBaseline = 'middle';
	hitCtx.fillStyle = 'white';
	hitCtx.fillText(textString, width / 2, height / 2);
}

function draw3DText() {
	let fontSize = getResponsiveFontSize();
	ctx.font = `900 ${fontSize}px ${fontFamily}`;
	ctx.textAlign = 'center';
	ctx.textBaseline = 'middle';
	const cx = width / 2;
	const cy = height / 2;
	ctx.shadowColor = 'black';
	ctx.shadowBlur = 30;
	ctx.shadowOffsetY = 25;
	ctx.fillStyle = 'black';
	ctx.fillText(textString, cx, cy);
	ctx.shadowBlur = 0;
	ctx.shadowOffsetY = 0;
	ctx.fillStyle = '#111';
	ctx.fillText(textString, cx + 4, cy + 4);
	let gradient = ctx.createLinearGradient(0, cy - fontSize / 2, 0, cy + fontSize / 2);
	gradient.addColorStop(0, '#050505');
	gradient.addColorStop(0.3, '#1a1a1a');
	gradient.addColorStop(0.5, '#080808');
	gradient.addColorStop(1, '#000');
	ctx.fillStyle = gradient;
	ctx.fillText(textString, cx, cy);
	ctx.save();
	ctx.fillStyle = isLightning ? `rgba(255,255,255,${0.1 + lightningIntensity * 0.8})` : 'rgba(255,255,255,0.08)';
	ctx.fillText(textString, cx, cy - 1);
	ctx.restore();
}

function createCloudTexture(size, brightness) {
	const tempCanvas = document.createElement('canvas');
	tempCanvas.width = size;
	tempCanvas.height = size;
	const tempCtx = tempCanvas.getContext('2d');
	const centerX = size / 2;
	const centerY = size / 2;
	for (let i = 0; i < 3; i++) {
		const offsetX = (Math.random() - 0.5) * size * 0.3;
		const offsetY = (Math.random() - 0.5) * size * 0.3;
		const radius = size * (0.4 + Math.random() * 0.2);
		const gradient = tempCtx.createRadialGradient(centerX + offsetX, centerY + offsetY, 0, centerX + offsetX, centerY + offsetY, radius);
		const alpha = 0.12 + Math.random() * 0.1;
		gradient.addColorStop(0, `rgba(${brightness + 20}, ${brightness + 20}, ${brightness + 30}, ${alpha})`);
		gradient.addColorStop(0.5, `rgba(${brightness}, ${brightness}, ${brightness + 10}, ${alpha * 0.5})`);
		gradient.addColorStop(1, `rgba(${brightness - 10}, ${brightness - 10}, ${brightness}, 0)`);
		tempCtx.fillStyle = gradient;
		tempCtx.fillRect(0, 0, size, size);
	}
	return tempCanvas;
}
class CloudLayer {
	constructor(depth, yPos) {
		this.depth = depth;
		this.speed = (0.12 + depth * 0.2);
		this.offset = 0;
		this.y = yPos;
		this.brightness = 20 + depth * 12;
		this.cloudTextures = [];
		const numClouds = 6;
		for (let i = 0; i < numClouds; i++) {
			const size = 300 + Math.random() * 500;
			this.cloudTextures.push({
				texture: createCloudTexture(size, this.brightness),
				size: size,
				x: (i / numClouds) * width * 1.5,
				offsetY: (Math.random() - 0.5) * 100
			});
		}
	}
	update() {
		this.offset += this.speed;
		if (this.offset > width * 1.5) this.offset = 0;
	}
	draw() {
		ctx.save();
		let alpha = 0.6 + this.depth * 0.4;
		if (isLightning && lightningIntensity > 0) alpha = Math.min(1, alpha + lightningIntensity * 0.4);
		ctx.globalAlpha = alpha;
		if (isLightning && lightningIntensity > 0) ctx.globalCompositeOperation = 'lighter';
		for (let loop = 0; loop < 2; loop++) {
			this.cloudTextures.forEach(cloud => {
				const x = cloud.x - this.offset + (loop * width * 1.5);
				const y = this.y + cloud.offsetY;
				if (x + cloud.size > -cloud.size && x < width + cloud.size) ctx.drawImage(cloud.texture, x - cloud.size / 2, y - cloud.size / 2);
			});
		}
		ctx.restore();
	}
}

function initClouds() {
	cloudLayers = [];
	cloudLayers.push(new CloudLayer(0.3, height * 0.1));
	cloudLayers.push(new CloudLayer(0.6, height * 0.2));
	cloudLayers.push(new CloudLayer(1.0, height * 0.35));
}
class LightningBolt {
	constructor() {
		this.segments = [];
		this.life = 1.0;
		this.brightness = Math.random() * 0.5 + 0.5;
		this.generateBolt();
	}
	generateBolt() {
		const startX = Math.random() * width;
		const startY = 0;
		const endX = startX + (Math.random() - 0.5) * 500;
		const endY = height * (Math.random() * 0.5 + 0.2);
		this.segments = this.createBranch(startX, startY, endX, endY, 1);
		if (Math.random() > 0.6) {
			const idx = Math.floor(this.segments.length * 0.5);
			const branchPoint = this.segments[idx];
			const branchEnd = {
				x: branchPoint.x + (Math.random() - 0.5) * 300,
				y: branchPoint.y + Math.random() * 200 + 50
			};
			this.segments.push(...this.createBranch(branchPoint.x, branchPoint.y, branchEnd.x, branchEnd.y, 0.5));
		}
	}
	createBranch(x1, y1, x2, y2, widthMult) {
		const segments = [];
		const steps = 12;
		let currentX = x1,
			currentY = y1;
		for (let i = 0; i <= steps; i++) {
			const t = i / steps;
			const targetX = x1 + (x2 - x1) * t + (Math.random() - 0.5) * 80;
			const targetY = y1 + (y2 - y1) * t;
			segments.push({
				x1: currentX,
				y1: currentY,
				x2: targetX,
				y2: targetY,
				width: (Math.random() * 2.5 + 1) * widthMult
			});
			currentX = targetX;
			currentY = targetY;
		}
		return segments;
	}
	update() {
		this.life -= 0.1;
	}
	draw() {
		ctx.save();
		ctx.globalAlpha = this.life * this.brightness;
		ctx.lineCap = 'round';
		ctx.shadowBlur = 25;
		ctx.shadowColor = 'rgba(210, 230, 255, 0.9)';
		this.segments.forEach(seg => {
			ctx.strokeStyle = 'rgba(180, 210, 255, 0.2)';
			ctx.lineWidth = seg.width * 12;
			ctx.beginPath();
			ctx.moveTo(seg.x1, seg.y1);
			ctx.lineTo(seg.x2, seg.y2);
			ctx.stroke();
			ctx.strokeStyle = 'rgba(255, 255, 255, 1)';
			ctx.lineWidth = seg.width * 2;
			ctx.beginPath();
			ctx.moveTo(seg.x1, seg.y1);
			ctx.lineTo(seg.x2, seg.y2);
			ctx.stroke();
		});
		ctx.restore();
	}
}
class Splash {
	constructor(x, y, impactAngle) {
		this.x = x;
		this.y = y;
		const baseAngle = -Math.PI / 2 - rainAngleRad;
		const spreadAngle = (Math.random() - 0.5) * Math.PI * 0.7;
		const angle = baseAngle + spreadAngle;
		const speed = Math.random() * 5 + 2;
		this.vx = Math.cos(angle) * speed;
		this.vy = Math.sin(angle) * speed;
		this.life = 1.0;
		this.size = Math.random() * 2 + 0.5;
	}
	update() {
		this.x += this.vx;
		this.y += this.vy;
		this.vy += 0.4;
		this.vx *= 0.97;
		this.life -= 0.05;
	}
	draw() {
		ctx.fillStyle = `rgba(240, 245, 255, ${this.life})`;
		ctx.fillRect(this.x, this.y, this.size, this.size);
	}
}
class Drop {
	constructor() {
		this.reset();
		this.y = Math.random() * height;
		this.x = Math.random() * width + height * rainVelocityX;
	}
	reset() {
		this.x = Math.random() * width - height * 0.3 * rainVelocityX;
		this.y = -20;
		const baseSpeed = Math.random() * 14 + 11;
		this.vy = baseSpeed * rainVelocityY;
		this.baseVx = baseSpeed * rainVelocityX;
		this.vx = this.baseVx;
		this.state = 'falling';
		this.size = Math.random() * 1.4 + 1.2;
		this.oscillationSpeed = Math.random() * 0.08 + 0.04;
		this.phase = Math.random() * Math.PI * 2;
		this.z = Math.random();
		this.canCollide = (this.z > 0.46 && this.z < 0.54);
	}
	isSolid(tx, ty) {
		if (ty < 0 || ty >= height || tx < 0 || tx >= width) return false;
		if (ty > height / 2 - 200 && ty < height / 2 + 200) {
			const pixel = hitCtx.getImageData(Math.floor(tx), Math.floor(ty), 1, 1).data;
			return pixel[0] > 120;
		}
		return false;
	}
	update() {
		let nextY = this.y + this.vy;
		let nextX = this.x + this.vx;
		if (this.canCollide) {
			let isCurrentlyInSolid = this.isSolid(nextX, nextY);
			let wasInSolid = this.isSolid(this.x, this.y);
			if (this.state === 'falling' && isCurrentlyInSolid && !wasInSolid) {
				this.state = 'flowing';
				this.y = nextY;
				this.vy = 0;
				this.vx = 0;
				for (let k = 0; k < 6; k++) splashes.push(new Splash(this.x, this.y, rainAngleRad));
			} else if (this.state === 'flowing') {
				if (isCurrentlyInSolid) {
					if (Math.random() < 0.12) {
						this.vy = 0.08;
					} else {
						if (this.vy < 4) this.vy += 0.25;
					}
					let noise = (Math.random() - 0.5) * 1.5;
					this.vx = (Math.sin(this.y * this.oscillationSpeed + this.phase) * 0.4) + noise;
					this.y += this.vy;
					this.x += this.vx;
				} else {
					this.state = 'detaching';
					this.vx = this.baseVx * 0.4;
					this.vy = 1.5;
				}
			} else if (this.state === 'detaching') {
				this.y += this.vy;
				this.x += this.vx;
				this.vy += 0.45;
				this.vx += this.baseVx * 0.04;
				if (this.vy > 10) {
					this.state = 'falling';
					this.vx = this.baseVx;
				}
			} else {
				this.y = nextY;
				this.x = nextX;
				if (this.vy < 24 * rainVelocityY) this.vy += 0.5 * rainVelocityY;
			}
		} else {
			this.y = nextY;
			this.x = nextX;
			if (this.vy < 24 * rainVelocityY) this.vy += 0.5 * rainVelocityY;
		}
		if (this.y > height || this.x > width + 150 || this.x < -150) this.reset();
	}
	draw() {
		let opacity = this.z * 0.45;
		if (isLightning) opacity = Math.min(0.9, opacity + lightningIntensity * 0.5);
		if (this.state === 'falling') {
			ctx.fillStyle = `rgba(180, 205, 230, ${opacity})`;
			const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
			const len = speed * (this.z + 1.1);
			const angle = Math.atan2(this.vy, this.vx);
			ctx.save();
			ctx.translate(this.x, this.y);
			ctx.rotate(angle);
			ctx.fillRect(0, -0.6, len, 1.2);
			ctx.restore();
		} else {
			let stretch = this.vy * 16;
			if (stretch < this.size) stretch = this.size;
			let grad = ctx.createLinearGradient(this.x, this.y - stretch, this.x, this.y);
			grad.addColorStop(0, `rgba(255, 255, 255, 0)`);
			grad.addColorStop(1, `rgba(255, 255, 255, ${0.2 + (isLightning ? lightningIntensity * 0.6 : 0)})`);
			ctx.fillStyle = grad;
			ctx.beginPath();
			ctx.moveTo(this.x - this.size / 2, this.y);
			ctx.lineTo(this.x, this.y - stretch);
			ctx.lineTo(this.x + this.size / 2, this.y);
			ctx.fill();
			ctx.beginPath();
			ctx.fillStyle = `rgba(255, 255, 255, 0.12)`;
			ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
			ctx.fill();
		}
	}
}

function animate() {
	if (!isLightning && Math.random() < 0.005) {
		isLightning = true;
		lightningTimer = Math.random() * 15 + 10;
		lightningIntensity = 1.0;
		lightningBolts.push(new LightningBolt());
		if (Math.random() > 0.6) setTimeout(() => lightningBolts.push(new LightningBolt()), 100);
	}
	if (isLightning) {
		lightningTimer--;
		lightningIntensity = Math.max(0, lightningIntensity - 0.05);
		if (lightningTimer <= 0) {
			isLightning = false;
			lightningIntensity = 0;
		}
	}
	ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
	ctx.fillRect(0, 0, width, height);
	cloudLayers.forEach(layer => {
		layer.update();
		layer.draw();
	});
	for (let i = lightningBolts.length - 1; i >= 0; i--) {
		lightningBolts[i].update();
		lightningBolts[i].draw();
		if (lightningBolts[i].life <= 0) lightningBolts.splice(i, 1);
	}
	draw3DText();
	drops.forEach(drop => {
		drop.update();
		drop.draw();
	});
	for (let i = splashes.length - 1; i >= 0; i--) {
		splashes[i].update();
		splashes[i].draw();
		if (splashes[i].life <= 0) splashes.splice(i, 1);
	}
	requestAnimationFrame(animate);
}
window.addEventListener('resize', resize);
document.fonts.ready.then(() => {
	resize();
	for (let i = 0; i < dropCount; i++) drops.push(new Drop());
	animate();
});
Canvas 2Dで1300粒の雨が文字に衝突・飛沫・表面を流れ落ちるエフェクト。

ピクセルベースの衝突判定、稲妻、雲レイヤー、ビネットまで含めた梅雨の大雨を丸ごと再現する演出です。

Three.jsで作る、雷が光る3Dストームエフェクト

<canvas id="kumonosu-canvas"></canvas>
body {
	margin: 0;
	overflow: hidden;
	background-color: #000;
}
#kumonosu-canvas {
	display: block;
	width: 100vw;
	height: 100vh;
}
let scene, camera, renderer, cloudParticles = [],
	flash, rain, rainGeo, rainCount = 15000;

function init() {
	scene = new THREE.Scene();
	camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
	camera.position.z = 1;
	camera.rotation.x = 1.16;
	camera.rotation.y = -0.12;
	camera.rotation.z = 0.27;
	scene.add(new THREE.AmbientLight(0x555555));
	let directionalLight = new THREE.DirectionalLight(0xffeedd);
	directionalLight.position.set(0, 1, 0);
	scene.add(directionalLight);
	flash = new THREE.PointLight(0x062d89, 30, 500, 1.7);
	flash.position.set(200, 300, 100);
	scene.add(flash);
	// JS内での取得IDも kumonosu-canvas に変更
	const kumonosuCanvasElement = document.getElementById('kumonosu-canvas');
	renderer = new THREE.WebGLRenderer({
		canvas: kumonosuCanvasElement,
		antialias: true
	});
	scene.fog = new THREE.FogExp2(0x11111f, 0.002);
	renderer.setClearColor(scene.fog.color);
	renderer.setSize(window.innerWidth, window.innerHeight);
	// 雨の生成
	rainGeo = new THREE.Geometry();
	for (let j = 0; j < rainCount; j++) {
		let rainDrop = new THREE.Vector3(Math.random() * 400 - 200, Math.random() * 500 - 250, Math.random() * 400 - 200);
		rainDrop.velocity = 0;
		rainGeo.vertices.push(rainDrop);
	}
	let rainMaterial = new THREE.PointsMaterial({
		color: 0xaaaaaa,
		size: 0.1,
		transparent: true
	});
	rain = new THREE.Points(rainGeo, rainMaterial);
	scene.add(rain);
	// 雲の生成
	let loader = new THREE.TextureLoader();
	loader.load("https://assets.codepen.io/682745/cloud2.png", function(texture) {
		let cloudGeo = new THREE.PlaneBufferGeometry(500, 500);
		let cloudMaterial = new THREE.MeshLambertMaterial({
			map: texture,
			transparent: true
		});
		for (let i = 0; i < 25; i++) {
			let cloud = new THREE.Mesh(cloudGeo, cloudMaterial);
			cloud.position.set(Math.random() * 800 - 400, 500, Math.random() * 500 - 450);
			cloud.rotation.x = 1.16;
			cloud.rotation.y = -0.12;
			cloud.rotation.z = Math.random() * 360;
			cloud.material.opacity = 0.6;
			cloudParticles.push(cloud);
			scene.add(cloud);
		}
		animate();
	});
	window.addEventListener('resize', onWindowResize, false);
}

function onWindowResize() {
	camera.aspect = window.innerWidth / window.innerHeight;
	camera.updateProjectionMatrix();
	renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate() {
	cloudParticles.forEach(i => {
		i.rotation.z -= 0.002;
	});
	rainGeo.vertices.forEach(p => {
		p.velocity -= 0.1 + Math.random() * 0.1;
		p.y += p.velocity;
		if (p.y < -200) {
			p.y = 200;
			p.velocity = 0;
		}
	});
	rainGeo.verticesNeedUpdate = true;
	rain.rotation.y += 0.002;
	if (Math.random() > 0.93 || flash.power > 100) {
		if (flash.power < 100) {
			flash.position.set(Math.random() * 400, 300 + Math.random() * 200, 100);
		}
		flash.power = 50 + Math.random() * 500;
	}
	renderer.render(scene, camera);
	requestAnimationFrame(animate);
}
init();
https://cdnjs.cloudflare.com/ajax/libs/three.js/r124/three.min.js
15000粒の雨パーティクル、回転する雲テクスチャ25枚、ランダムな雷フラッシュを組み合わせた3Dストームシーン。

Three.jsのフォグとライティングで没入感のある嵐の雰囲気を演出します。

CSSだけで作る、アメトーークっぽいカラフルな雫が落ちるアニメーション

<div class="kumonosu-backdrop">
	<div class="kumonosu-container">
		<div class="kumonosu-cloud">
			<div class="kumonosu-drop kumonosu-drop-1">
				<div class="kumonosu-top"></div>
				<div class="kumonosu-bot"></div>
			</div>
			<div class="kumonosu-drop kumonosu-drop-2">
				<div class="kumonosu-top"></div>
				<div class="kumonosu-bot"></div>
			</div>
			<div class="kumonosu-drop kumonosu-drop-3">
				<div class="kumonosu-top"></div>
				<div class="kumonosu-bot"></div>
			</div>
		</div>
	</div>
</div>
* {
	box-sizing: border-box;
}
html, body {
	margin: 0;
	padding: 0;
	width: 100%;
	height: 100%;
	background: #ffffff;
	display: flex;
	justify-content: center;
	align-items: center;
	overflow: hidden;
}
.kumonosu-backdrop {
	width: 100%;
	height: 100dvh;
	display: flex;
	justify-content: center;
	align-items: center;
	position: relative;
}
.kumonosu-container {
	position: relative;
	display: flex;
	justify-content: center;
	align-items: center;
	transform: scale(clamp(0.4, 1vw + 0.5, 1.2));
	width: 500px;
	height: 400px;
}
.kumonosu-cloud {
	position: relative;
	width: 320px;
	height: 100px;
	background: #e0e0e0;
	border-radius: 100px;
	z-index: 10;
}
.kumonosu-cloud::before {
	content: "";
	position: absolute;
	top: -50px;
	left: 40px;
	width: 110px;
	height: 110px;
	border-radius: 50%;
	background: #e0e0e0;
	box-shadow: 90px -10px 0 30px #e0e0e0;
}
.kumonosu-drop {
	position: absolute;
	top: 100px;
	animation: kumonosu-drip 1.5s linear infinite;
	z-index: 1;
}
.kumonosu-top {
	height: 0px;
	width: 0px;
	border: 10px solid transparent;
	transform: rotate(-10deg) translateX(-4px);
}
.kumonosu-bot {
	position: relative;
	height: 19px;
	width: 19px;
	top: -9px;
	border-radius: 50%;
}
.kumonosu-drop-1 {
	left: 80px;
	animation-delay: 600ms;
}
.kumonosu-drop-1 .kumonosu-top {
	border-bottom: 28px solid #FF6B6B;
}
.kumonosu-drop-1 .kumonosu-bot {
	background: #FF6B6B;
}
.kumonosu-drop-2 {
	left: 170px;
	animation-delay: 0s;
}
.kumonosu-drop-2 .kumonosu-top {
	border-bottom: 28px solid #FFD93D;
}
.kumonosu-drop-2 .kumonosu-bot {
	background: #FFD93D;
}
.kumonosu-drop-3 {
	left: 260px;
	animation-delay: 1s;
}
.kumonosu-drop-3 .kumonosu-top {
	border-bottom: 28px solid #6BCB77;
}
.kumonosu-drop-3 .kumonosu-bot {
	background: #6BCB77;
}
@keyframes kumonosu-drip {
	0% {
		transform: translate(0px, 0px);
		opacity: 1;
	}
	50% {
		opacity: 1;
	}
	100% {
		transform: translate(40px, 200px);
		opacity: 0;
	}
}
@media (max-width: 400px) {
	.kumonosu-container {
		transform: scale(0.6);
	}
}
CSSだけで雲からカラフルな雫が落ちるアニメーション。

疑似要素とborderテクニックで雲と雫の形を再現し、animation-delayで時間差のある落下を表現。JS不要で軽量です。