HextaUIHextaUI
/Components/Application

Waitlist Form

Animated and minimal waitlist form with email input and submit button.

Preview

Join the waitlist for theHextaUI

Installation

WaitlistForm.tsx
"use client";

import * as React from "react";
import { useState, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
declare module "canvas-confetti";

// Constants
const SMALL_BREAKPOINT = 570;

const containerVariants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1, transition: { staggerChildren: 0.2 } },
};

const childVariants = {
  hidden: { opacity: 0, filter: "blur(10px)" },
  visible: {
    opacity: 1,
    filter: "blur(0px)",
    transition: { duration: 0.5 },
  },
};

// Hook to track window width
function useWindowWidth() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    setWidth(window.innerWidth);
    const handleResize = () => setWidth(window.innerWidth);

    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return width;
}

const WaitlistForm = () => {
  const [email, setEmail] = useState("");
  const [submitted, setSubmitted] = useState(false);
  const width = useWindowWidth();

  const handleSubmit = useCallback((e: React.FormEvent) => {
    e.preventDefault();
    setSubmitted(true);

    import("canvas-confetti").then((confetti) => {
      confetti.default({
        particleCount: 100,
        spread: 70,
        origin: { y: 0.6 },
      });
    });
  }, []);

  const ThankYouMessage = (
    <motion.div
      layout
      initial={{ opacity: 0, filter: "blur(10px)" }}
      animate={{
        opacity: 1,
        filter: "blur(0px)",
        transition: { duration: 0.5 },
      }}
      exit={{
        opacity: 0,
        filter: "blur(10px)",
        transition: { duration: 0.5 },
      }}
    >
      <div className="flex flex-col items-center justify-center text-center">
        <h1 className="text-4xl font-medium bg-gradient-to-b from-[#00000030] to-[#000] bg-clip-text text-transparent">
          Thank you for joining the waitlist!
        </h1>
        <p className="mt-4 text-lg text-gray-700">
          We will notify you with updates soon.
        </p>
      </div>
    </motion.div>
  );

  const FormContent = (
    <motion.div
      layout
      key="form"
      className="w-full"
      initial={{ opacity: 0, filter: "blur(10px)" }}
      animate={{
        opacity: 1,
        filter: "blur(0px)",
        transition: { duration: 0.5 },
      }}
      exit={{
        opacity: 0,
        filter: "blur(10px)",
        transition: { duration: 0.5 },
      }}
    >
      <motion.div
        className={` ${
          width <= SMALL_BREAKPOINT ? "text-left mb-4" : "text-center mb-8"
        }`}
        variants={childVariants}
      >
        <h1 className="text-4xl font-medium bg-gradient-to-b from-[#00000030] to-[#000] bg-clip-text text-transparent">
          Join the waitlist for the
          <span
            className={`pl-2 ${
              width <= SMALL_BREAKPOINT ? "" : "block"
            } bg-gradient-to-r from-[#ff9292] to-[#ff0000] bg-clip-text text-transparent`}
          >
            HextaUI
          </span>
        </h1>
      </motion.div>

      <motion.form
        onSubmit={handleSubmit}
        className={
          width <= SMALL_BREAKPOINT
            ? "flex flex-col gap-4 items-start w-full"
            : "relative w-full "
        }
        variants={childVariants}
      >
        <div
          className={
            width <= SMALL_BREAKPOINT
              ? "flex flex-col gap-4 w-full"
              : "flex flex-row items-center w-full space-x-4 rounded-full border border-gray-200 bg-white p-2"
          }
        >
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="name@example.com"
            className={`${
              width <= SMALL_BREAKPOINT
                ? "px-4 py-4 w-full text-lg focus:outline-none rounded-full border border-gray-200 bg-white"
                : "flex-1 rounded-full bg-transparent px-4 py-4 text-lg focus:outline-none"
            }`}
            required
          />
          <button
            type="submit"
            className={`${
              width <= SMALL_BREAKPOINT
                ? "rounded-full bg-[#3a3aff] px-6 py-4 text-lg font-medium text-white hover:bg-[#4a2aff] transition w-full"
                : "rounded-full bg-[#3a3aff] px-6 py-4 text-lg font-medium text-white hover:bg-[#4a2aff] transition"
            }`}
          >
            Join the waitlist
          </button>
        </div>
      </motion.form>
    </motion.div>
  );

  return (
    <motion.div
      layout
      className={`w-[95%] max-w-2xl rounded-4xl bg-white border border-black/10 shadow-2xl/10 ${
        width <= SMALL_BREAKPOINT ? "p-8" : "p-12"
      } min-h-[300px] flex items-center`}
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      <AnimatePresence mode="wait">
        {!submitted ? FormContent : ThankYouMessage}
      </AnimatePresence>
    </motion.div>
  );
};

export { WaitlistForm };

Usage

App.tsx
import { WaitlistForm } from "@/components/library/WaitlistForm";

export default function App() {
  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-100">
      <WaitlistForm />
    </div>
  );
}
Edit on GitHub

Last updated on