Waitlist Signup Component

Build high-converting waitlist signup forms with validation, social proof, and seamless user experience to grow your pre-launch audience.

Waitlist Signup Overview

The waitlist signup component helps you collect email addresses and build anticipation before your official launch with conversion-optimized forms.

🎯 Conversion Features

  • • Email validation and verification
  • • Duplicate prevention
  • • Social proof display
  • • Success confirmation
  • • Mobile-optimized design
  • • A/B testing ready

🔧 Technical Features

  • • Server-side validation
  • • Real-time feedback
  • • Loading states
  • • Error handling
  • • Analytics tracking
  • • Accessibility support

Basic Implementation

Simple Waitlist Form

Clean, conversion-focused waitlist signup form:

src/components/WaitlistSignup.tsx
1"use client";
2
3import { useState } from "react";
4import { Button } from "~/components/ui/button";
5import { Input } from "~/components/ui/input";
6import { Alert, AlertDescription } from "~/components/ui/alert";
7import { CheckCircle, AlertCircle, Loader2 } from "lucide-react";
8import { addToWaitlist } from "~/server/actions/waitlist";
9
10export function WaitlistSignup() {
11 const [email, setEmail] = useState("");
12 const [isLoading, setIsLoading] = useState(false);
13 const [message, setMessage] = useState<{
14 type: "success" | "error";
15 text: string;
16 } | null>(null);
17
18 const handleSubmit = async (e: React.FormEvent) => {
19 e.preventDefault();
20
21 if (!email) {
22 setMessage({ type: "error", text: "Please enter your email address" });
23 return;
24 }
25
26 setIsLoading(true);
27 setMessage(null);
28
29 try {
30 const result = await addToWaitlist(email);
31
32 if (result.success) {
33 setMessage({
34 type: "success",
35 text: "🎉 You&apos;re on the waitlist! We&apos;ll notify you when we launch."
36 });
37 setEmail("");
38
39 // Track conversion
40 if (typeof window !== "undefined" && window.gtag) {
41 window.gtag("event", "waitlist_signup", {
42 event_category: "engagement",
43 event_label: "email_signup"
44 });
45 }
46 } else {
47 setMessage({
48 type: "error",
49 text: result.error || "Something went wrong. Please try again."
50 });
51 }
52 } catch (error) {
53 setMessage({
54 type: "error",
55 text: "Network error. Please check your connection and try again."
56 });
57 } finally {
58 setIsLoading(false);
59 }
60 };
61
62 return (
63 <div className="w-full max-w-md mx-auto">
64 <form onSubmit={handleSubmit} className="space-y-4">
65 <div className="flex gap-2">
66 <Input
67 type="email"
68 placeholder="Enter your email address"
69 value={email}
70 onChange={(e) => setEmail(e.target.value)}
71 disabled={isLoading}
72 className="flex-1"
73 required
74 />
75 <Button
76 type="submit"
77 disabled={isLoading || !email}
78 className="shrink-0"
79 >
80 {isLoading ? (
81 <>
82 <Loader2 className="mr-2 h-4 w-4 animate-spin" />
83 Joining...
84 </>
85 ) : (
86 "Join Waitlist"
87 )}
88 </Button>
89 </div>
90
91 {message && (
92 <Alert className={`${
93 message.type === "success"
94 ? "border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200"
95 : "border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200"
96 }`}>
97 {message.type === "success" ? (
98 <CheckCircle className="h-4 w-4" />
99 ) : (
100 <AlertCircle className="h-4 w-4" />
101 )}
102 <AlertDescription>{message.text}</AlertDescription>
103 </Alert>
104 )}
105
106 <p className="text-xs text-center text-muted-foreground">
107 We respect your privacy. Unsubscribe at any time.
108 </p>
109 </form>
110 </div>
111 );
112}
Enhanced Waitlist Section

Full section with social proof, benefits, and enhanced UX:

