DHxWhy

Early Access Pass CTA – Email Signup Section that Issues a Holographic Pass

CTAs 3

About

Early Access Pass CTA for React and Tailwind CSS (shadcn/ui): an email signup section that issues a membership-style pass on submit, printing the name, rolling the number up and stamping the date. It is built to turn a signup into something visitors can hold. The pass tilts toward the pointer with a moving sheen; the form validates the address, shows Try again when the promise rejects and announces the issue politely. Controlled or uncontrolled pass, split or stacked layout, reduced motion, light and dark. Use it for waitlists, newsletters, betas and event registration. The included demo types, issues, retries once and makes no requests. Requires React only, with no animation library.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Early Access Pass CTA – Email Signup Section that Issues a Holographic Pass to your project and fits it to your theme.

Code

How Early Access Pass CTA – Email Signup Section that Issues a Holographic Pass is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import * as React from "react";
import { EarlyAccessPassCta, nameFromEmail } from "@/components/ui/early-access-pass-cta";
import type { EarlyAccessPassData, EarlyAccessPassLayout } from "@/components/ui/early-access-pass-cta";

const EMAILS = ["jordan@alder.studio", "mina@alder.studio"];
const FIRST_NUMBER = 1284;
const LAYOUTS: EarlyAccessPassLayout[] = ["auto", "split", "stack"];

function orbit(target: HTMLElement, duration: number, onDone: () => void) {
  const rect = target.getBoundingClientRect();
  const centre = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  const fire = (type: string, x: number, y: number) => {
    target.dispatchEvent(new PointerEvent(type, { clientX: x, clientY: y, bubbles: true, cancelable: true, pointerType: "mouse", pointerId: 1, isPrimary: true }));
  };
  let frame = 0;
  const started = performance.now();
  fire("pointerenter", centre.x, centre.y);
  const tick = (now: number) => {
    const progress = Math.min(1, (now - started) / duration);
    const reach = Math.sin(progress * Math.PI);
    const angle = -Math.PI / 2 + progress * Math.PI * 2;
    const x = centre.x + Math.cos(angle) * rect.width * 0.44 * reach;
    const y = centre.y + Math.sin(angle) * rect.height * 0.44 * reach;
    fire("pointermove", x, y);
    if (progress < 1) { frame = requestAnimationFrame(tick); return; }
    fire("pointerleave", x, y);
    onDone();
  };
  frame = requestAnimationFrame(tick);
  return () => { cancelAnimationFrame(frame); fire("pointerleave", centre.x, centre.y); };
}

