Dynamic Ripple
A fluid, water ripple effect that reacts to cursor movement or touch. Can be used in cards, buttons, or section dividers.
Water Ripple Effect
Move your cursor over this card to create dynamic water ripple effects. Perfect for creating engaging UI elements.
Cosmic Waves
A more intense ripple effect with slower speed, creating a cosmic wave-like animation that reacts to your movements.
Nature Pulse
A subtle but fast ripple effect that mimics the gentle pulse of nature. Lower intensity with higher speed.
Maximum Energy
ExtremeThe most intense and fastest ripple effect, creating an energetic and dynamic interaction experience.
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
4export 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 } from "react";
4import { cn } from "@/lib/utils";
5
6interface Drop {
7 x: number;
8 y: number;
9 radius: number;
10 maxRadius: number;
11 speed: number;
12 opacity: number;
13 color: string;
14}
15
16export interface DynamicRippleProps
17 extends React.HTMLAttributes<HTMLDivElement> {
18 theme?: "blue" | "purple" | "green" | "amber" | "rose" | "custom";
19 customColors?: {
20 primary: string;
21 secondary: string;
22 };
23 intensity?: 1 | 2 | 3 | 4 | 5;
24 speed?: 1 | 2 | 3 | 4 | 5;
25 reactToCursor?: boolean;
26 autoAnimate?: boolean;
27 rounded?: "none" | "sm" | "md" | "lg" | "xl" | "full";
28 gradientOverlay?: boolean;
29 children: React.ReactNode;
30}
31
32export function DynamicRipple({
33 theme = "blue",
34 customColors,
35 intensity = 3,
36 speed = 3,
37 reactToCursor = true,
38 autoAnimate = true,
39 rounded = "lg",
40 gradientOverlay = true,
41 className,
42 children,
43 ...props
44}: DynamicRippleProps) {
45 const containerRef = useRef<HTMLDivElement>(null);
46 const canvasRef = useRef<HTMLCanvasElement>(null);
47 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
48 const animationRef = useRef<number>(0);
49 const dropsRef = useRef<Drop[]>([]);
50
51 const intensityFactors = useMemo(
52 () => ({
53 1: { size: 0.6, opacity: 0.3, count: 3 },
54 2: { size: 0.8, opacity: 0.4, count: 5 },
55 3: { size: 1.0, opacity: 0.5, count: 7 },
56 4: { size: 1.2, opacity: 0.6, count: 9 },
57 5: { size: 1.5, opacity: 0.7, count: 12 },
58 }),
59 []
60 );
61
62 const speedFactors = useMemo(
63 () => ({
64 1: 0.5,
65 2: 0.75,
66 3: 1.0,
67 4: 1.25,
68 5: 1.5,
69 }),
70 []
71 );
72
73 const themeColors = useMemo(
74 () => ({
75 blue: {
76 primary: "rgba(59, 130, 246, 0.7)",
77 secondary: "rgba(6, 182, 212, 0.7)",
78 overlay: "from-blue-500/10 to-cyan-500/10",
79 },
80 purple: {
81 primary: "rgba(139, 92, 246, 0.7)",
82 secondary: "rgba(216, 180, 254, 0.7)",
83 overlay: "from-purple-500/10 to-pink-500/10",
84 },
85 green: {
86 primary: "rgba(16, 185, 129, 0.7)",
87 secondary: "rgba(110, 231, 183, 0.7)",
88 overlay: "from-green-500/10 to-emerald-500/10",
89 },
90 amber: {
91 primary: "rgba(245, 158, 11, 0.7)",
92 secondary: "rgba(252, 211, 77, 0.7)",
93 overlay: "from-amber-500/10 to-yellow-500/10",
94 },
95 rose: {
96 primary: "rgba(244, 63, 94, 0.7)",
97 secondary: "rgba(251, 113, 133, 0.7)",
98 overlay: "from-rose-500/10 to-pink-500/10",
99 },
100 custom: {
101 primary: customColors?.primary || "rgba(59, 130, 246, 0.7)",
102 secondary: customColors?.secondary || "rgba(6, 182, 212, 0.7)",
103 overlay: "from-gray-500/10 to-gray-400/10",
104 },
105 }),
106 [customColors]
107 );
108
109 const currentTheme = themeColors[theme];
110
111 const roundedStyles = useMemo(
112 () => ({
113 none: "rounded-none",
114 sm: "rounded-sm",
115 md: "rounded-md",
116 lg: "rounded-lg",
117 xl: "rounded-xl",
118 full: "rounded-full",
119 }),
120 []
121 );
122
123 useEffect(() => {
124 if (!canvasRef.current || !containerRef.current) return;
125
126 const updateDimensions = () => {
127 if (!containerRef.current) return;
128
129 const { width, height } = containerRef.current.getBoundingClientRect();
130 setDimensions({ width, height });
131
132 if (canvasRef.current) {
133 canvasRef.current.width = width;
134 canvasRef.current.height = height;
135 }
136 };
137
138 updateDimensions();
139
140 const container = containerRef.current;
141 const resizeObserver = new ResizeObserver(updateDimensions);
142 resizeObserver.observe(container);
143
144 return () => {
145 resizeObserver.disconnect();
146 cancelAnimationFrame(animationRef.current);
147 };
148 }, []);
149
150 useEffect(() => {
151 if (!canvasRef.current || !dimensions.width || !dimensions.height) return;
152
153 const currentIntensityFactors = intensityFactors[intensity];
154 const currentSpeedFactor = speedFactors[speed];
155
156 const createDrop = (x: number, y: number, userInitiated = false) => {
157 const maxRadius =
158 Math.min(dimensions.width, dimensions.height) *
159 0.3 *
160 currentIntensityFactors.size;
161
162 return {
163 x,
164 y,
165 radius: 0,
166 maxRadius,
167 speed: currentSpeedFactor * (userInitiated ? 1.5 : 1),
168 opacity: currentIntensityFactors.opacity * (userInitiated ? 1.2 : 1),
169 color:
170 Math.random() > 0.5 ? currentTheme.primary : currentTheme.secondary,
171 };
172 };
173
174 if (autoAnimate) {
175 const initialDrops = Array.from({
176 length: currentIntensityFactors.count,
177 }).map(() => {
178 const x = Math.random() * dimensions.width;
179 const y = Math.random() * dimensions.height;
180 return createDrop(x, y);
181 });
182
183 dropsRef.current = initialDrops;
184 }
185
186 const handlePointerMove = (e: MouseEvent | TouchEvent) => {
187 if (!reactToCursor || !containerRef.current) return;
188
189 const rect = containerRef.current.getBoundingClientRect();
190 let clientX, clientY;
191
192 if ("touches" in e) {
193 clientX = e.touches[0].clientX;
194 clientY = e.touches[0].clientY;
195 } else {
196 clientX = e.clientX;
197 clientY = e.clientY;
198 }
199
200 const x = clientX - rect.left;
201 const y = clientY - rect.top;
202
203 dropsRef.current.push(createDrop(x, y, true));
204
205 if (dropsRef.current.length > 20) {
206 dropsRef.current = dropsRef.current.slice(-20);
207 }
208 };
209
210 const container = containerRef.current;
211 if (reactToCursor && container) {
212 container.addEventListener("mousemove", handlePointerMove);
213 container.addEventListener("touchmove", handlePointerMove);
214 }
215
216 return () => {
217 if (container) {
218 container.removeEventListener("mousemove", handlePointerMove);
219 container.removeEventListener("touchmove", handlePointerMove);
220 }
221 };
222 }, [
223 dimensions,
224 intensity,
225 speed,
226 reactToCursor,
227 autoAnimate,
228 currentTheme.primary,
229 currentTheme.secondary,
230 intensityFactors,
231 speedFactors,
232 ]);
233
234 useEffect(() => {
235 if (!canvasRef.current || !dimensions.width || !dimensions.height) return;
236
237 const ctx = canvasRef.current.getContext("2d");
238 if (!ctx) return;
239
240 const currentIntensityFactors = intensityFactors[intensity];
241 const currentSpeedFactor = speedFactors[speed];
242
243 const animate = () => {
244 ctx.clearRect(0, 0, dimensions.width, dimensions.height);
245 dropsRef.current = dropsRef.current.filter((drop) => {
246 drop.radius += drop.speed;
247 ctx.beginPath();
248 ctx.arc(drop.x, drop.y, drop.radius, 0, Math.PI * 2);
249 ctx.strokeStyle = drop.color;
250 ctx.lineWidth = 2;
251 ctx.globalAlpha = Math.max(
252 0,
253 drop.opacity * (1 - drop.radius / drop.maxRadius)
254 );
255 ctx.stroke();
256 return drop.radius < drop.maxRadius;
257 });
258
259 if (autoAnimate && Math.random() < 0.05 * currentSpeedFactor) {
260 const x = Math.random() * dimensions.width;
261 const y = Math.random() * dimensions.height;
262
263 dropsRef.current.push({
264 x,
265 y,
266 radius: 0,
267 maxRadius:
268 Math.min(dimensions.width, dimensions.height) *
269 0.2 *
270 currentIntensityFactors.size,
271 speed: currentSpeedFactor,
272 opacity: currentIntensityFactors.opacity,
273 color:
274 Math.random() > 0.5 ? currentTheme.primary : currentTheme.secondary,
275 });
276 }
277
278 animationRef.current = requestAnimationFrame(animate);
279 };
280
281 animate();
282
283 return () => {
284 cancelAnimationFrame(animationRef.current);
285 };
286 }, [
287 dimensions,
288 intensity,
289 speed,
290 autoAnimate,
291 currentTheme.primary,
292 currentTheme.secondary,
293 intensityFactors,
294 speedFactors,
295 ]);
296
297 return (
298 <div
299 ref={containerRef}
300 className={cn(
301 "relative overflow-hidden",
302 roundedStyles[rounded],
303 className
304 )}
305 {...props}
306 >
307 <canvas
308 ref={canvasRef}
309 className="absolute inset-0 w-full h-full pointer-events-none"
310 />
311 {gradientOverlay && (
312 <div
313 className={cn(
314 "absolute inset-0 bg-gradient-to-br opacity-30 pointer-events-none",
315 currentTheme.overlay
316 )}
317 />
318 )}
319 <div className="relative z-10">{children}</div>
320 </div>
321 );
322}
323
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 | "blue" | The color theme of the ripple effect. Possible values: "blue", "purple", "green", "amber", "rose", "custom". |
customColors | object | undefined | Custom colors for the ripple when theme is set to "custom". The object should include properties: primary and secondary. |
intensity | number | 3 | The intensity of the ripple effect (1-5). |
speed | number | 3 | The speed of the ripple animation (1-5). |
reactToCursor | boolean | true | Whether to enable the ripple effect on cursor movement. |
autoAnimate | boolean | true | Whether to enable the automatic ripple animation. |
rounded | string | "lg" | The border radius of the component. Possible values: "none", "sm", "md", "lg", "xl", "full". |
gradientOverlay | boolean | true | Whether to apply a gradient overlay. |
className | string | "" | Additional CSS classes to apply. |
children | React.ReactNode | undefined | Content to be rendered inside the ripple component. |
Examples
Water Ripple Effect
Move your cursor over this card to create ripples
Custom Gradient Colors
Indigo to pink custom color blend
High Intensity Ripple
Maximum intensity with slower speed
Fast Ripple
Low intensity with maximum speed
Circular Ripple
With rounded full style
Cursor-Only Ripple
Only reacts to cursor movement, no automatic animation
Clean Interface
Ripple effect without gradient overlay for cleaner UI
Security Features
- End-to-end encryption
- Two-factor authentication
- Secure cloud backup