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
Configuration1import { 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
TypeScript1"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
Name | Type | Default | Description |
---|---|---|---|
variant | string | "default" | The variant of the button. Possible values: "default", "outline", "ghost", "mercury", "ripple", "gradient". |
size | string | "md" | The size of the button. Possible values: "xs", "sm", "md", "lg", "xl", "2xl". |
theme | string | "silver" | The color theme of the button. Possible values: "silver", "gold", "copper", "mercury", "steel", "obsidian", "emerald", "ruby", "sapphire", "custom". |
customColors | object | undefined | Custom colors for the button when theme is set to "custom". The object should include properties: base, highlight, shadow, and optionally text, border, and glow. |
intensity | number | 3 | The intensity of the liquid effect (1-5). |
magnetic | boolean | true | Whether to enable the magnetic pull effect. |
clickEffect | boolean | true | Whether to enable the click animation. |
asChild | boolean | false | Whether to render the button as a child element. |
rounded | string | "md" | The border radius of the button. Possible values: "none", "sm", "md", "lg", "full". |
shadow | boolean | string | true | Whether to show a shadow effect. Can be a boolean or a specific shadow size: "sm", "md", "lg", "xl". |
hoverAnimation | boolean | true | Whether to enable hover animation. |
textured | boolean | false | Whether to apply a textured effect. |
icon | React.ReactNode | undefined | Icon to be displayed before the children. |
iconAfter | React.ReactNode | undefined | Icon to be displayed after the children. |
className | string | "" | Additional CSS classes to apply. |
children | React.ReactNode | undefined | Content of the button. |
onClick | (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void | undefined | Callback function to be called when the button is clicked. |