A dreamy glass bubble that floats over your content — add to your site in seconds
Move your mouse over the image
Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. Adjust tint, blur radius, vibrancy, and shape. Works beautifully over any background — photos, gradients, or videos.
"use client";
import React, { useRef, useEffect, useState } from "react";
export interface FrostedBubbleProps {
/** Background image URL */
backgroundImage?: string;
/** Tint opacity (0-1, default 0.12) */
tint?: number;
/** Color vibrancy boost (default 1.18) */
vibrancy?: number;
/** Blur radius in pixels (default 18) */
blurRadius?: number;
/** Width of the glass bubble */
width?: number;
/** Height of the glass bubble */
height?: number;
/** Corner roundness - use 9999 for pill shape */
roundness?: number;
/** Whether bubble follows mouse */
followMouse?: boolean;
/** Container width */
containerWidth?: number;
/** Container height */
containerHeight?: number;
/** Optional className */
className?: string;
}
export default function FrostedBubble({
backgroundImage,
tint = 0.12,
vibrancy = 1.18,
blurRadius = 18,
width = 320,
height = 128,
roundness = 9999,
followMouse = true,
containerWidth = 400,
containerHeight = 500,
className = "",
}: FrostedBubbleProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [targetPosition, setTargetPosition] = useState({ x: 0, y: 0 });
const animationRef = useRef<number | null>(null);
// Generate unique filter ID
const blurFxId = useRef(`blur-fx-${Math.random().toString(36).substr(2, 9)}`);
useEffect(() => {
if (!followMouse) return;
const container = containerRef.current;
if (!container) return;
const handleMouseMove = (e: MouseEvent) => {
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left - containerWidth / 2;
const y = e.clientY - rect.top - containerHeight / 2;
setTargetPosition({ x, y });
};
const handleMouseLeave = () => {
setTargetPosition({ x: 0, y: 0 });
};
container.addEventListener("mousemove", handleMouseMove);
container.addEventListener("mouseleave", handleMouseLeave);
return () => {
container.removeEventListener("mousemove", handleMouseMove);
container.removeEventListener("mouseleave", handleMouseLeave);
};
}, [followMouse, containerWidth, containerHeight]);
// Smooth animation loop
useEffect(() => {
if (!followMouse) return;
const animate = () => {
setPosition((prev) => ({
x: prev.x + (targetPosition.x - prev.x) * 0.08,
y: prev.y + (targetPosition.y - prev.y) * 0.08,
}));
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [followMouse, targetPosition]);
return (
<div
ref={containerRef}
className={`relative overflow-hidden ${className}`}
style={{
width: containerWidth,
height: containerHeight,
}}
>
{/* Background Image */}
{backgroundImage && (
<div
className="absolute inset-0 bg-cover bg-center"
style={{
backgroundImage: `url(${backgroundImage})`,
}}
/>
)}
{/* SVG Filter for Blur Effect */}
<svg className="absolute w-0 h-0" aria-hidden="true">
<defs>
<filter id={blurFxId.current} x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation={blurRadius} result="blurred" />
<feColorMatrix
in="blurred"
type="matrix"
values={`1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 ${vibrancy} 0`}
result="enhanced"
/>
</filter>
</defs>
</svg>
{/* Glass Overlay */}
<div
className="absolute pointer-events-none"
style={{
left: "50%",
top: "50%",
transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${position.y}px))`,
width,
height,
borderRadius: roundness,
background: `rgba(0, 0, 0, ${tint})`,
backdropFilter: `url(#${blurFxId.current}) saturate(${vibrancy})`,
WebkitBackdropFilter: `url(#${blurFxId.current}) saturate(${vibrancy})`,
boxShadow: `
inset 0 0 2px 1px hsla(0, 0%, 100%, 0.32),
inset 0 0 8px 3px hsla(0, 0%, 100%, 0.14),
inset 0 3px 14px hsla(240, 10%, 8%, 0.06),
inset 0 6px 20px hsla(240, 10%, 8%, 0.06),
inset 0 5px 48px hsla(240, 10%, 8%, 0.06)
`,
transition: followMouse ? "none" : "transform 0.3s ease-out",
}}
/>
</div>
);
}
export { FrostedBubble };// BASIC: Minimal setup
import FrostedBubble from "@/components/FrostedBubble";
export default function MyComponent() {
return (
<FrostedBubble backgroundImage="/your-image.jpg" />
);
}
// HERO SECTION: Full-width with custom effect
export function HeroWithGlass() {
return (
<div className="relative w-full h-screen">
<FrostedBubble
backgroundImage="/hero-bg.jpg"
containerWidth={1200}
containerHeight={600}
width={400}
height={120}
tint={0.15}
blurRadius={24}
vibrancy={1.3}
/>
</div>
);
}
// CIRCLE: Perfect circle shape
export function CircleGlass() {
return (
<FrostedBubble
backgroundImage="/photo.jpg"
width={200}
height={200}
roundness={9999}
/>
);
}
// CARD: Rounded rectangle overlay
export function CardOverlay() {
return (
<FrostedBubble
backgroundImage="/card-bg.jpg"
width={350}
height={200}
roundness={24}
followMouse={false}
/>
);
}
// INTENSE: Strong blur effect
export function IntenseBlur() {
return (
<FrostedBubble
backgroundImage="/image.jpg"
tint={0.25}
blurRadius={40}
vibrancy={1.5}
/>
);
}