Introducing Nuvyx UI v1.0.0

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

Interactive

Move your cursor over this card to create dynamic water ripple effects. Perfect for creating engaging UI elements.

Cosmic Waves

High Intensity

A more intense ripple effect with slower speed, creating a cosmic wave-like animation that reacts to your movements.

Nature Pulse

Fast

A subtle but fast ripple effect that mimics the gentle pulse of nature. Lower intensity with higher speed.

Maximum Energy

Extreme

The most intense and fastest ripple effect, creating an energetic and dynamic interaction experience.

Installation Guide

1

Install Dependencies

Utility Functions

npm install clsx tailwind-merge
2

Setup Configuration

Create file: /lib/utils.ts
/lib/utils.ts
Configuration
1import { 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

Dynamic Ripple.tsx
TypeScript
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
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

NameTypeDefaultDescription
themestring"blue"The color theme of the ripple effect. Possible values: "blue", "purple", "green", "amber", "rose", "custom".
customColorsobjectundefinedCustom colors for the ripple when theme is set to "custom". The object should include properties: primary and secondary.
intensitynumber3The intensity of the ripple effect (1-5).
speednumber3The speed of the ripple animation (1-5).
reactToCursorbooleantrueWhether to enable the ripple effect on cursor movement.
autoAnimatebooleantrueWhether to enable the automatic ripple animation.
roundedstring"lg"The border radius of the component. Possible values: "none", "sm", "md", "lg", "xl", "full".
gradientOverlaybooleantrueWhether to apply a gradient overlay.
classNamestring""Additional CSS classes to apply.
childrenReact.ReactNodeundefinedContent 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

Premium Plan

$29/month
Unlimited projects
Priority support
Custom reporting
Get Started Today