src/components/WaitlistSection.tsx
1"use client";
2
3import { useState, useEffect } from "react";
4import { Card, CardContent } from "~/components/ui/card";
5import { Badge } from "~/components/ui/badge";
6import { WaitlistSignup } from "./WaitlistSignup";
7import { getWaitlistStats } from "~/server/actions/waitlist";
8
9export function WaitlistSection() {
10 const [stats, setStats] = useState<{
11 total: number;
12 recentSignups: number;
13 }>({ total: 0, recentSignups: 0 });
14
15 useEffect(() => {
16 async function loadStats() {
17 try {
18 const data = await getWaitlistStats();
19 setStats(data);
20 } catch (error) {
21 console.error("Failed to load waitlist stats:", error);
22 }
23 }
24
25 loadStats();
26 }, []);
27
28 return (
29 <section className="py-16 px-4">
30 <div className="container mx-auto max-w-4xl">
31 <div className="text-center space-y-8">
32 {/* Header */}
33 <div className="space-y-4">
34 <Badge variant="secondary" className="text-sm">
35 🚀 Coming Soon
36 </Badge>
37 <h2 className="text-4xl font-bold">
38 Be the First to Know
39 </h2>
40 <p className="text-xl text-muted-foreground max-w-2xl mx-auto">
41 Join our waitlist to get early access, exclusive updates,
42 and special launch pricing when we go live.
43 </p>
44 </div>
45
46 {/* Social Proof */}
47 {stats.total > 0 && (
48 <div className="flex items-center justify-center gap-6 text-sm text-muted-foreground">
49 <div className="flex items-center gap-2">
50 <span className="font-semibold text-foreground">
51 {stats.total.toLocaleString()}
52 </span>
53 <span>people joined</span>
54 </div>
55 {stats.recentSignups > 0 && (
56 <>
57 <span></span>
58 <div className="flex items-center gap-2">
59 <span className="font-semibold text-foreground">
60 {stats.recentSignups}
61 </span>
62 <span>joined this week</span>
63 </div>
64 </>
65 )}
66 </div>
67 )}
68
69 {/* Signup Form */}
70 <div className="max-w-md mx-auto">
71 <WaitlistSignup />
72 </div>
73
74 {/* Benefits */}
75 <div className="grid md:grid-cols-3 gap-6 mt-12">
76 <Card className="border-0 bg-muted/50">
77 <CardContent className="p-6 text-center space-y-3">
78 <div className="w-12 h-12 mx-auto bg-primary/10 rounded-full flex items-center justify-center">
79 <span className="text-2xl">🚀</span>
80 </div>
81 <h3 className="font-semibold">Early Access</h3>
82 <p className="text-sm text-muted-foreground">
83 Get exclusive access before the public launch
84 </p>
85 </CardContent>
86 </Card>
87
88 <Card className="border-0 bg-muted/50">
89 <CardContent className="p-6 text-center space-y-3">
90 <div className="w-12 h-12 mx-auto bg-primary/10 rounded-full flex items-center justify-center">
91 <span className="text-2xl">💰</span>
92 </div>
93 <h3 className="font-semibold">Special Pricing</h3>
94 <p className="text-sm text-muted-foreground">
95 Exclusive discounts for waitlist members
96 </p>
97 </CardContent>
98 </Card>
99
100 <Card className="border-0 bg-muted/50">
101 <CardContent className="p-6 text-center space-y-3">
102 <div className="w-12 h-12 mx-auto bg-primary/10 rounded-full flex items-center justify-center">
103 <span className="text-2xl">📧</span>
104 </div>
105 <h3 className="font-semibold">Updates</h3>
106 <p className="text-sm text-muted-foreground">
107 Behind-the-scenes progress and launch updates
108 </p>
109 </CardContent>
110 </Card>
111 </div>
112
113 {/* Trust Indicators */}
114 <div className="flex items-center justify-center gap-4 text-xs text-muted-foreground">
115 <span>✅ No spam ever</span>
116 <span></span>
117 <span>✅ Unsubscribe anytime</span>
118 <span></span>
119 <span>✅ Your data is secure</span>
120 </div>
121 </div>
122 </div>
123 </section>
124 );
125}

Server Actions

Waitlist Server Actions

Secure server-side email handling with validation:

