Back to snippets
tsx
v1.0.0
MIT

Animated Button Component

A customizable button with smooth hover animations and multiple variants

react
animation
ui
button
framer-motion
/**
* @title Animated Button Component
* @author TacoLabs
* @version 1.0.0
* @dependencies framer-motion, tailwindcss
* @created 2025-03-12
*/

import { motion } from 'framer-motion';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
  "relative inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none overflow-hidden",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
  ripple?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, ripple = true, asChild = false, ...props }, ref) => {
    const [rippleEffect, setRippleEffect] = useState<{ x: number; y: number; size: number } | null>(null);

    const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
      if (!ripple) return;
      
      const button = e.currentTarget;
      const rect = button.getBoundingClientRect();
      const size = Math.max(rect.width, rect.height) * 2;
      
      setRippleEffect({
        x: e.clientX - rect.left,
        y: e.clientY - rect.top,
        size,
      });
      
      setTimeout(() => setRippleEffect(null), 600);
    };

    return (
      <motion.button
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        onClick={handleClick}
        whileHover={{ scale: 1.02 }}
        whileTap={{ scale: 0.98 }}
        {...props}
      >
        {props.children}
        {rippleEffect && (
          <span
            className="absolute bg-white/20 rounded-full pointer-events-none animate-ripple"
            style={{
              left: rippleEffect.x - rippleEffect.size / 2,
              top: rippleEffect.y - rippleEffect.size / 2,
              width: rippleEffect.size,
              height: rippleEffect.size,
            }}
          />
        )}
      </motion.button>
    );
  }
);
Button.displayName = "Button";

export { Button, buttonVariants };
import { Button } from '@/components/ui/button' // Basic usage <Button>Click me</Button> // With variants <Button variant="destructive">Delete</Button> <Button variant="outline">Cancel</Button> // With sizes <Button size="sm">Small</Button> <Button size="lg">Large</Button> // With animation <Button ripple={true}>With Ripple Effect</Button>

Installation

To use this component, you need to install the following dependencies:

npm install framer-motion class-variance-authority tailwindcss

Snippet Information

TA
TacoLabs

Author

Created
10 months ago
Stars
42
Views
128
Version
1.0.0
License
MIT