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";23import { 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";910export 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);1718 const handleSubmit = async (e: React.FormEvent) => {19 e.preventDefault();2021 if (!email) {22 setMessage({ type: "error", text: "Please enter your email address" });23 return;24 }2526 setIsLoading(true);27 setMessage(null);2829 try {30 const result = await addToWaitlist(email);3132 if (result.success) {33 setMessage({34 type: "success",35 text: "🎉 You're on the waitlist! We'll notify you when we launch."36 });37 setEmail("");3839 // Track conversion40 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 };6162 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 <Input67 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 required74 />75 <Button76 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>9091 {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 )}105106 <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";23import { 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";89export function WaitlistSection() {10 const [stats, setStats] = useState<{11 total: number;12 recentSignups: number;13 }>({ total: 0, recentSignups: 0 });1415 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 }2425 loadStats();26 }, []);2728 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 Soon36 </Badge>37 <h2 className="text-4xl font-bold">38 Be the First to Know39 </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>4546 {/* 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 )}6869 {/* Signup Form */}70 <div className="max-w-md mx-auto">71 <WaitlistSignup />72 </div>7374 {/* 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 launch84 </p>85 </CardContent>86 </Card>8788 <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 members96 </p>97 </CardContent>98 </Card>99100 <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 updates108 </p>109 </CardContent>110 </Card>111 </div>112113 {/* 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";23import { db } from "~/server/db";4import { z } from "zod";5import { sendWaitlistWelcomeEmail } from "~/server/lib/emailService";6import { logger } from "~/lib/logger";78const emailSchema = z.string().email("Please enter a valid email address");910export async function addToWaitlist(email: string) {11 try {12 // Validate email13 const validEmail = emailSchema.parse(email.toLowerCase().trim());1415 // Check for existing entry16 const existing = await db.waitlist.findUnique({17 where: { email: validEmail },18 });1920 if (existing) {21 if (!existing.isActive) {22 // Reactivate if previously unsubscribed23 await db.waitlist.update({24 where: { email: validEmail },25 data: {26 isActive: true,27 updatedAt: new Date(),28 },29 });3031 return {32 success: true,33 message: "Welcome back! You're back on the waitlist."34 };35 }3637 return {38 success: false,39 error: "You're already on our waitlist! 🎉"40 };41 }4243 // Add to waitlist44 const waitlistEntry = await db.waitlist.create({45 data: {46 email: validEmail,47 isActive: true,48 source: "website", // Track signup source49 ipAddress: "", // Add IP for analytics (optional)50 },51 });5253 // Send welcome email (async, don'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 });6061 // Log successful signup62 logger.info("New waitlist signup", {63 email: validEmail,64 waitlistId: waitlistEntry.id,65 source: "website",66 });6768 return {69 success: true,70 message: "Success! Check your email for confirmation."71 };7273 } catch (error) {74 if (error instanceof z.ZodError) {75 return {76 success: false,77 error: error.errors[0]?.message || "Invalid email address"78 };79 }8081 logger.error("Waitlist signup error", error, { email });8283 return {84 success: false,85 error: "Something went wrong. Please try again."86 };87 }88}8990export 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 days101 },102 },103 }),104 ]);105106 return { total, recentSignups };107 } catch (error) {108 logger.error("Failed to get waitlist stats", error);109 return { total: 0, recentSignups: 0 };110 }111}112113export async function unsubscribeFromWaitlist(email: string) {114 try {115 const validEmail = emailSchema.parse(email.toLowerCase().trim());116117 await db.waitlist.update({118 where: { email: validEmail },119 data: {120 isActive: false,121 updatedAt: new Date(),122 },123 });124125 logger.info("Waitlist unsubscribe", { email: validEmail });126127 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.ts2import { Resend } from "resend";34const resend = new Resend(process.env.RESEND_API_KEY);56export 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 });1415 if (error) {16 throw new Error(`Email send error: ${error.message}`);17 }1819 return data;20 } catch (error) {21 console.error("Waitlist welcome email error:", error);22 throw error;23 }24}2526function 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're on the waitlist!44 </h1>4546 <p style="margin: 0 0 20px; color: #4a5568; font-size: 16px; line-height: 1.6;">47 Thanks for joining our waitlist! You're now part of an exclusive group48 who will be the first to experience ${process.env.COMPANY_NAME}.49 </p>5051 <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'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'll send you progress updates as we build</li>55 <li style="margin-bottom: 8px;">You'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>6061 <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>6667 <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'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 Unsubscribe75 </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";23import { 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";89export function WaitlistReferral({ userEmail }: { userEmail: string }) {10 const [copied, setCopied] = useState(false);1112 // Generate unique referral code13 const referralCode = btoa(userEmail).slice(0, 8);14 const referralUrl = `${window.location.origin}/waitlist?ref=${referralCode}`;1516 const copyToClipboard = async () => {17 await navigator.clipboard.writeText(referralUrl);18 setCopied(true);19 setTimeout(() => setCopied(false), 2000);20 };2122 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 };3334 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>4344 <div className="flex gap-2">45 <Input46 value={referralUrl}47 readOnly48 className="flex-1"49 />50 <Button51 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 <Button63 variant="outline"64 size="sm"65 onClick={shareReferral}66 className="shrink-0"67 >68 <Share2 className="h-4 w-4" />69 </Button>70 </div>7172 <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 CMS3 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 ];1011 const completedCount = milestones.filter(m => m.completed).length;12 const progress = (completedCount / milestones.length) * 100;1314 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 <div21 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>2930 <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.completed35 ? "bg-green-500"36 : milestone.current37 ? "bg-primary animate-pulse"38 : "bg-muted"39 }`} />40 <span className={`text-sm ${41 milestone.completed42 ? "text-foreground line-through"43 : milestone.current44 ? "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.