export default function Demo({ autoplay = true }: { autoplay?: boolean }) {
  const params = React.useMemo(() => new URLSearchParams(typeof location === "undefined" ? "" : location.search), []);
  const speed = Number(params.get("speed") ?? 1) || 1;
  const fail = params.get("fail");
  const lock = params.get("lock") === "1";
  const uncontrolled = params.get("uncontrolled") === "1";
  const layout = LAYOUTS.find((value) => value === params.get("layout")) ?? "auto";
  const tilt = params.get("tilt") !== "0";
  const sheen = params.get("sheen") !== "0";
  const preset = params.get("preset") === "1";

  const [email, setEmail] = React.useState(preset ? EMAILS[0]! : "");
  const [pass, setPass] = React.useState((preset ? { name: "Jordan", number: FIRST_NUMBER, issuedAt: new Date() } : null) as EarlyAccessPassData | null);
  const [requested, setRequested] = React.useState("");
  const [status, setStatus] = React.useState("idle");
  const [run, setRun] = React.useState(0);
  const [paused, setPaused] = React.useState(false);
  const [reduced, setReduced] = React.useState(false);
  const sectionRef = React.useRef(null as HTMLElement | null);
  const attempts = React.useRef(0);
  const runRef = React.useRef(0);
  const realPointer = React.useRef(false);
  const cancelOrbit = React.useRef(null as (() => void) | null);
  runRef.current = run;

  const failsFirst = (index: number) => fail === "1" || (fail !== "0" && index % 2 === 1);

  React.useEffect(() => {
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setReduced(media.matches);
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  React.useEffect(() => {
    if (!autoplay || paused) return;
    const timers: number[] = [];
    const at = (ms: number, task: () => void) => timers.push(window.setTimeout(task, Math.round(ms * speed)));
    const form = () => sectionRef.current?.querySelector("form");
    const text = EMAILS[run % EMAILS.length] ?? EMAILS[0]!;
    attempts.current = 0;
    for (let i = 1; i <= text.length; i += 1) at(500 + (i - 1) * 90, () => setEmail(text.slice(0, i)));
    at(2400, () => form()?.requestSubmit());
    const retry = failsFirst(run);
    if (retry) at(4000, () => form()?.requestSubmit());
    at(retry ? 5800 : 4200, () => {
      const card = sectionRef.current?.querySelector("[data-pass]");
      if (!(card instanceof HTMLElement) || realPointer.current || reduced) return;
      cancelOrbit.current = orbit(card, Math.round(2400 * speed), () => { cancelOrbit.current = null; });
    });
    at(12000, () => { setPass(null); setEmail(""); setRun((previous) => previous + 1); });
    return () => { timers.forEach((timer) => window.clearTimeout(timer)); cancelOrbit.current?.(); cancelOrbit.current = null; };
  }, [autoplay, paused, run, speed, fail, reduced]);

  const takeOver = () => { if (autoplay) setPaused(true); cancelOrbit.current?.(); cancelOrbit.current = null; };
  const watchPointer = (event: React.PointerEvent) => {
    if (!event.isTrusted) return;
    realPointer.current = true;
    cancelOrbit.current?.();
    cancelOrbit.current = null;
  };

  const submit = async (address: string) => {
    const index = runRef.current;
    attempts.current += 1;
    await new Promise((resolve) => window.setTimeout(resolve, Math.round(600 * speed)));
    if (failsFirst(index) && attempts.current === 1) throw new Error("Network hiccup");
    return { name: nameFromEmail(address), number: FIRST_NUMBER + index, issuedAt: new Date() };
  };

  return (
    <main
      data-demo-root
      data-run={run}
      data-requested={requested}
      data-host-status={status}
      onPointerDownCapture={takeOver}
      onKeyDownCapture={takeOver}
      onPointerMoveCapture={watchPointer}
      className="flex min-h-[min(100svh,760px)] w-full items-center justify-center bg-background px-6 py-8 text-foreground sm:px-10"
    >
      <EarlyAccessPassCta
        key={uncontrolled ? run : "demo"}
        ref={sectionRef}
        className="max-w-[1040px]"
        brand={{ name: "Alder" }}
        eyebrow="Early access"
        heading="Be first on the list."
        lead="Alder opens to a small group each month. Leave your email and we will hold a pass with your name on it."
        note="No spam. One email when we open."
        email={email}
        onEmailChange={setEmail}
        onSubmit={submit}
        onStatusChange={setStatus}
        layout={layout}
        tilt={tilt}
        sheen={sheen}
        {...(uncontrolled
          ? {}
          : { pass, onPassChange: (next: EarlyAccessPassData | null) => { setRequested(next ? String(next.number) : ""); if (!lock) setPass(next); } })}
      />
    </main>
  );
}

Dependencies

More by DHxWhy

View all
  • Product Rail Gallery – Fashion Store Hero Section

    Product Rail Gallery – Fashion Store Hero Section

    2.2K
  • Sushi Conveyor Belt Menu – Restaurant Hero Section

    Sushi Conveyor Belt Menu – Restaurant Hero Section

    900
  • Team Lanyard Rail – Meet the Team Section with Flip Badges

    Team Lanyard Rail – Meet the Team Section with Flip Badges

    851
  • Skateboard Deck Rail Gallery – Skate Shop Hero Section

    Skateboard Deck Rail Gallery – Skate Shop Hero Section

    444
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    331
  • Picture Rail Image Gallery – Art Print Shop Hero Section

    Picture Rail Image Gallery – Art Print Shop Hero Section

    280
  • Pendant Lamp Rail Gallery – Lighting Store Hero Section

    Pendant Lamp Rail Gallery – Lighting Store Hero Section

    248
  • World Testimonials Globe – Social Proof Hero Section

    World Testimonials Globe – Social Proof Hero Section

    222

More in CTAs

View all
  • Hills Landscape Footer with Call to Action

    Hills Landscape Footer with Call to Action

    90
  • Call to Action

    Call to Action

    6
  • Call to Action 01

    Call to Action 01

    56
  • CTA 3

    CTA 3

    411
  • Cta 4

    Cta 4

    300
  • Responsive Hero Banner

    Responsive Hero Banner

    2.1K
  • How It Works Steps

    How It Works Steps

    90
  • Two-Plan Pricing Cards

    Two-Plan Pricing Cards

    37