Waitlist Signup

Conversion-optimized signup forms for building your pre-launch audience.

Conversion

  • • Email validation
  • • Duplicate prevention
  • • Social proof display
  • • Success confirmation

Technical

  • • Server-side validation
  • • Real-time feedback
  • • Loading states
  • • Accessibility support

Signup Form

src/components/WaitlistSignup.tsx
1"use client";
2
3import { useState } from "react";
4import { addToWaitlist } from "~/server/actions/waitlist";
5
6export function WaitlistSignup() {
7 const [email, setEmail] = useState("");
8 const [isLoading, setIsLoading] = useState(false);
9 const [message, setMessage] = useState(null);
10
11 const handleSubmit = async (e) => {
12 e.preventDefault();
13 setIsLoading(true);
14
15 const result = await addToWaitlist(email);
16
17 if (result.success) {
18 setMessage({ type: "success", text: "You're on the list!" });
19 setEmail("");
20 } else {
21 setMessage({ type: "error", text: result.error });
22 }
23
24 setIsLoading(false);
25 };
26
27 return (
28 <form onSubmit={handleSubmit} className="flex gap-2">
29 <input
30 type="email"
31 placeholder="Enter your email"
32 value={email}
33 onChange={(e) => setEmail(e.target.value)}
34 disabled={isLoading}
35 required
36 />
37 <button type="submit" disabled={isLoading}>
38 {isLoading ? "Joining..." : "Join"}
39 </button>
40
41 {message && (
42 <p className={message.type === "success" ? "text-green-600" : "text-red-600"}>
43 {message.text}
44 </p>
45 )}
46 </form>
47 );
48}

Server Action

src/server/actions/waitlist.ts
1"use server";
2
3import { db } from "~/server/db";
4import { z } from "zod";
5
6const emailSchema = z.string().email("Please enter a valid email");
7
8export async function addToWaitlist(email: string) {
9 try {
10 const validEmail = emailSchema.parse(email.toLowerCase().trim());
11
12 const existing = await db.waitlist.findUnique({
13 where: { email: validEmail },
14 });
15
16 if (existing) {
17 return { success: false, error: "You're already on the list!" };
18 }
19
20 await db.waitlist.create({
21 data: { email: validEmail, isActive: true },
22 });
23
24 return { success: true, message: "You're on the list!" };
25
26 } catch (error) {
27 if (error instanceof z.ZodError) {
28 return { success: false, error: error.errors[0]?.message };
29 }
30 return { success: false, error: "Something went wrong" };
31 }
32}

Social Proof

1// Fetch waitlist stats
2export async function getWaitlistStats() {
3 const [total, recentSignups] = await Promise.all([
4 db.waitlist.count({ where: { isActive: true } }),
5 db.waitlist.count({
6 where: {
7 isActive: true,
8 createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
9 },
10 }),
11 ]);
12
13 return { total, recentSignups };
14}
15
16// Display
17{stats.total > 0 && (
18 <div className="text-sm text-muted-foreground">
19 <strong>{stats.total.toLocaleString()}</strong> people joined
20 </div>
21)}

Best Practices

Conversion

  • • Clear, benefit-focused headlines
  • • Show social proof
  • • Minimize form fields
  • • Add trust indicators

Technical

  • • Validate server-side
  • • Handle duplicates gracefully
  • • Provide immediate feedback
  • • Ensure mobile responsive

Documentation Complete

Return to the documentation overview

Back to Docs →