Morphing Blob
Interactive blob elements that change shape dynamically. Works as background visuals, buttons, or section transitions.
Morphing Blob Components
Interactive, customizable fluid elements that add dynamic motion to your interface
Basic Components
Simple, lightweight blobs with various themes and complexities
Primary
Secondary
Accent
Advanced Options
Enhanced blobs with custom complexity, speed and glow effects
AI Powered
Quantum
Feature Showcase
Feature Highlight
Create engaging, dynamic UI elements that respond to user interaction
Custom Colors
Define your own gradient color schemes to match your brand identity
Responsive Design
Blobs adapt seamlessly to different screen sizes and device types
Installation Guide
Install Dependencies
Utility Functions
npm install clsx tailwind-merge
Setup Configuration
/lib/utils.ts
1import { clsx, type ClassValue } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4 export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs));
6 }
Copy Component Code
1"use client";
2import type React from "react";
3import { useRef, useEffect, useState, useMemo, useCallback } from "react";
4import { cn } from "@/lib/utils";
5
6export interface MorphingBlobProps
7 extends React.HTMLAttributes<HTMLDivElement> {
8 theme?:
9 | "primary"
10 | "secondary"
11 | "accent"
12 | "success"
13 | "warning"
14 | "danger"
15 | "custom";
16 customColors?: {
17 from: string;
18 via?: string;
19 to: string;
20 };
21 size?: "sm" | "md" | "lg" | "xl" | "full";
22 complexity?: 1 | 2 | 3 | 4 | 5;
23 speed?: 1 | 2 | 3 | 4 | 5;
24 hoverEffect?: boolean;
25 clickEffect?: boolean;
26 pulse?: boolean;
27 glow?: boolean;
28 glowIntensity?: 1 | 2 | 3 | 4 | 5;
29 opacity?: number;
30 smooth?: boolean;
31 effect3D?: boolean;
32 children?: React.ReactNode;
33}
34
35export function MorphingBlob({
36 theme = "primary",
37 customColors,
38 size = "md",
39 complexity = 3,
40 speed = 3,
41 hoverEffect = true,
42 clickEffect = true,
43 pulse = false,
44 glow = true,
45 glowIntensity = 3,
46 opacity = 100,
47 smooth = true,
48 effect3D = false,
49 className,
50 children,
51 ...props
52}: MorphingBlobProps) {
53 const blobRef = useRef<HTMLDivElement>(null);
54 const [isHovered, setIsHovered] = useState(false);
55 const [isClicked, setIsClicked] = useState(false);
56 const [blobPath, setBlobPath] = useState("");
57 const [prevBlobPath, setPrevBlobPath] = useState("");
58 const [rotation, setRotation] = useState(0);
59 const requestRef = useRef<number | null>(null);
60 const previousTimeRef = useRef<number | null>(null);
61 const animationProgress = useRef(0);
62
63 const themeColors = useMemo(
64 () => ({
65 primary: {
66 gradient: "from-blue-400 via-blue-600 to-indigo-700",
67 glow: "shadow-blue-500/60",
68 filter: "drop-shadow(0 0 8px rgba(59, 130, 246, 0.5))",
69 },
70 secondary: {
71 gradient: "from-purple-400 via-purple-600 to-violet-800",
72 glow: "shadow-purple-500/60",
73 filter: "drop-shadow(0 0 8px rgba(147, 51, 234, 0.5))",
74 },
75 accent: {
76 gradient: "from-teal-300 via-teal-500 to-emerald-700",
77 glow: "shadow-teal-500/60",
78 filter: "drop-shadow(0 0 8px rgba(20, 184, 166, 0.5))",
79 },
80 success: {
81 gradient: "from-green-300 via-green-500 to-emerald-700",
82 glow: "shadow-green-500/60",
83 filter: "drop-shadow(0 0 8px rgba(34, 197, 94, 0.5))",
84 },
85 warning: {
86 gradient: "from-yellow-300 via-amber-500 to-orange-600",
87 glow: "shadow-amber-500/60",
88 filter: "drop-shadow(0 0 8px rgba(245, 158, 11, 0.5))",
89 },
90 danger: {
91 gradient: "from-red-300 via-red-500 to-rose-700",
92 glow: "shadow-red-500/60",
93 filter: "drop-shadow(0 0 8px rgba(239, 68, 68, 0.5))",
94 },
95 custom: {
96 gradient: customColors
97 ? `from-[${customColors.from}] ${
98 customColors.via ? `via-[${customColors.via}]` : ""
99 } to-[${customColors.to}]`
100 : "",
101 glow: customColors
102 ? `shadow-[${customColors.from}]/60`
103 : "shadow-blue-500/60",
104 filter: `drop-shadow(0 0 8px ${
105 customColors?.from || "rgba(59, 130, 246, 0.5)"
106 })`,
107 },
108 }),
109 [customColors]
110 );
111
112 const currentTheme = themeColors[theme];
113
114 const sizeClasses = {
115 sm: "w-32 h-32 md:w-40 md:h-40",
116 md: "w-40 h-40 md:w-56 md:h-56",
117 lg: "w-56 h-56 md:w-72 md:h-72",
118 xl: "w-64 h-64 md:w-96 md:h-96",
119 full: "w-full h-full",
120 };
121
122 const complexityFactors = useMemo(
123 () => ({
124 1: { points: 6, variance: 0.15, tension: 0.2 },
125 2: { points: 8, variance: 0.25, tension: 0.3 },
126 3: { points: 10, variance: 0.35, tension: 0.4 },
127 4: { points: 12, variance: 0.45, tension: 0.5 },
128 5: { points: 16, variance: 0.55, tension: 0.6 },
129 }),
130 []
131 );
132
133 const speedFactors = useMemo(
134 () => ({
135 1: 12000,
136 2: 9000,
137 3: 6000,
138 4: 4000,
139 5: 2000,
140 }),
141 []
142 );
143
144 const glowIntensityClasses = {
145 1: "shadow-md",
146 2: "shadow-lg",
147 3: "shadow-xl",
148 4: "shadow-2xl",
149 5: "shadow-2xl shadow-xl",
150 };
151
152 const isValidNumber = useCallback((num: number) => {
153 return typeof num === "number" && !isNaN(num) && isFinite(num);
154 }, []);
155
156 const generateBlobPath = useCallback(
157 (
158 factor: { points: number; variance: number; tension: number },
159 hover = false,
160 click = false
161 ) => {
162 const { points, variance, tension } = factor;
163 const centerX = 50;
164 const centerY = 50;
165 const baseRadius = hover ? 42 : click ? 38 : 40;
166 const angleStep = (Math.PI * 2) / points;
167
168 const blobPoints = [];
169 for (let i = 0; i < points; i++) {
170 const angle = i * angleStep;
171 const waveVariation = Math.sin(i * 3) * 0.15;
172 const randomVariance =
173 1 - variance + Math.random() * variance * 2 + waveVariation;
174 const radius = Math.max(baseRadius * randomVariance, 5);
175
176 const x = centerX + Math.cos(angle) * radius;
177 const y = centerY + Math.sin(angle) * radius;
178 blobPoints.push({ x, y });
179 }
180
181 let path = `M${blobPoints[0].x},${blobPoints[0].y}`;
182
183 for (let i = 0; i < points; i++) {
184 const current = blobPoints[i];
185 const next = blobPoints[(i + 1) % points];
186
187 const cp1x =
188 current.x +
189 (next.x - blobPoints[(i - 1 + points) % points].x) * tension;
190 const cp1y =
191 current.y +
192 (next.y - blobPoints[(i - 1 + points) % points].y) * tension;
193 const cp2x =
194 next.x - (blobPoints[(i + 2) % points].x - current.x) * tension;
195 const cp2y =
196 next.y - (blobPoints[(i + 2) % points].y - current.y) * tension;
197
198 const validCp1x = isValidNumber(cp1x) ? cp1x : current.x;
199 const validCp1y = isValidNumber(cp1y) ? cp1y : current.y;
200 const validCp2x = isValidNumber(cp2x) ? cp2x : next.x;
201 const validCp2y = isValidNumber(cp2y) ? cp2y : next.y;
202
203 path += ` C${validCp1x},${validCp1y} ${validCp2x},${validCp2y} ${next.x},${next.y}`;
204 }
205
206 path += " Z";
207 return path;
208 },
209 [isValidNumber]
210 );
211
212 const interpolatePaths = useCallback(
213 (path1: string, path2: string, progress: number) => {
214 if (!path1 || !path2) return path2 || path1 || "";
215
216 const extractPoints = (path: string) => {
217 const regex = /([MC]) ?([^MC]+)/g;
218 const matches = [...path.matchAll(regex)];
219
220 const points: number[][] = [];
221
222 for (const match of matches) {
223 const [, command, coordStr] = match;
224 const coords = coordStr
225 .trim()
226 .split(/[ ,]/)
227 .filter(Boolean)
228 .map(parseFloat);
229
230 if (command === "M" && coords.length >= 2) {
231 const x = coords[0];
232 const y = coords[1];
233 if (isValidNumber(x) && isValidNumber(y)) {
234 points.push([x, y]);
235 }
236 } else if (command === "C" && coords.length >= 6) {
237 const endX = coords[4];
238 const endY = coords[5];
239 if (isValidNumber(endX) && isValidNumber(endY)) {
240 points.push([endX, endY]);
241 }
242 }
243 }
244
245 return points;
246 };
247
248 const points1 = extractPoints(path1);
249 const points2 = extractPoints(path2);
250
251 if (points1.length !== points2.length || points1.length === 0) {
252 return path2;
253 }
254
255 const interpolatedPoints = points1.map((point, i) => {
256 if (i < points2.length) {
257 const x = point[0] + (points2[i][0] - point[0]) * progress;
258 const y = point[1] + (points2[i][1] - point[1]) * progress;
259
260 return [
261 isValidNumber(x) ? x : point[0],
262 isValidNumber(y) ? y : point[1],
263 ];
264 }
265 return point;
266 });
267
268 let newPath = `M${interpolatedPoints[0][0]},${interpolatedPoints[0][1]}`;
269
270 for (let i = 1; i < interpolatedPoints.length; i++) {
271 const prev = interpolatedPoints[i - 1];
272 const curr = interpolatedPoints[i];
273
274 const tension = 0.4;
275 const dx = curr[0] - prev[0];
276 const dy = curr[1] - prev[1];
277
278 const cpx1 = prev[0] + dx * tension;
279 const cpy1 = prev[1] + dy * tension;
280 const cpx2 = curr[0] - dx * tension;
281 const cpy2 = curr[1] - dy * tension;
282
283 const validCpx1 = isValidNumber(cpx1) ? cpx1 : prev[0];
284 const validCpy1 = isValidNumber(cpy1) ? cpy1 : prev[1];
285 const validCpx2 = isValidNumber(cpx2) ? cpx2 : curr[0];
286 const validCpy2 = isValidNumber(cpy2) ? cpy2 : curr[1];
287
288 newPath += ` C${validCpx1},${validCpy1} ${validCpx2},${validCpy2} ${curr[0]},${curr[1]}`;
289 }
290
291 newPath += " Z";
292 return newPath;
293 },
294 [isValidNumber]
295 );
296
297 const animate = useCallback(
298 (time: number) => {
299 if (previousTimeRef.current === null) {
300 previousTimeRef.current = time;
301 }
302
303 const deltaTime = time - (previousTimeRef.current || 0);
304 const factor = complexityFactors[complexity];
305 const duration = speedFactors[speed];
306
307 animationProgress.current += deltaTime / duration;
308 if (animationProgress.current >= 1) {
309 animationProgress.current = 0;
310 setPrevBlobPath(blobPath);
311 setBlobPath(generateBlobPath(factor, isHovered, isClicked));
312 setRotation((prev) => (prev + 30) % 360);
313 }
314
315 previousTimeRef.current = time;
316 requestRef.current = requestAnimationFrame(animate);
317 },
318 [
319 blobPath,
320 complexity,
321 speed,
322 isHovered,
323 isClicked,
324 complexityFactors,
325 speedFactors,
326 generateBlobPath,
327 ]
328 );
329
330 useEffect(() => {
331 const factor = complexityFactors[complexity];
332
333 if (!blobPath) {
334 const initialPath = generateBlobPath(factor, isHovered, isClicked);
335 setBlobPath(initialPath);
336 setPrevBlobPath(initialPath);
337 }
338
339 if (smooth) {
340 requestRef.current = requestAnimationFrame(animate);
341 return () => {
342 if (requestRef.current !== null) {
343 cancelAnimationFrame(requestRef.current);
344 }
345 };
346 } else {
347 const interval = setInterval(() => {
348 setPrevBlobPath(blobPath);
349 setBlobPath(generateBlobPath(factor, isHovered, isClicked));
350 setRotation((prev) => (prev + 30) % 360);
351 }, speedFactors[speed] / 6);
352
353 return () => clearInterval(interval);
354 }
355 }, [
356 complexity,
357 speed,
358 isHovered,
359 isClicked,
360 smooth,
361 animate,
362 blobPath,
363 complexityFactors,
364 speedFactors,
365 generateBlobPath,
366 ]);
367
368 const handleMouseEnter = () => {
369 if (hoverEffect) {
370 setIsHovered(true);
371 }
372 };
373
374 const handleMouseLeave = () => {
375 if (hoverEffect) {
376 setIsHovered(false);
377 }
378 if (clickEffect) {
379 setIsClicked(false);
380 }
381 };
382
383 const handleMouseDown = () => {
384 if (clickEffect) {
385 setIsClicked(true);
386 }
387 };
388
389 const handleMouseUp = () => {
390 if (clickEffect) {
391 setIsClicked(false);
392 }
393 };
394
395 const displayPath =
396 smooth && prevBlobPath
397 ? interpolatePaths(prevBlobPath, blobPath, animationProgress.current)
398 : blobPath;
399
400 return (
401 <div
402 ref={blobRef}
403 className={cn(
404 "relative flex items-center justify-center transition-all duration-500",
405 sizeClasses[size],
406 glow && glowIntensityClasses[glowIntensity],
407 glow && currentTheme.glow,
408 pulse && "animate-pulse",
409 className
410 )}
411 onMouseEnter={handleMouseEnter}
412 onMouseLeave={handleMouseLeave}
413 onMouseDown={handleMouseDown}
414 onMouseUp={handleMouseUp}
415 style={{
416 opacity: opacity / 100,
417 }}
418 {...props}
419 >
420 <svg
421 viewBox="0 0 100 100"
422 className="absolute inset-0 w-full h-full"
423 style={{
424 transform: `rotate(${rotation}deg)`,
425 transition: "transform 8s ease-in-out",
426 filter: glow ? currentTheme.filter : "none",
427 }}
428 >
429 {effect3D && (
430 <path
431 d={displayPath}
432 className="transition-all duration-300"
433 fill="rgba(0,0,0,0.2)"
434 transform="translate(3,3)"
435 />
436 )}
437
438 <path
439 d={displayPath}
440 className={cn(
441 "transition-all duration-300 bg-gradient-to-br",
442 effect3D ? "backdrop-blur-lg" : ""
443 )}
444 fill="url(#blob-gradient)"
445 />
446
447 {effect3D && (
448 <path
449 d={displayPath}
450 className="transition-all duration-300"
451 fill="rgba(255,255,255,0.1)"
452 transform="translate(-1.5,-1.5) scale(0.98)"
453 />
454 )}
455
456 <defs>
457 <linearGradient
458 id="blob-gradient"
459 x1="0%"
460 y1="0%"
461 x2="100%"
462 y2="100%"
463 >
464 <stop offset="0%" className="stop-color-from" />
465 {theme === "custom" && customColors?.via && (
466 <stop offset="50%" className="stop-color-via" />
467 )}
468 <stop offset="100%" className="stop-color-to" />
469 </linearGradient>
470
471 <radialGradient
472 id="blob-radial"
473 cx="50%"
474 cy="50%"
475 r="50%"
476 fx="50%"
477 fy="50%"
478 >
479 <stop offset="0%" stopColor="rgba(255,255,255,0.15)" />
480 <stop offset="100%" stopColor="rgba(255,255,255,0)" />
481 </radialGradient>
482
483 <filter id="blur-filter" x="-50%" y="-50%" width="200%" height="200%">
484 <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
485 </filter>
486 </defs>
487 </svg>
488
489 {children && (
490 <div className="relative z-10 flex items-center justify-center text-white transition-all duration-300">
491 {children}
492 </div>
493 )}
494 </div>
495 );
496}
497
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 |
---|---|---|---|
theme | string | "primary" | The color theme of the blob. Possible values: "primary", "secondary", "accent", "success", "warning", "danger", "custom". |
customColors | object | undefined | Custom colors for the blob when theme is set to "custom". The object should include properties: from, to, and optionally via. |
size | string | "md" | The size of the blob. Possible values: "sm", "md", "lg", "xl", "full". |
complexity | number | 3 | The complexity of the blob shape (1-5). |
speed | number | 3 | The speed of the morphing animation (1-5). |
hoverEffect | boolean | true | Whether to enable the hover effect. |
clickEffect | boolean | true | Whether to enable the click effect. |
pulse | boolean | false | Whether to enable the pulse animation. |
glow | boolean | true | Whether to show a glow effect. |
glowIntensity | number | 3 | The intensity of the glow effect (1-5). |
opacity | number | 100 | The opacity of the blob (percentage). |
smooth | boolean | true | Whether to enable smooth transitions in the morphing animation. |
effect3D | boolean | false | Whether to enable a 3D effect on the blob. |
className | string | "" | Additional CSS classes to apply. |
children | React.ReactNode | undefined | Content to be rendered inside the blob component. |
Examples
Get Started
Beautiful animated backgrounds for modern UIs
Security
Enterprise-grade protection for your applications
Premium
Elevate your UI with custom gradient effects
AI Integration
Smart, adaptive components that learn and respond