Animated gradient background with mouse tracking — add to your site in seconds
Move your mouse over the gradient
Copy URL or Code
Paste to your AI coding assistant and say:
“Add this lava bubbles background to my hero section”
Done. Your AI handles the rest.
Fully customizable. Ask your AI to change colors, animation speeds, transparency, blur amount, or blob sizes.
"use client";
import React, { useEffect, useRef } from "react";
interface LavaBubblesProps {
children?: React.ReactNode;
className?: string;
}
/**
* Lava Bubbles - A mesmerizing animated gradient background
* with gooey liquid effect and mouse tracking
*/
export function LavaBubbles({ children, className = "" }: LavaBubblesProps) {
const containerRef = useRef<HTMLDivElement>(null);
const interactiveRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let curX = 0;
let curY = 0;
let tgX = 0;
let tgY = 0;
let animationId: number;
let isHovering = false;
const handleMouseMove = (event: MouseEvent) => {
if (!containerRef.current || !isHovering) return;
const rect = containerRef.current.getBoundingClientRect();
tgX = event.clientX - rect.left - rect.width / 2;
tgY = event.clientY - rect.top - rect.height / 2;
};
const handleMouseEnter = () => {
isHovering = true;
};
const handleMouseLeave = () => {
isHovering = false;
tgX = 0;
tgY = 0;
};
const animate = () => {
if (!interactiveRef.current) return;
curX += (tgX - curX) / 20;
curY += (tgY - curY) / 20;
interactiveRef.current.style.transform = `translate(${Math.round(curX)}px, ${Math.round(curY)}px)`;
animationId = requestAnimationFrame(animate);
};
const container = containerRef.current;
if (container) {
container.addEventListener("mousemove", handleMouseMove);
container.addEventListener("mouseenter", handleMouseEnter);
container.addEventListener("mouseleave", handleMouseLeave);
}
animate();
return () => {
if (container) {
container.removeEventListener("mousemove", handleMouseMove);
container.removeEventListener("mouseenter", handleMouseEnter);
container.removeEventListener("mouseleave", handleMouseLeave);
}
cancelAnimationFrame(animationId);
};
}, []);
return (
<div
ref={containerRef}
className={`relative w-full h-full overflow-hidden ${className}`}
style={{
"--color-bg1": "rgb(235, 180, 150)",
"--color-bg2": "rgb(220, 160, 130)",
"--color1": "180, 60, 50",
"--color2": "255, 230, 150",
"--color3": "220, 100, 70",
"--color4": "255, 200, 100",
"--color5": "140, 50, 80",
"--color-interactive": "255, 220, 130",
"--circle-size": "80%",
} as React.CSSProperties}
>
<div
className="absolute inset-0 overflow-hidden"
style={{ background: "linear-gradient(40deg, var(--color-bg1), var(--color-bg2))" }}
>
<svg xmlns="http://www.w3.org/2000/svg" className="fixed top-0 left-0 w-0 h-0">
<defs>
<filter id="lava-goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -8" result="goo" />
<feBlend in="SourceGraphic" in2="goo" />
</filter>
</defs>
</svg>
<div className="w-full h-full" style={{ filter: "url(#lava-goo) blur(40px)" }}>
<div className="absolute" style={{ background: "radial-gradient(circle at center, rgba(var(--color1), 1) 0, rgba(var(--color1), 0) 50%) no-repeat", mixBlendMode: "normal", width: "var(--circle-size)", height: "var(--circle-size)", top: "calc(50% - var(--circle-size) / 2)", left: "calc(50% - var(--circle-size) / 2)", transformOrigin: "center center", animation: "moveVertical 8s ease infinite" }} />
<div className="absolute" style={{ background: "radial-gradient(circle at center, rgba(var(--color2), 1) 0, rgba(var(--color2), 0) 50%) no-repeat", mixBlendMode: "normal", width: "var(--circle-size)", height: "var(--circle-size)", top: "calc(50% - var(--circle-size) / 2)", left: "calc(50% - var(--circle-size) / 2)", transformOrigin: "calc(50% - 400px)", animation: "moveInCircle 6s reverse infinite" }} />
<div className="absolute" style={{ background: "radial-gradient(circle at center, rgba(var(--color3), 1) 0, rgba(var(--color3), 0) 50%) no-repeat", mixBlendMode: "normal", width: "var(--circle-size)", height: "var(--circle-size)", top: "calc(50% - var(--circle-size) / 2 + 200px)", left: "calc(50% - var(--circle-size) / 2 - 500px)", transformOrigin: "calc(50% + 400px)", animation: "moveInCircle 10s linear infinite" }} />
<div className="absolute" style={{ background: "radial-gradient(circle at center, rgba(var(--color4), 1) 0, rgba(var(--color4), 0) 50%) no-repeat", mixBlendMode: "normal", width: "var(--circle-size)", height: "var(--circle-size)", top: "calc(50% - var(--circle-size) / 2)", left: "calc(50% - var(--circle-size) / 2)", transformOrigin: "calc(50% - 200px)", animation: "moveHorizontal 10s ease infinite" }} />
<div className="absolute" style={{ background: "radial-gradient(circle at center, rgba(var(--color5), 1) 0, rgba(var(--color5), 0) 50%) no-repeat", mixBlendMode: "normal", width: "calc(var(--circle-size) * 2)", height: "calc(var(--circle-size) * 2)", top: "calc(50% - var(--circle-size))", left: "calc(50% - var(--circle-size))", transformOrigin: "calc(50% - 800px) calc(50% + 200px)", animation: "moveInCircle 7s ease infinite" }} />
<div ref={interactiveRef} className="absolute" style={{ background: "radial-gradient(circle at center, rgba(var(--color-interactive), 1) 0, rgba(var(--color-interactive), 0) 50%) no-repeat", mixBlendMode: "normal", width: "var(--circle-size)", height: "var(--circle-size)", top: "calc(50% - var(--circle-size) / 2)", left: "calc(50% - var(--circle-size) / 2)" }} />
</div>
</div>
{children && <div className="relative z-10 w-full h-full">{children}</div>}
<style jsx global>{`
@keyframes moveInCircle {
0% { transform: rotate(0deg); }
50% { transform: rotate(180deg); }
100% { transform: rotate(360deg); }
}
@keyframes moveVertical {
0% { transform: translateY(-50%); }
50% { transform: translateY(50%); }
100% { transform: translateY(-50%); }
}
@keyframes moveHorizontal {
0% { transform: translateX(-50%) translateY(-10%); }
50% { transform: translateX(50%) translateY(10%); }
100% { transform: translateX(-50%) translateY(-10%); }
}
`}</style>
</div>
);
}
export default LavaBubbles;import LavaBubbles from "@/components/LavaBubbles";
// Basic usage - fills parent container
export default function HeroSection() {
return (
<div className="h-screen">
<LavaBubbles />
</div>
);
}
// With content overlay
export function HeroWithContent() {
return (
<div className="h-screen">
<LavaBubbles>
<div className="flex items-center justify-center h-full">
<h1 className="text-5xl font-bold text-white drop-shadow-lg">
Welcome to My Site
</h1>
</div>
</LavaBubbles>
</div>
);
}
// Fixed height section
export function FixedHeightSection() {
return (
<div className="h-[500px]">
<LavaBubbles className="rounded-2xl" />
</div>
);
}