Docs
Breeze Waitlist
Breeze Waitlist
A minimal, animated waitlist form component with Framer Motion transitions.
Loading...
Installation
Install the following dependencies:
npm install clsx tailwind-merge zod lucide-react canvas-confetti framer-motion
Add the utils.ts file to the @/lib folder
utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Create a breeze-waitlist.tsx file and copy the below code.
breeze-waitlist.tsx
"use client";
import React, { useState, RefObject, useEffect, useRef } from "react";
import { z } from "zod";
import { Mail, X } from "lucide-react";
import confetti from "canvas-confetti";
import { AnimatePresence, motion } from "framer-motion";
// Email validation schema
const emailSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
});
// Confetti animation
const launchConfetti = () => {
const duration = 4 * 1000;
const animationEnd = Date.now() + duration;
let skew = 1;
const randomInRange = (min: number, max: number) =>
Math.random() * (max - min) + min;
(function frame() {
const timeLeft = animationEnd - Date.now();
const ticks = Math.max(200, 500 * (timeLeft / duration));
skew = Math.max(0.8, skew - 0.001);
confetti({
particleCount: 1,
startVelocity: 0,
ticks,
origin: {
x: Math.random(),
y: Math.random() * skew - 0.2,
},
colors: ["#ffffff"],
shapes: ["circle"],
gravity: randomInRange(0.4, 0.6),
scalar: randomInRange(0.4, 1),
drift: randomInRange(-0.4, 0.4),
});
if (timeLeft > 0) {
requestAnimationFrame(frame);
}
})();
};
// Detect outside click
const useClickOutside = (
ref: RefObject<HTMLElement | null>,
handleOnClickOutside: (event: MouseEvent | TouchEvent) => void
) => {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handleOnClickOutside(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handleOnClickOutside]);
};
const BreezeWaitlist = () => {
const [formData, setFormData] = useState({ email: "" });
const [error, setError] = useState<string | null>(null);
const [submittedEmail, setSubmittedEmail] = useState("");
const [open, setOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [success, setSuccess] = useState(false);
// Email validation
const validate = (email: string) => {
const result = emailSchema.safeParse({ email });
setError(result.success ? null : result.error.issues[0].message);
return result.success;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ email: e.target.value });
validate(e.target.value);
};
// Handle form submission locally
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (validate(formData.email)) {
setSubmitting(true);
setTimeout(() => {
setSubmittedEmail(formData.email);
setSuccess(true);
launchConfetti();
setSubmitting(false);
}, 1000); // simulate async delay
}
};
const ref = useRef<HTMLDivElement>(null);
useClickOutside(ref, () => setOpen(false));
return (
<>
{/* Trigger Button */}
<motion.button
layoutId="waitlist-wrapper"
onClick={() => setOpen(true)}
className="group inline-flex items-center gap-2 rounded-xl bg-black text-white font-semibold px-6 py-3 hover:bg-black/60 transition cursor-pointer"
>
Get Early Access
<motion.span
layoutId="waitlist-title"
className="group-hover:translate-x-1 transition-transform"
>
👀
</motion.span>
</motion.button>
{/* Modal */}
<AnimatePresence>
{open && (
<motion.div
key="modal"
className="fixed inset-0 z-50 flex items-center justify-center px-4"
layoutId="waitlist-wrapper"
>
<div className="bg-amber-100/20 p-1 rounded-2xl">
<div
ref={ref}
className="bg-black/75 backdrop-blur-md w-full md:min-w-md p-6 md:px-10 rounded-2xl relative shadow-lg"
>
<button
onClick={() => setOpen(false)}
className="absolute top-4 right-4 text-white hover:bg-black rounded-full p-1 cursor-pointer"
>
<X size={20} />
</button>
<motion.span
aria-hidden
className="text-sm text-transparent"
layoutId="waitlist-title"
>
Get Early Access 👀
</motion.span>
<AnimatePresence mode="popLayout">
{success ? (
<motion.div
key="success"
initial={{ y: -32, opacity: 0, filter: "blur(4px)" }}
animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
transition={{ type: "spring", duration: 0.4, bounce: 0 }}
>
<div className="text-center space-y-2 p-10">
<p className="text-xl text-gray-100">You're in!</p>
<p className="text-xl text-gray-100">
We'll notify you early.
</p>
</div>
</motion.div>
) : (
<motion.div
exit={{ y: 8, opacity: 0, filter: "blur(4px)" }}
transition={{ type: "spring", duration: 0.4, bounce: 0 }}
key="open-child"
>
<form
onSubmit={handleFormSubmit}
className="flex flex-col items-start py-12"
>
<div className="flex gap-2 w-full">
<div className="w-full relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Mail className="text-muted-foreground size-4" />
</div>
<input
type="email"
required
placeholder="Enter your email"
className="pl-10 w-full px-4 py-3 text-muted-foreground rounded-xl border border-amber-100/20 focus:outline-none focus:ring-white/20 focus:border-white/20"
value={formData.email}
onChange={handleChange}
/>
</div>
<button
type="submit"
disabled={submitting}
className="group inline-flex items-center gap-2 rounded-xl bg-gradient-to-b from-white to-white/50 text-black font-semibold px-4 py-3 hover:bg-white transition cursor-pointer"
>
{submitting ? "Submitting..." : "Submit"}
</button>
</div>
{error && (
<p className="text-red-400 text-sm pl-2 pt-1">
{error}
</p>
)}
</form>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
};
export default BreezeWaitlist;
Update the import paths to match your project setup.
Usage
import BreezeWaitlist from "@/components/breezeblocks/breeze-waitlist";
<div className="__waitlist">
<BreezeWaitlist />
</div>
Props
Prop | Type | Description | Default |
---|---|---|---|
— | — | This component takes no props. | — |
On This Page