Components
Button
Button
Ultra-modern button component with 3D effects, multiple variants, loading states, and smooth animations for enhanced user interaction.
Basic Variants
Sizes
With Icons
Loading States
Disabled States
Interactive Demo
Click the buttons below to see the 3D effects and animations in action!
Installation
1
Install the packages
npm i motion clsx tailwind-merge lucide-react2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
button.tsx
"use client";
import { motion, AnimatePresence } from "motion/react";
import React from "react";
import { cn } from "@/lib/utils";
import { Loader2 } from "lucide-react";
interface ButtonProps {
variant?: "primary" | "secondary" | "ghost" | "outline" | "destructive";
size?: "sm" | "md" | "lg" | "xl";
loading?: boolean;
disabled?: boolean;
icon?: React.ReactNode;
iconPosition?: "left" | "right";
children?: React.ReactNode;
onClick?: () => void;
className?: string;
}
const Button: React.FC<ButtonProps> = ({
variant = "primary",
size = "md",
loading = false,
disabled = false,
icon,
iconPosition = "left",
children,
onClick,
className,
}) => {
const isDisabled = disabled || loading;
const baseClasses = "relative inline-flex items-center justify-center font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed overflow-hidden";
const variantClasses = {
primary: "bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-lg hover:shadow-xl hover:from-blue-600 hover:to-purple-700 focus:ring-blue-500",
secondary: "bg-gradient-to-r from-gray-100 to-gray-200 text-gray-900 shadow-md hover:shadow-lg hover:from-gray-200 hover:to-gray-300 focus:ring-gray-500 dark:from-gray-800 dark:to-gray-700 dark:text-white dark:hover:from-gray-700 dark:hover:to-gray-600",
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:ring-gray-500 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white",
outline: "border-2 border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 hover:border-gray-400 focus:ring-gray-500 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:border-gray-500",
destructive: "bg-gradient-to-r from-red-500 to-pink-600 text-white shadow-lg hover:shadow-xl hover:from-red-600 hover:to-pink-700 focus:ring-red-500",
};
const sizeClasses = {
sm: "px-3 py-1.5 text-sm rounded-lg gap-1.5",
md: "px-4 py-2 text-base rounded-lg gap-2",
lg: "px-6 py-3 text-lg rounded-xl gap-2.5",
xl: "px-8 py-4 text-xl rounded-2xl gap-3",
};
const buttonVariants = {
primary: {
scale: [1, 0.98, 1],
transition: { duration: 0.2 }
},
secondary: {
scale: [1, 0.98, 1],
transition: { duration: 0.2 }
},
ghost: {
scale: [1, 1.02, 1],
transition: { duration: 0.2 }
},
outline: {
scale: [1, 0.98, 1],
transition: { duration: 0.2 }
},
destructive: {
scale: [1, 0.95, 1],
transition: { duration: 0.3 }
},
};
return (
<motion.button
className={cn(
baseClasses,
variantClasses[variant],
sizeClasses[size],
isDisabled && "opacity-60 cursor-not-allowed",
className
)}
onClick={isDisabled ? undefined : onClick}
disabled={isDisabled}
whileHover={!isDisabled ? { y: -2 } : {}}
whileTap={!isDisabled ? buttonVariants[variant] : {}}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/* 3D Shadow Effect */}
<motion.div
className="absolute inset-0 bg-black/10 rounded-inherit"
initial={{ y: 0 }}
whileHover={{ y: 1 }}
transition={{ duration: 0.2 }}
/>
{/* Ripple Effect */}
<motion.div
className="absolute inset-0 bg-white/20 rounded-inherit"
initial={{ scale: 0, opacity: 1 }}
whileTap={{ scale: 4, opacity: 0 }}
transition={{ duration: 0.4 }}
/>
{/* Content */}
<AnimatePresence mode="wait">
{loading ? (
<motion.div
key="loading"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="flex items-center gap-2"
>
<Loader2 className="w-4 h-4 animate-spin" />
<span>Loading...</span>
</motion.div>
) : (
<motion.div
key="content"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="flex items-center relative z-10"
>
{icon && iconPosition === "left" && (
<motion.span
initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
>
{icon}
</motion.span>
)}
{children && (
<motion.span
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.15 }}
>
{children}
</motion.span>
)}
{icon && iconPosition === "right" && (
<motion.span
initial={{ x: 10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
>
{icon}
</motion.span>
)}
</motion.div>
)}
</AnimatePresence>
{/* Glow Effect for Primary */}
{variant === "primary" && (
<motion.div
className="absolute inset-0 bg-gradient-to-r from-blue-400/20 to-purple-400/20 rounded-inherit blur-xl"
initial={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
)}
</motion.button>
);
};
export default Button;"use client";
import { motion, AnimatePresence } from "motion/react";
import React from "react";
import { cn } from "@/lib/utils";
import { Loader2 } from "lucide-react";
interface ButtonProps {
variant?: "primary" | "secondary" | "ghost" | "outline" | "destructive";
size?: "sm" | "md" | "lg" | "xl";
loading?: boolean;
disabled?: boolean;
icon?: React.ReactNode;
iconPosition?: "left" | "right";
children?: React.ReactNode;
onClick?: () => void;
className?: string;
}
const Button: React.FC<ButtonProps> = ({
variant = "primary",
size = "md",
loading = false,
disabled = false,
icon,
iconPosition = "left",
children,
onClick,
className,
}) => {
const isDisabled = disabled || loading;
const baseClasses = "relative inline-flex items-center justify-center font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed overflow-hidden";
const variantClasses = {
primary: "bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-lg hover:shadow-xl hover:from-blue-600 hover:to-purple-700 focus:ring-blue-500",
secondary: "bg-gradient-to-r from-gray-100 to-gray-200 text-gray-900 shadow-md hover:shadow-lg hover:from-gray-200 hover:to-gray-300 focus:ring-gray-500 dark:from-gray-800 dark:to-gray-700 dark:text-white dark:hover:from-gray-700 dark:hover:to-gray-600",
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:ring-gray-500 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white",
outline: "border-2 border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 hover:border-gray-400 focus:ring-gray-500 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:border-gray-500",
destructive: "bg-gradient-to-r from-red-500 to-pink-600 text-white shadow-lg hover:shadow-xl hover:from-red-600 hover:to-pink-700 focus:ring-red-500",
};
const sizeClasses = {
sm: "px-3 py-1.5 text-sm rounded-lg gap-1.5",
md: "px-4 py-2 text-base rounded-lg gap-2",
lg: "px-6 py-3 text-lg rounded-xl gap-2.5",
xl: "px-8 py-4 text-xl rounded-2xl gap-3",
};
const buttonVariants = {
primary: {
scale: [1, 0.98, 1],
transition: { duration: 0.2 }
},
secondary: {
scale: [1, 0.98, 1],
transition: { duration: 0.2 }
},
ghost: {
scale: [1, 1.02, 1],
transition: { duration: 0.2 }
},
outline: {
scale: [1, 0.98, 1],
transition: { duration: 0.2 }
},
destructive: {
scale: [1, 0.95, 1],
transition: { duration: 0.3 }
},
};
return (
<motion.button
className={cn(
baseClasses,
variantClasses[variant],
sizeClasses[size],
isDisabled && "opacity-60 cursor-not-allowed",
className
)}
onClick={isDisabled ? undefined : onClick}
disabled={isDisabled}
whileHover={!isDisabled ? { y: -2 } : {}}
whileTap={!isDisabled ? buttonVariants[variant] : {}}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/* 3D Shadow Effect */}
<motion.div
className="absolute inset-0 bg-black/10 rounded-inherit"
initial={{ y: 0 }}
whileHover={{ y: 1 }}
transition={{ duration: 0.2 }}
/>
{/* Ripple Effect */}
<motion.div
className="absolute inset-0 bg-white/20 rounded-inherit"
initial={{ scale: 0, opacity: 1 }}
whileTap={{ scale: 4, opacity: 0 }}
transition={{ duration: 0.4 }}
/>
{/* Content */}
<AnimatePresence mode="wait">
{loading ? (
<motion.div
key="loading"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="flex items-center gap-2"
>
<Loader2 className="w-4 h-4 animate-spin" />
<span>Loading...</span>
</motion.div>
) : (
<motion.div
key="content"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="flex items-center relative z-10"
>
{icon && iconPosition === "left" && (
<motion.span
initial={{ x: -10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
>
{icon}
</motion.span>
)}
{children && (
<motion.span
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.15 }}
>
{children}
</motion.span>
)}
{icon && iconPosition === "right" && (
<motion.span
initial={{ x: 10, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
>
{icon}
</motion.span>
)}
</motion.div>
)}
</AnimatePresence>
{/* Glow Effect for Primary */}
{variant === "primary" && (
<motion.div
className="absolute inset-0 bg-gradient-to-r from-blue-400/20 to-purple-400/20 rounded-inherit blur-xl"
initial={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
)}
</motion.button>
);
};
export default Button;4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | 'primary' | 'secondary' | 'ghost' | 'outline' | 'destructive' | 'primary' | The visual style variant of the button. |
| size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The size of the button. |
| loading | boolean | false | Shows loading spinner and disables interaction. |
| disabled | boolean | false | Disables the button interaction. |
| icon | ReactNode | undefined | Icon to display in the button. |
| iconPosition | 'left' | 'right' | 'left' | Position of the icon relative to text. |
| children | ReactNode | undefined | The button content/text. |
| onClick | () => void | () => {} | Click handler function. |