src/server/actions/waitlist.ts
1"use server";
2
3import { db } from "~/server/db";
4import { z } from "zod";
5import { sendWaitlistWelcomeEmail } from "~/server/lib/emailService";
6import { logger } from "~/lib/logger";
7
8const emailSchema = z.string().email("Please enter a valid email address");
9
10export async function addToWaitlist(email: string) {
11 try {
12 // Validate email
13 const validEmail = emailSchema.parse(email.toLowerCase().trim());
14
15 // Check for existing entry
16 const existing = await db.waitlist.findUnique({
17 where: { email: validEmail },
18 });
19
20 if (existing) {
21 if (!existing.isActive) {
22 // Reactivate if previously unsubscribed
23 await db.waitlist.update({
24 where: { email: validEmail },
25 data: {
26 isActive: true,
27 updatedAt: new Date(),
28 },
29 });
30
31 return {
32 success: true,
33 message: "Welcome back! You&apos;re back on the waitlist."
34 };
35 }
36
37 return {
38 success: false,
39 error: "You&apos;re already on our waitlist! 🎉"
40 };
41 }
42
43 // Add to waitlist
44 const waitlistEntry = await db.waitlist.create({
45 data: {
46 email: validEmail,
47 isActive: true,
48 source: "website", // Track signup source
49 ipAddress: "", // Add IP for analytics (optional)
50 },
51 });
52
53 // Send welcome email (async, don&apos;t wait)
54 sendWaitlistWelcomeEmail(validEmail).catch(error => {
55 logger.error("Failed to send waitlist welcome email", error, {
56 email: validEmail,
57 waitlistId: waitlistEntry.id,
58 });
59 });
60
61 // Log successful signup
62 logger.info("New waitlist signup", {
63 email: validEmail,
64 waitlistId: waitlistEntry.id,
65 source: "website",
66 });
67
68 return {
69 success: true,
70 message: "Success! Check your email for confirmation."
71 };
72
73 } catch (error) {
74 if (error instanceof z.ZodError) {
75 return {
76 success: false,
77 error: error.errors[0]?.message || "Invalid email address"
78 };
79 }
80
81 logger.error("Waitlist signup error", error, { email });
82
83 return {
84 success: false,
85 error: "Something went wrong. Please try again."
86 };
87 }
88}
89
90export async function getWaitlistStats() {
91 try {
92 const [total, recentSignups] = await Promise.all([
93 db.waitlist.count({
94 where: { isActive: true },
95 }),
96 db.waitlist.count({
97 where: {
98 isActive: true,
99 createdAt: {
100 gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // Last 7 days
101 },
102 },
103 }),
104 ]);
105
106 return { total, recentSignups };
107 } catch (error) {
108 logger.error("Failed to get waitlist stats", error);
109 return { total: 0, recentSignups: 0 };
110 }
111}
112
113export async function unsubscribeFromWaitlist(email: string) {
114 try {
115 const validEmail = emailSchema.parse(email.toLowerCase().trim());
116
117 await db.waitlist.update({
118 where: { email: validEmail },
119 data: {
120 isActive: false,
121 updatedAt: new Date(),
122 },
123 });
124
125 logger.info("Waitlist unsubscribe", { email: validEmail });
126
127 return { success: true };
128 } catch (error) {
129 logger.error("Waitlist unsubscribe error", error, { email });
130 return { success: false, error: "Failed to unsubscribe" };
131 }
132}
Welcome Email Integration

Automated welcome emails for new waitlist members:

