---
title: Confetti Button
description: An interactive button that triggers a confetti animation effect when clicked or hovered.
---

<ComponentPreview name="confetti-button-demo" />

## Installation

<Tabs defaultValue="cli">

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx lightswind@latest add confetti-button
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

```tsx
import React, { useEffect, useState, useRef } from "react";
import { cn } from "@/components/lib/utils";
import { Loader2 } from "lucide-react";
import { cva, type VariantProps } from "class-variance-authority";

// Confetti type
type ConfettiOptions = {
  particleCount?: number;
  spread?: number;
  startVelocity?: number;
  decay?: number;
  gravity?: number;
  drift?: number;
  ticks?: number;
  origin?: {
    x?: number;
    y?: number;
  };
  colors?: string[];
  shapes?: string[];
  scalar?: number;
  zIndex?: number;
  disableForReducedMotion?: boolean;
};

// Global declaration
declare global {
  interface Window {
    confetti?: (options?: ConfettiOptions) => void;
  }
}

// Variants for button styling
const confettiButtonVariants = cva(
  "inline-flex items-center justify-center gap-2 font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/90",
        outline: "border bg-background hover:bg-accent hover:text-accent-foreground",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
        gradient: "bg-gradient-to-r from-primarylw to-purple-600 text-white hover:from-primarylw hover:to-purple-700",
      },
      size: {
        default: "h-10 px-4 py-2 rounded-md",
        sm: "h-8 px-3 py-1 rounded-md text-sm",
        lg: "h-12 px-6 py-3 rounded-md text-lg",
        xl: "h-14 px-8 py-4 rounded-md text-xl",
        icon: "h-10 w-10 rounded-full",
        pill: "h-10 px-6 py-2 rounded-full",
      },
      animation: {
        none: "",
        pulse: "animate-pulse",
        bounce: "hover:animate-bounce",
        scale: "active:scale-95",
        shake: "hover:animate-[wiggle_0.3s_ease-in-out]",
        glow: "hover:shadow-[0_0_15px_rgba(255,255,255,0.5)]",
        expand: "active:scale-110 transition-transform",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
      animation: "scale",
    },
  }
);

export interface ConfettiButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof confettiButtonVariants> {
  asChild?: boolean;
  icon?: React.ReactNode;
  iconPosition?: "left" | "right";
  loading?: boolean;
  confettiOptions?: ConfettiOptions;
  autoConfetti?: boolean;
  triggerOnHover?: boolean;
}

const ConfettiButton = React.forwardRef<HTMLButtonElement, ConfettiButtonProps>(
  (
    {
      className,
      variant,
      size,
      animation,
      asChild = false,
      children,
      icon,
      iconPosition = "left",
      loading = false,
      confettiOptions = {
        particleCount: 100,
        spread: 70,
      },
      autoConfetti = false,
      triggerOnHover = false,
      ...props
    },
    ref
  ) => {
    const [scriptLoaded, setScriptLoaded] = useState(false);
    const buttonRef = useRef<HTMLButtonElement | null>(null);

    // Load confetti script dynamically
    useEffect(() => {
      if (window.confetti) {
        setScriptLoaded(true);
        return;
      }

      const existingScript = document.querySelector(
        'script[src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.4.0/dist/confetti.browser.min.js"]'
      ) as HTMLScriptElement;

      if (existingScript) {
        const handleLoad = () => setScriptLoaded(true);
        existingScript.addEventListener("load", handleLoad);
        if (window.confetti) {
          setScriptLoaded(true);
        }
        return () => {
          existingScript.removeEventListener("load", handleLoad);
        };
      }

      const script = document.createElement("script");
      script.src =
        "https://cdn.jsdelivr.net/npm/canvas-confetti@1.4.0/dist/confetti.browser.min.js";
      script.async = true;
      script.onload = () => setScriptLoaded(true);
      document.body.appendChild(script);
    }, []);

    // Auto confetti on mount if needed
    useEffect(() => {
      if ((scriptLoaded || window.confetti) && autoConfetti && buttonRef.current) {
        const rect = buttonRef.current.getBoundingClientRect();
        const x = (rect.left + rect.width / 2) / window.innerWidth;
        const y = (rect.top + rect.height / 2) / window.innerHeight;

        const confettiFn = window.confetti;
        if (confettiFn) {
          confettiFn({
            ...confettiOptions,
            origin: { x, y },
          });
        }
      }
    }, [scriptLoaded, autoConfetti, confettiOptions]);

    const triggerConfetti = () => {
      const confettiFn = window.confetti;
      if (confettiFn && buttonRef.current) {
        const rect = buttonRef.current.getBoundingClientRect();
        const x = (rect.left + rect.width / 2) / window.innerWidth;
        const y = (rect.top + rect.height / 2) / window.innerHeight;

        confettiFn({
          ...confettiOptions,
          origin: { x, y },
        });
      }
    };

    return (
      <button
        ref={(node) => {
          if (typeof ref === "function") ref(node);
          else if (ref) ref.current = node;
          buttonRef.current = node;
        }}
        className={cn(confettiButtonVariants({ variant, size, animation }), className)}
        onClick={(e) => {
          triggerConfetti();
          props.onClick?.(e);
        }}
        onMouseEnter={triggerOnHover ? () => triggerConfetti() : undefined}
        disabled={loading || props.disabled}
        {...props}
      >
        {loading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
        {!loading && icon && iconPosition === "left" && (
          <span className="mr-1">{icon}</span>
        )}
        {children}
        {!loading && icon && iconPosition === "right" && (
          <span className="ml-1">{icon}</span>
        )}
      </button>
    );
  }
);

ConfettiButton.displayName = "ConfettiButton";

export { ConfettiButton, confettiButtonVariants };

```

</Steps>

</TabsContent>

</Tabs>

## Usage

```tsx
import { ConfettiButton } from "@/components/lightswind/confetti-button";
```

```tsx
import { ConfettiButton } from "@/components/lightswind/confetti-button";
import { Sparkles } from "lucide-react";

export function ConfettiButtonDemo() {
  return (
    <ConfettiButton
      icon={<Sparkles className="h-4 w-4" />}
      confettiOptions={{
        particleCount: 100,
        spread: 70
      }}
    >
      Click for Confetti
    </ConfettiButton>
  );
}
```
