Bubbles Background
An interactive fluid bubble background component with animated colorful blobs that respond to user interaction.
Installation Guide
1
Install Dependencies
Tailwind CSS
npm install tailwindcss postcss autoprefixer && npx tailwindcss init -p
2
Setup Configuration
Create file:
/lib/utils.ts
/lib/utils.ts
Configuration1import { clsx, type ClassValue } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs));
6}
3
Copy Component Code
Bubbles Background.tsx
TypeScript1"use client";
2
3import React, { useEffect, useRef } from "react";
4import type { CSSProperties } from "react";
5
6interface BubblesProps {
7 backgroundColorA?: string;
8 backgroundColorB?: string;
9 bubbleColors?: {
10 colorA?: string;
11 colorB?: string;
12 colorC?: string;
13 colorD?: string;
14 colorE?: string;
15 interactive?: string;
16 };
17 /** Any valid CSS `mix-blend-mode` value */
18 blendMode?: CSSProperties["mixBlendMode"];
19 bubbleSize?: string;
20}
21
22const BubbleBackground: React.FC<BubblesProps> = ({
23 backgroundColorA = "rgb(108, 0, 162)",
24 backgroundColorB = "rgb(0, 17, 82)",
25 bubbleColors = {
26 colorA: "18, 113, 255",
27 colorB: "221, 74, 255",
28 colorC: "100, 220, 255",
29 colorD: "200, 50, 50",
30 colorE: "180, 180, 50",
31 interactive: "148, 100, 255",
32 },
33 blendMode = "hard-light",
34 bubbleSize = "80%",
35}) => {
36 const interactiveRef = useRef<HTMLDivElement>(null);
37
38 useEffect(() => {
39 let curX = 0;
40 let curY = 0;
41 let tgX = 0;
42 let tgY = 0;
43 const easeFactor = 10;
44
45 function move() {
46 if (!interactiveRef.current) return;
47
48 curX += (tgX - curX) / easeFactor;
49 curY += (tgY - curY) / easeFactor;
50
51 interactiveRef.current.style.transform = `translate(${Math.round(
52 curX
53 )}px, ${Math.round(curY)}px)`;
54 requestAnimationFrame(move);
55 }
56
57 const handlePointerMove = (e: PointerEvent) => {
58 tgX = e.clientX;
59 tgY = e.clientY;
60 };
61
62 window.addEventListener("pointermove", handlePointerMove);
63 move();
64
65 return () => {
66 window.removeEventListener("pointermove", handlePointerMove);
67 };
68 }, []);
69
70 const bounceVAnimation = `
71 @keyframes bounceV {
72 0% { transform: translateY(-50%); }
73 50% { transform: translateY(50%); }
74 100% { transform: translateY(-50%); }
75 }
76 `;
77
78 const bounceHAnimation = `
79 @keyframes bounceH {
80 0% { transform: translateX(-50%) translateY(-10%); }
81 50% { transform: translateX(50%) translateY(10%); }
82 100% { transform: translateX(-50%) translateY(-10%); }
83 }
84 `;
85
86 const moveInCircleAnimation = `
87 @keyframes moveInCircle {
88 0% { transform: rotate(0deg); }
89 50% { transform: rotate(180deg); }
90 100% { transform: rotate(360deg); }
91 }
92 `;
93
94 const gooFilter = `
95 <filter id="goo">
96 <feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
97 <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" />
98 <feBlend in="SourceGraphic" in2="goo" />
99 </filter>
100 `;
101
102 return (
103 <>
104 <style jsx global>{`
105 @import url("https://fonts.googleapis.com/css2?family=DynaPuff:wght@400..700&display=swap");
106 ${bounceVAnimation}
107 ${bounceHAnimation}
108 ${moveInCircleAnimation}
109 `}</style>
110
111 <div
112 className="w-screen h-screen relative overflow-hidden"
113 style={{
114 background: `linear-gradient(40deg, ${backgroundColorA}, ${backgroundColorB})`,
115 }}
116 >
117 <svg
118 className="hidden"
119 xmlns="http://www.w3.org/2000/svg"
120 dangerouslySetInnerHTML={{ __html: gooFilter }}
121 />
122
123 <div
124 className="w-full h-full"
125 style={{
126 filter: "url(#goo) blur(40px)",
127 }}
128 >
129 <div
130 className="absolute opacity-100"
131 style={{
132 width: bubbleSize,
133 height: bubbleSize,
134 top: `calc(50% - ${bubbleSize} / 2)`,
135 left: `calc(50% - ${bubbleSize} / 2)`,
136 background: `radial-gradient(circle at center, rgba(${bubbleColors.colorA}, 0.8) 0, rgba(${bubbleColors.colorA}, 0) 50%) no-repeat`,
137 mixBlendMode: blendMode,
138 transformOrigin: "center center",
139 animation: "bounceV 30s ease infinite",
140 }}
141 ></div>
142
143 <div
144 className="absolute opacity-100"
145 style={{
146 width: bubbleSize,
147 height: bubbleSize,
148 top: `calc(50% - ${bubbleSize} / 2)`,
149 left: `calc(50% - ${bubbleSize} / 2)`,
150 background: `radial-gradient(circle at center, rgba(${bubbleColors.colorB}, 0.8) 0, rgba(${bubbleColors.colorB}, 0) 50%) no-repeat`,
151 mixBlendMode: blendMode,
152 transformOrigin: "calc(50% - 400px)",
153 animation: "moveInCircle 20s reverse infinite",
154 }}
155 ></div>
156
157 <div
158 className="absolute opacity-100"
159 style={{
160 width: bubbleSize,
161 height: bubbleSize,
162 top: `calc(50% - ${bubbleSize} / 2 + 200px)`,
163 left: `calc(50% - ${bubbleSize} / 2 - 500px)`,
164 background: `radial-gradient(circle at center, rgba(${bubbleColors.colorC}, 0.8) 0, rgba(${bubbleColors.colorC}, 0) 50%) no-repeat`,
165 mixBlendMode: blendMode,
166 transformOrigin: "calc(50% + 400px)",
167 animation: "moveInCircle 40s linear infinite",
168 }}
169 ></div>
170 <div
171 className="absolute opacity-70"
172 style={{
173 width: bubbleSize,
174 height: bubbleSize,
175 top: `calc(50% - ${bubbleSize} / 2)`,
176 left: `calc(50% - ${bubbleSize} / 2)`,
177 background: `radial-gradient(circle at center, rgba(${bubbleColors.colorD}, 0.8) 0, rgba(${bubbleColors.colorD}, 0) 50%) no-repeat`,
178 mixBlendMode: blendMode,
179 transformOrigin: "calc(50% - 200px)",
180 animation: "bounceH 40s ease infinite",
181 }}
182 ></div>
183
184 <div
185 className="absolute opacity-100"
186 style={{
187 width: `calc(${bubbleSize} * 2)`,
188 height: `calc(${bubbleSize} * 2)`,
189 top: `calc(50% - ${bubbleSize})`,
190 left: `calc(50% - ${bubbleSize})`,
191 background: `radial-gradient(circle at center, rgba(${bubbleColors.colorE}, 0.8) 0, rgba(${bubbleColors.colorE}, 0) 50%) no-repeat`,
192 mixBlendMode: blendMode,
193 transformOrigin: "calc(50% - 800px) calc(50% + 200px)",
194 animation: "moveInCircle 20s ease infinite",
195 }}
196 ></div>
197
198 <div
199 ref={interactiveRef}
200 className="absolute w-full h-full opacity-70"
201 style={{
202 top: "-50%",
203 left: "-50%",
204 background: `radial-gradient(circle at center, rgba(${bubbleColors.interactive}, 0.8) 0, rgba(${bubbleColors.interactive}, 0) 50%) no-repeat`,
205 mixBlendMode: blendMode,
206 }}
207 ></div>
208 </div>
209 </div>
210 </>
211 );
212};
213
214export default BubbleBackground;
215
4
Final Steps
Update Import Paths
Make sure to update the import paths in the component code to match your project structure. For example, change @/components/ui/button
to match your UI components location.
Props
Name | Type | Default | Description |
---|---|---|---|
backgroundColorA | string | "rgb(108, 0, 162)" | First color for the background gradient. |
backgroundColorB | string | "rgb(0, 17, 82)" | Second color for the background gradient. |
bubbleColors | object | {
colorA: "18, 113, 255",
colorB: "221, 74, 255",
colorC: "100, 220, 255",
colorD: "200, 50, 50",
colorE: "180, 180, 50",
interactive: "148, 100, 255"
} | RGB color values for different bubbles and the interactive bubble. |
blendMode | string | "hard-light" | CSS blend mode for the bubble elements. |
bubbleSize | string | "80%" | Size of the bubble elements relative to the container. |