Introducing Nuvyx UI v1.0.0

Liquid Metal Button

Buttons that behave like mercury when clicked. A fluid effect that reacts to the mouse pointer.

Liquid Metal Buttons

Interactive buttons with realistic metal effects and fluid animations

Premium Finishes

Liquid Effects

Style Variations

Premium Collection

Our most interactive buttons with full liquid metal effects

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
4  export function cn(...inputs: ClassValue[]) {
5      return twMerge(clsx(inputs));
6  }
3

Copy Component Code

Liquid Metal Button.tsx
TypeScript
1"use client";
2
3import type React from "react";
4import { useRef, useEffect, useState, useCallback, useMemo } from "react";
5import { cn } from "@/lib/utils";
6
7export interface LiquidMetalButtonProps
8  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
9  variant?: "default" | "outline" | "ghost" | "mercury" | "ripple" | "gradient";
10  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
11  theme?:
12    | "silver"
13    | "gold"
14    | "copper"
15    | "mercury"
16    | "steel"
17    | "obsidian"
18    | "emerald"
19    | "ruby"
20    | "sapphire"
21    | "custom";
22  customColors?: {
23    base: string;
24    highlight: string;
25    shadow: string;
26    text?: string;
27    border?: string;
28    glow?: string;
29  };
30  intensity?: 1 | 2 | 3 | 4 | 5;
31  magnetic?: boolean;
32  clickEffect?: boolean;
33  asChild?: boolean;
34  rounded?: "none" | "sm" | "md" | "lg" | "full";
35  shadow?: boolean | "sm" | "md" | "lg" | "xl";
36  hoverAnimation?: boolean;
37  textured?: boolean;
38  icon?: React.ReactNode;
39  iconAfter?: React.ReactNode;
40  children: React.ReactNode;
41  onClick?: () => void;
42}
43
44interface Droplet {
45  x: number;
46  y: number;
47  size: number;
48  opacity: number;
49  speedX: number;
50  speedY: number;
51  life: number;
52}
53
54export function LiquidMetalButton({
55  variant = "default",
56  size = "md",
57  theme = "silver",
58  customColors,
59  intensity = 3,
60  magnetic = true,
61  clickEffect = true,
62  rounded = "md",
63  shadow = true,
64  hoverAnimation = true,
65  textured = false,
66  icon,
67  iconAfter,
68  className,
69  children,
70  onClick,
71  ...props
72}: LiquidMetalButtonProps) {
73  const buttonRef = useRef<HTMLButtonElement>(null);
74  const canvasRef = useRef<HTMLCanvasElement>(null);
75  const [isHovered, setIsHovered] = useState(false);
76  const [isPressed, setIsPressed] = useState(false);
77  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
78  const [buttonDimensions, setButtonDimensions] = useState({
79    width: 0,
80    height: 0,
81    top: 0,
82    left: 0,
83  });
84  const [droplets, setDroplets] = useState<Droplet[]>([]);
85  const animationRef = useRef<number>(0);
86  const lastRippleTime = useRef<number>(0);
87  const dropletsRef = useRef<Droplet[]>([]);
88
89  const intensityFactors = useMemo(
90    () => ({
91      1: 0.2,
92      2: 0.4,
93      3: 0.6,
94      4: 0.8,
95      5: 1.0,
96    }),
97    []
98  );
99
100  const themeColors = useMemo(
101    () => ({
102      silver: {
103        base: "bg-gradient-to-b from-gray-200 via-gray-300 to-gray-400 dark:from-gray-700 dark:via-gray-800 dark:to-gray-900",
104        text: "text-gray-800 dark:text-gray-200",
105        highlight: "rgba(255, 255, 255, 0.8)",
106        shadow: "rgba(0, 0, 0, 0.3)",
107        glow: "shadow-gray-400/50",
108        border: "border-gray-400",
109        texture:
110          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMCwwLDAsMC4wMikiLz48L3N2Zz4=')]",
111      },
112      gold: {
113        base: "bg-gradient-to-b from-amber-200 via-amber-300 to-amber-500 dark:from-amber-700 dark:via-amber-800 dark:to-amber-900",
114        text: "text-amber-900 dark:text-amber-200",
115        highlight: "rgba(255, 235, 150, 0.8)",
116        shadow: "rgba(120, 80, 0, 0.4)",
117        glow: "shadow-amber-400/50",
118        border: "border-amber-500",
119        texture:
120          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMjU1LDIxNSwwLDAuMDIpIi8+PC9zdmc+')]",
121      },
122      copper: {
123        base: "bg-gradient-to-b from-orange-200 via-orange-300 to-orange-600 dark:from-orange-700 dark:via-orange-800 dark:to-orange-900",
124        text: "text-orange-900 dark:text-orange-200",
125        highlight: "rgba(255, 200, 150, 0.8)",
126        shadow: "rgba(120, 60, 0, 0.4)",
127        glow: "shadow-orange-400/50",
128        border: "border-orange-500",
129        texture:
130          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMjU1LDE2MCwwLDAuMDIpIi8+PC9zdmc+')]",
131      },
132      mercury: {
133        base: "bg-gradient-to-b from-blue-200 via-blue-300 to-blue-400 dark:from-blue-700 dark:via-blue-800 dark:to-blue-900",
134        text: "text-blue-900 dark:text-blue-100",
135        highlight: "rgba(200, 230, 255, 0.8)",
136        shadow: "rgba(0, 50, 120, 0.4)",
137        glow: "shadow-blue-400/50",
138        border: "border-blue-400",
139        texture:
140          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMTgwLDIzMCwyNTUsMC4wMikiLz48L3N2Zz4=')]",
141      },
142      steel: {
143        base: "bg-gradient-to-b from-slate-300 via-slate-400 to-slate-600 dark:from-slate-700 dark:via-slate-800 dark:to-slate-900",
144        text: "text-slate-900 dark:text-slate-100",
145        highlight: "rgba(220, 230, 240, 0.8)",
146        shadow: "rgba(30, 40, 50, 0.4)",
147        glow: "shadow-slate-400/50",
148        border: "border-slate-500",
149        texture:
150          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMjIwLDIyMCwyMjAsMC4wMikiLz48L3N2Zz4=')]",
151      },
152      obsidian: {
153        base: "bg-gradient-to-b from-gray-600 via-gray-700 to-gray-900 dark:from-gray-700 dark:via-gray-800 dark:to-gray-900",
154        text: "text-gray-100 dark:text-gray-100",
155        highlight: "rgba(180, 180, 180, 0.3)",
156        shadow: "rgba(0, 0, 0, 0.5)",
157        glow: "shadow-gray-900/70",
158        border: "border-gray-700",
159        texture:
160          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wMSkiLz48L3N2Zz4=')]",
161      },
162      emerald: {
163        base: "bg-gradient-to-b from-emerald-300 via-emerald-400 to-emerald-600 dark:from-emerald-700 dark:via-emerald-800 dark:to-emerald-900",
164        text: "text-emerald-950 dark:text-emerald-100",
165        highlight: "rgba(180, 255, 220, 0.7)",
166        shadow: "rgba(0, 80, 60, 0.4)",
167        glow: "shadow-emerald-500/50",
168        border: "border-emerald-500",
169        texture:
170          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMTAwLDI1NSwxNTAsMC4wMikiLz48L3N2Zz4=')]",
171      },
172      ruby: {
173        base: "bg-gradient-to-b from-red-300 via-red-400 to-red-600 dark:from-red-700 dark:via-red-800 dark:to-red-900",
174        text: "text-red-950 dark:text-red-100",
175        highlight: "rgba(255, 200, 200, 0.7)",
176        shadow: "rgba(100, 0, 0, 0.4)",
177        glow: "shadow-red-500/50",
178        border: "border-red-500",
179        texture:
180          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMjU1LDEwMCwxMDAsMC4wMikiLz48L3N2Zz4=')]",
181      },
182      sapphire: {
183        base: "bg-gradient-to-b from-indigo-300 via-indigo-400 to-indigo-600 dark:from-indigo-700 dark:via-indigo-800 dark:to-indigo-900",
184        text: "text-indigo-950 dark:text-indigo-100",
185        highlight: "rgba(200, 200, 255, 0.7)",
186        shadow: "rgba(40, 0, 100, 0.4)",
187        glow: "shadow-indigo-500/50",
188        border: "border-indigo-500",
189        texture:
190          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMTUwLDE1MCwyNTUsMC4wMikiLz48L3N2Zz4=')]",
191      },
192      custom: {
193        base: customColors?.base
194          ? customColors.base.startsWith("#")
195            ? `bg-[${customColors.base}]`
196            : customColors.base
197          : "bg-gradient-to-b from-gray-300 to-gray-400",
198        text: customColors?.text || "text-gray-800",
199        highlight: customColors?.highlight || "rgba(255, 255, 255, 0.8)",
200        shadow: customColors?.shadow || "rgba(0, 0, 0, 0.3)",
201        glow: customColors?.glow || "shadow-gray-400/50",
202        border: customColors?.border || "border-gray-400",
203        texture:
204          "bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjIiPjxyZWN0IHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9InJnYmEoMCwwLDAsMC4wMikiLz48L3N2Zz4=')]",
205      },
206    }),
207    [customColors]
208  );
209
210  const currentTheme = useMemo(() => themeColors[theme], [theme, themeColors]);
211
212  const sizeClasses = {
213    xs: "px-2 py-1 text-xs",
214    sm: "px-3 py-1.5 text-sm",
215    md: "px-4 py-2 text-base",
216    lg: "px-5 py-2.5 text-lg",
217    xl: "px-6 py-3 text-xl",
218    "2xl": "px-8 py-4 text-2xl",
219  };
220
221  const roundedClasses = {
222    none: "rounded-none",
223    sm: "rounded-sm",
224    md: "rounded-md",
225    lg: "rounded-lg",
226    full: "rounded-full",
227  };
228
229  const shadowClasses = {
230    true: "shadow-lg",
231    sm: "shadow-sm",
232    md: "shadow-md",
233    lg: "shadow-lg",
234    xl: "shadow-xl",
235    false: "",
236  };
237
238  useEffect(() => {
239    dropletsRef.current = droplets;
240  }, [droplets]);
241
242  useEffect(() => {
243    if (!buttonRef.current || !canvasRef.current) return;
244
245    const updateDimensions = () => {
246      if (!buttonRef.current || !canvasRef.current) return;
247
248      const rect = buttonRef.current.getBoundingClientRect();
249      setButtonDimensions({
250        width: rect.width,
251        height: rect.height,
252        top: rect.top,
253        left: rect.left,
254      });
255
256      canvasRef.current.width = rect.width;
257      canvasRef.current.height = rect.height;
258    };
259
260    updateDimensions();
261
262    window.addEventListener("resize", updateDimensions);
263
264    return () => {
265      window.removeEventListener("resize", updateDimensions);
266      cancelAnimationFrame(animationRef.current);
267    };
268  }, []);
269
270  const createDroplet = useCallback(
271    (x: number, y: number) => {
272      if (!isHovered || (variant !== "mercury" && variant !== "ripple"))
273        return false;
274
275      const now = Date.now();
276      if (now - lastRippleTime.current > 30) {
277        const factor = intensityFactors[intensity];
278        const size = Math.random() * 8 * factor + 3;
279
280        setDroplets((prev) => {
281          const newDroplets = [
282            ...prev,
283            {
284              x,
285              y,
286              size,
287              opacity: 0.7,
288              speedX: (Math.random() - 0.5) * 2 * factor,
289              speedY: (Math.random() - 0.5) * 2 * factor,
290              life: 1.0,
291            },
292          ];
293          return newDroplets.length > 25 ? newDroplets.slice(-25) : newDroplets;
294        });
295
296        lastRippleTime.current = now;
297        return true;
298      }
299
300      return false;
301    },
302    [isHovered, variant, intensity, intensityFactors]
303  );
304
305  useEffect(() => {
306    if (!isHovered) return;
307
308    const button = buttonRef.current;
309    if (!button) return;
310
311    const handleMouseMove = (e: MouseEvent) => {
312      if (!button) return;
313
314      const rect = button.getBoundingClientRect();
315      const x = e.clientX - rect.left;
316      const y = e.clientY - rect.top;
317
318      setMousePosition({ x, y });
319
320      if (
321        (variant === "mercury" || variant === "ripple") &&
322        Math.random() > 0.6
323      ) {
324        createDroplet(x, y);
325      }
326    };
327
328    document.addEventListener("mousemove", handleMouseMove);
329
330    return () => {
331      document.removeEventListener("mousemove", handleMouseMove);
332    };
333  }, [isHovered, variant, createDroplet]);
334
335  useEffect(() => {
336    if (!canvasRef.current || (variant !== "mercury" && variant !== "ripple"))
337      return;
338
339    const ctx = canvasRef.current.getContext("2d");
340    if (!ctx) return;
341
342    let frameCount = 0;
343
344    const animate = () => {
345      if (!isHovered) {
346        ctx.clearRect(0, 0, buttonDimensions.width, buttonDimensions.height);
347        return;
348      }
349
350      frameCount++;
351      ctx.clearRect(0, 0, buttonDimensions.width, buttonDimensions.height);
352      const currentDroplets = dropletsRef.current;
353      const updatedDroplets = currentDroplets
354        .map((droplet) => {
355          const gravity = variant === "ripple" ? 0.08 : 0.02;
356
357          return {
358            ...droplet,
359            x: droplet.x + droplet.speedX,
360            y: droplet.y + droplet.speedY + gravity,
361            speedX: droplet.speedX * 0.98,
362            speedY: droplet.speedY * 0.98 + gravity,
363            life: droplet.life - 0.015,
364            size: droplet.size * 0.99,
365          };
366        })
367        .filter((droplet) => droplet.life > 0);
368
369      updatedDroplets.forEach((droplet) => {
370        const gradientColor =
371          variant === "mercury"
372            ? ctx.createRadialGradient(
373                droplet.x,
374                droplet.y,
375                0,
376                droplet.x,
377                droplet.y,
378                droplet.size
379              )
380            : ctx.createRadialGradient(
381                droplet.x,
382                droplet.y,
383                0,
384                droplet.x,
385                droplet.y,
386                droplet.size * 1.2
387              );
388
389        if (variant === "mercury") {
390          gradientColor.addColorStop(
391            0,
392            `rgba(240, 250, 255, ${droplet.life * 0.9})`
393          );
394          gradientColor.addColorStop(
395            0.6,
396            `rgba(180, 220, 255, ${droplet.life * 0.7})`
397          );
398          gradientColor.addColorStop(
399            1,
400            `rgba(100, 180, 255, ${droplet.life * 0.3})`
401          );
402        } else {
403          gradientColor.addColorStop(
404            0,
405            `rgba(255, 255, 255, ${droplet.life * 0.8})`
406          );
407          gradientColor.addColorStop(
408            0.7,
409            `rgba(220, 230, 255, ${droplet.life * 0.5})`
410          );
411          gradientColor.addColorStop(
412            1,
413            `rgba(200, 210, 255, ${droplet.life * 0.1})`
414          );
415        }
416
417        ctx.beginPath();
418        ctx.arc(droplet.x, droplet.y, droplet.size, 0, Math.PI * 2);
419        ctx.fillStyle = gradientColor;
420        ctx.fill();
421      });
422
423      if (frameCount % 4 === 0) {
424        setDroplets(updatedDroplets);
425      }
426
427      animationRef.current = requestAnimationFrame(animate);
428    };
429
430    animate();
431
432    return () => {
433      cancelAnimationFrame(animationRef.current);
434      if (ctx) {
435        ctx.clearRect(0, 0, buttonDimensions.width, buttonDimensions.height);
436      }
437    };
438  }, [isHovered, variant, buttonDimensions.width, buttonDimensions.height]);
439
440  useEffect(() => {
441    if (!magnetic || !isHovered) return;
442
443    const button = buttonRef.current;
444    if (!button) return;
445
446    const handleMouseMove = (e: MouseEvent) => {
447      if (!button) return;
448
449      const rect = button.getBoundingClientRect();
450      const centerX = rect.left + rect.width / 2;
451      const centerY = rect.top + rect.height / 2;
452
453      const distanceX = e.clientX - centerX;
454      const distanceY = e.clientY - centerY;
455
456      const totalDistance = Math.sqrt(
457        distanceX * distanceX + distanceY * distanceY
458      );
459
460      const maxDistance = Math.max(rect.width, rect.height) * 1.8;
461      const factor = intensityFactors[intensity];
462      const pullStrength =
463        Math.max(0, 1 - totalDistance / maxDistance) * 15 * factor;
464
465      if (totalDistance < maxDistance) {
466        const moveX = (distanceX / totalDistance) * pullStrength;
467        const moveY = (distanceY / totalDistance) * pullStrength;
468
469        button.style.transform = `translate(${moveX}px, ${moveY}px)`;
470        button.style.transition =
471          "transform 0.1s cubic-bezier(0.2, 0.8, 0.2, 1)";
472      } else {
473        button.style.transform = "";
474      }
475    };
476
477    document.addEventListener("mousemove", handleMouseMove);
478
479    return () => {
480      document.removeEventListener("mousemove", handleMouseMove);
481      if (button) {
482        button.style.transform = "";
483      }
484    };
485  }, [magnetic, isHovered, intensity, intensityFactors]);
486
487  const handleMouseEnter = () => {
488    setIsHovered(true);
489  };
490
491  const handleMouseLeave = () => {
492    setIsHovered(false);
493    setDroplets([]);
494    cancelAnimationFrame(animationRef.current);
495
496    if (canvasRef.current) {
497      const ctx = canvasRef.current.getContext("2d");
498      if (ctx) {
499        ctx.clearRect(0, 0, buttonDimensions.width, buttonDimensions.height);
500      }
501    }
502
503    if (buttonRef.current) {
504      buttonRef.current.style.transform = "";
505      buttonRef.current.style.transition = "transform 0.3s ease-out";
506    }
507  };
508
509  const handleMouseDown = () => {
510    if (!clickEffect) return;
511
512    setIsPressed(true);
513
514    if (variant === "mercury" || variant === "ripple") {
515      const factor = intensityFactors[intensity];
516      const newDroplets = Array.from({ length: 15 }).map(() => {
517        const angle = Math.random() * Math.PI * 2;
518        const speed = Math.random() * 3 * factor + 2;
519
520        return {
521          x: mousePosition.x,
522          y: mousePosition.y,
523          size: Math.random() * 12 * factor + 5,
524          opacity: 0.9,
525          speedX: Math.cos(angle) * speed,
526          speedY: Math.sin(angle) * speed,
527          life: 1.0,
528        };
529      });
530
531      setDroplets((prev) => [...prev, ...newDroplets]);
532    }
533  };
534
535  const handleMouseUp = () => {
536    if (clickEffect) {
537      setIsPressed(false);
538    }
539  };
540
541  const getButtonClasses = useCallback(() => {
542    switch (variant) {
543      case "outline":
544        return cn(
545          "bg-transparent border-2",
546          currentTheme.text,
547          currentTheme.border
548        );
549      case "ghost":
550        return cn(
551          "bg-transparent hover:bg-gray-100/20 dark:hover:bg-gray-800/30",
552          currentTheme.text
553        );
554      case "gradient":
555        return cn(
556          currentTheme.base,
557          currentTheme.text,
558          "bg-size-200 bg-pos-0 hover:bg-pos-100 transition-all duration-300"
559        );
560      case "mercury":
561      case "ripple":
562        return cn(
563          currentTheme.base,
564          currentTheme.text,
565          "relative overflow-hidden"
566        );
567      default:
568        return cn(currentTheme.base, currentTheme.text);
569    }
570  }, [variant, currentTheme]);
571
572  const Comp = "button";
573
574  const handleClick = () => {
575    if (onClick) {
576      onClick();
577    }
578    setIsPressed(true);
579    setTimeout(() => setIsPressed(false), 500);
580  };
581
582  return (
583    <Comp
584      ref={buttonRef}
585      onClick={handleClick}
586      className={cn(
587        "relative inline-flex items-center justify-center font-medium transition-all duration-200",
588        sizeClasses[size],
589        roundedClasses[rounded],
590        typeof shadow === "string"
591          ? shadowClasses[shadow]
592          : shadow
593          ? shadowClasses.true
594          : "",
595        shadow && currentTheme.glow,
596        getButtonClasses(),
597        textured && currentTheme.texture,
598        isPressed && "scale-95 transform",
599        hoverAnimation && "hover:-translate-y-0.5 hover:brightness-105",
600        "focus:outline-none focus:ring-2 focus:ring-offset-2 focus-visible:ring-opacity-75",
601        "focus-visible:ring-offset-2 focus-visible:ring-opacity-75",
602        "focus-visible:ring-blue-400 dark:focus-visible:ring-blue-500",
603        className
604      )}
605      onMouseEnter={handleMouseEnter}
606      onMouseLeave={handleMouseLeave}
607      onMouseDown={handleMouseDown}
608      onMouseUp={handleMouseUp}
609      style={{
610        willChange: "transform",
611        transition: isHovered
612          ? "transform 0.1s cubic-bezier(0.2, 0.8, 0.2, 1), filter 0.2s ease, opacity 0.2s ease"
613          : "transform 0.3s ease-out, filter 0.3s ease, opacity 0.3s ease",
614      }}
615      {...props}
616    >
617      {(variant === "mercury" || variant === "ripple") && (
618        <canvas
619          ref={canvasRef}
620          className="absolute inset-0 pointer-events-none opacity-90"
621          style={{ borderRadius: "inherit" }}
622        />
623      )}
624
625      <span className="relative z-10 flex items-center gap-2">
626        {icon && <span className="inline-flex">{icon}</span>}
627        <span>{children}</span>
628        {iconAfter && <span className="inline-flex">{iconAfter}</span>}
629      </span>
630
631      {variant !== "ghost" && variant !== "outline" && (
632        <div
633          className="absolute inset-0 pointer-events-none overflow-hidden"
634          style={{ borderRadius: "inherit" }}
635        >
636          <div
637            className="absolute inset-0 opacity-70 transition-all duration-300"
638            style={{
639              background: `linear-gradient(135deg, ${currentTheme.highlight} 0%, transparent 50%, ${currentTheme.shadow} 100%)`,
640              transform: isHovered
641                ? `rotate(${
642                    (mousePosition.x / buttonDimensions.width) * 45
643                  }deg) scale(1.05)`
644                : "rotate(0deg) scale(1)",
645              transition: isHovered
646                ? "transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.2s ease"
647                : "transform 0.4s ease-out, opacity 0.3s ease",
648            }}
649          />
650
651          <div
652            className="absolute inset-0 opacity-40"
653            style={{
654              background: `radial-gradient(ellipse at ${
655                isHovered
656                  ? `${(mousePosition.x / buttonDimensions.width) * 100}% ${
657                      (mousePosition.y / buttonDimensions.height) * 100
658                    }%`
659                  : "center"
660              }, 
661                           ${currentTheme.highlight} 0%, 
662                           transparent 70%)`,
663              transition: "opacity 0.3s ease",
664              opacity: isHovered ? 0.6 : 0.2,
665            }}
666          />
667        </div>
668      )}
669
670      {variant !== "ghost" && variant !== "outline" && (
671        <div
672          className="absolute inset-0 pointer-events-none opacity-20"
673          style={{
674            borderRadius: "inherit",
675            boxShadow:
676              "inset 0 1px 3px rgba(0,0,0,0.2), inset 0 -1px 2px rgba(255,255,255,0.2)",
677          }}
678        />
679      )}
680    </Comp>
681  );
682}
683
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
variantstring"default"The variant of the button. Possible values: "default", "outline", "ghost", "mercury", "ripple", "gradient".
sizestring"md"The size of the button. Possible values: "xs", "sm", "md", "lg", "xl", "2xl".
themestring"silver"The color theme of the button. Possible values: "silver", "gold", "copper", "mercury", "steel", "obsidian", "emerald", "ruby", "sapphire", "custom".
customColorsobjectundefinedCustom colors for the button when theme is set to "custom". The object should include properties: base, highlight, shadow, and optionally text, border, and glow.
intensitynumber3The intensity of the liquid effect (1-5).
magneticbooleantrueWhether to enable the magnetic pull effect.
clickEffectbooleantrueWhether to enable the click animation.
asChildbooleanfalseWhether to render the button as a child element.
roundedstring"md"The border radius of the button. Possible values: "none", "sm", "md", "lg", "full".
shadowboolean | stringtrueWhether to show a shadow effect. Can be a boolean or a specific shadow size: "sm", "md", "lg", "xl".
hoverAnimationbooleantrueWhether to enable hover animation.
texturedbooleanfalseWhether to apply a textured effect.
iconReact.ReactNodeundefinedIcon to be displayed before the children.
iconAfterReact.ReactNodeundefinedIcon to be displayed after the children.
classNamestring""Additional CSS classes to apply.
childrenReact.ReactNodeundefinedContent of the button.
onClick(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => voidundefinedCallback function to be called when the button is clicked.

Examples