Waitlist welcome email
1// src/server/lib/emailService.ts
2import { Resend } from "resend";
3
4const resend = new Resend(process.env.RESEND_API_KEY);
5
6export async function sendWaitlistWelcomeEmail(email: string) {
7 try {
8 const { data, error } = await resend.emails.send({
9 from: `${process.env.COMPANY_NAME} <${process.env.FROM_EMAIL}>`,
10 to: [email],
11 subject: `Welcome to the ${process.env.COMPANY_NAME} waitlist! 🎉`,
12 html: generateWaitlistWelcomeHTML(email),
13 });
14
15 if (error) {
16 throw new Error(`Email send error: ${error.message}`);
17 }
18
19 return data;
20 } catch (error) {
21 console.error("Waitlist welcome email error:", error);
22 throw error;
23 }
24}
25
26function generateWaitlistWelcomeHTML(email: string) {
27 return `
28 <!DOCTYPE html>
29 <html>
30 <head>
31 <meta charset="utf-8">
32 <meta name="viewport" content="width=device-width, initial-scale=1.0">
33 <title>Welcome to the Waitlist!</title>
34 </head>
35 <body style="margin: 0; padding: 0; background-color: #f7fafc; font-family: system-ui, sans-serif;">
36 <table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f7fafc; padding: 40px 0;">
37 <tr>
38 <td align="center">
39 <table width="600" cellpadding="0" cellspacing="0" style="background-color: white; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
40 <tr>
41 <td style="padding: 40px; text-align: center;">
42 <h1 style="margin: 0 0 20px; color: #1a202c; font-size: 28px; font-weight: bold;">
43 🎉 You&apos;re on the waitlist!
44 </h1>
45
46 <p style="margin: 0 0 20px; color: #4a5568; font-size: 16px; line-height: 1.6;">
47 Thanks for joining our waitlist! You&apos;re now part of an exclusive group
48 who will be the first to experience ${process.env.COMPANY_NAME}.
49 </p>
50
51 <div style="background-color: #f7fafc; padding: 20px; border-radius: 6px; margin: 20px 0;">
52 <h3 style="margin: 0 0 10px; color: #2d3748; font-size: 18px;">What&apos;s Next?</h3>
53 <ul style="margin: 0; padding: 0 0 0 20px; text-align: left; color: #4a5568;">
54 <li style="margin-bottom: 8px;">We&apos;ll send you progress updates as we build</li>
55 <li style="margin-bottom: 8px;">You&apos;ll get early access before the public launch</li>
56 <li style="margin-bottom: 8px;">Exclusive pricing for waitlist members</li>
57 <li>Behind-the-scenes content and development insights</li>
58 </ul>
59 </div>
60
61 <p style="margin: 20px 0 0; color: #718096; font-size: 14px;">
62 Follow our progress on social media for real-time updates!
63 </p>
64 </td>
65 </tr>
66
67 <tr>
68 <td style="padding: 20px; text-align: center; border-top: 1px solid #e2e8f0; background-color: #f7fafc;">
69 <p style="margin: 0; color: #718096; font-size: 12px;">
70 You&apos;re receiving this because you signed up for our waitlist with ${email}.
71 <br>
72 <a href="${process.env.COMPANY_URL}/unsubscribe?email=${encodeURIComponent(email)}"
73 style="color: #4299e1; text-decoration: none;">
74 Unsubscribe
75 </a>
76 </p>
77 </td>
78 </tr>
79 </table>
80 </td>
81 </tr>
82 </table>
83 </body>
84 </html>
85 `;
86}

Advanced Features

Referral System

Add viral growth with referral tracking:

Referral system
1"use client";
2
3import { useState } from "react";
4import { Button } from "~/components/ui/button";
5import { Input } from "~/components/ui/input";
6import { Card, CardContent } from "~/components/ui/card";
7import { Share2, Copy, Check } from "lucide-react";
8
9export function WaitlistReferral({ userEmail }: { userEmail: string }) {
10 const [copied, setCopied] = useState(false);
11
12 // Generate unique referral code
13 const referralCode = btoa(userEmail).slice(0, 8);
14 const referralUrl = `${window.location.origin}/waitlist?ref=${referralCode}`;
15
16 const copyToClipboard = async () => {
17 await navigator.clipboard.writeText(referralUrl);
18 setCopied(true);
19 setTimeout(() => setCopied(false), 2000);
20 };
21
22 const shareReferral = async () => {
23 if (navigator.share) {
24 await navigator.share({
25 title: "Join the RankThis waitlist",
26 text: "Get early access to RankThis!",
27 url: referralUrl,
28 });
29 } else {
30 copyToClipboard();
31 }
32 };
33
34 return (
35 <Card className="mt-6">
36 <CardContent className="p-6 space-y-4">
37 <div className="text-center">
38 <h3 className="font-semibold mb-2">Invite Friends & Move Up</h3>
39 <p className="text-sm text-muted-foreground">
40 Refer friends to move up in line and get exclusive perks!
41 </p>
42 </div>
43
44 <div className="flex gap-2">
45 <Input
46 value={referralUrl}
47 readOnly
48 className="flex-1"
49 />
50 <Button
51 variant="outline"
52 size="sm"
53 onClick={copyToClipboard}
54 className="shrink-0"
55 >
56 {copied ? (
57 <Check className="h-4 w-4" />
58 ) : (
59 <Copy className="h-4 w-4" />
60 )}
61 </Button>
62 <Button
63 variant="outline"
64 size="sm"
65 onClick={shareReferral}
66 className="shrink-0"
67 >
68 <Share2 className="h-4 w-4" />
69 </Button>
70 </div>
71
72 <div className="grid grid-cols-3 gap-3 text-center text-xs">
73 <div>
74 <div className="font-medium">3 referrals</div>
75 <div className="text-muted-foreground">Week early access</div>
76 </div>
77 <div>
78 <div className="font-medium">5 referrals</div>
79 <div className="text-muted-foreground">25% discount</div>
80 </div>
81 <div>
82 <div className="font-medium">10 referrals</div>
83 <div className="text-muted-foreground">Free first month</div>
84 </div>
85 </div>
86 </CardContent>
87 </Card>
88 );
89}
Progress Indicator

Show launch progress to build anticipation:

Launch progress indicator
1export function LaunchProgress() {
2 // You can update these values manually or fetch from your CMS
3 const milestones = [
4 { label: "Concept & Design", completed: true },
5 { label: "Core Development", completed: true },
6 { label: "Beta Testing", completed: false, current: true },
7 { label: "Launch Preparation", completed: false },
8 { label: "Public Launch", completed: false },
9 ];
10
11 const completedCount = milestones.filter(m => m.completed).length;
12 const progress = (completedCount / milestones.length) * 100;
13
14 return (
15 <Card className="mt-8">
16 <CardContent className="p-6">
17 <div className="text-center mb-6">
18 <h3 className="font-semibold mb-2">Launch Progress</h3>
19 <div className="w-full bg-muted rounded-full h-2">
20 <div
21 className="bg-primary h-2 rounded-full transition-all duration-500"
22 style={{ width: `${progress}%` }}
23 />
24 </div>
25 <p className="text-sm text-muted-foreground mt-2">
26 {progress.toFixed(0)}% complete • Launching soon!
27 </p>
28 </div>
29
30 <div className="space-y-3">
31 {milestones.map((milestone, index) => (
32 <div key={index} className="flex items-center gap-3">
33 <div className={`w-3 h-3 rounded-full ${
34 milestone.completed
35 ? "bg-green-500"
36 : milestone.current
37 ? "bg-primary animate-pulse"
38 : "bg-muted"
39 }`} />
40 <span className={`text-sm ${
41 milestone.completed
42 ? "text-foreground line-through"
43 : milestone.current
44 ? "text-foreground font-medium"
45 : "text-muted-foreground"
46 }`}>
47 {milestone.label}
48 </span>
49 </div>
50 ))}
51 </div>
52 </CardContent>
53 </Card>
54 );
55}
💡 Waitlist Conversion Best Practices

Conversion Optimization

  • • Use clear, benefit-focused headlines
  • • Show social proof (signup count)
  • • Minimize form fields (email only)
  • • Add trust indicators
  • • Use urgency carefully
  • • Test different CTAs

Technical Best Practices

  • • Validate emails server-side
  • • Handle duplicates gracefully
  • • Provide immediate feedback
  • • Track analytics events
  • • Ensure mobile responsiveness
  • • Add keyboard accessibility

Waitlist Signup Complete!

Your waitlist signup forms are optimized for maximum conversions. Next, learn about customizing email templates.