More Forms Snippets
Waitlist Signup — Early Access Form HTML CSS JS
Waitlist Signup · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Waitlist Signup — Email Capture with Queue Position & Referral Sharing

A waitlist does two jobs before you've launched: it captures demand as a list of warm leads, and — done well — it turns each signup into a promoter through referral mechanics. This snippet builds the complete modern waitlist in plain HTML, CSS, and vanilla JavaScript: a signup card with social proof and email validation, then a confirmation that reveals the user's queue position and a referral link they can share to move up the line.
Social proof before the ask
Above the email field sits a small stack of avatars and a live count ("2,847 people waiting"). Showing that others have already joined is one of the most effective ways to increase signups — a waitlist nobody's on feels risky to join, while one with thousands already waiting signals the product is worth waiting for. The avatar stack uses overlapping circles (the standard "people" motif) built purely with CSS gradients, so it needs no images.
Validation before commitment
Submitting validates the email against a pragmatic regex and shows an inline error for an invalid address before anything else happens — there's no point sending someone to the confirmation state with a typo'd email that can never be contacted. The validation is intentionally simple (catching the common mistakes, not chasing full RFC compliance), which is the right trade-off for a low-friction signup.
The position reveal — why it works
On success, the card swaps to a confirmation showing "You're #1,284 in line." A concrete position transforms an abstract "you're signed up" into something the user has a stake in — and crucially, sets up the referral mechanic. Seeing a number they'd like to lower is exactly what motivates sharing, far more than a generic "tell your friends" plea. The position should come from your backend (the demo simulates it); it's the anchor the whole referral loop hangs on.
Referral sharing to skip ahead
Below the position is a unique referral link and a copy button, framed with the incentive: "Each friend who joins moves you up." This is the viral loop that powers launches like Robinhood and Superhuman — every signup is handed a personal stake and a tool to recruit others, turning a static lead list into compounding growth. The copy button uses the modern navigator.clipboard API with a graceful execCommand fallback for older browsers, and confirms with a green "✓ Copied" state so the user knows it worked.
Two states, one card
The signup form and the confirmation are two states of one card, toggled with the hidden attribute — keeping the user anchored in place rather than navigating away, which matters for a flow whose whole point is to immediately hand them the referral tool. In production the submit handler is where you'd POST the email to your waitlist service (which returns the real position and referral code); the demo generates both client-side so the full flow is visible without a backend.
Honest, portable, and adaptable
The referral code here is derived from the email for demonstration, but a real implementation must generate it server-side and track referrals authoritatively — client-side codes can be forged. The component is otherwise self-contained and easy to re-theme: swap the product name, the social-proof count, and the accent color, and it fits any pre-launch product, beta program, or early-access drop.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA waitlist card renders with social proof, an early-access badge, and an email signup form.
- 2Enter an emailType an invalid email to see the inline error; a valid one proceeds to the confirmation.
- 3See your positionThe card swaps to "You're #1,284 in line" — a concrete number that motivates sharing.
- 4Copy the referral linkClick Copy to copy the unique referral URL; the button confirms with a green "✓ Copied" state.
- 5Customize itChange the product name, the social-proof count, and the accent color to fit your launch.
- 6Connect a waitlist backendPOST the email to your waitlist service in the submit handler and use the real position and referral code it returns.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
In the submit handler, after validation, POST the email to your waitlist service (a tool like Waitlist/GetWaitlist, or your own endpoint). It should return the user's real queue position and a unique referral code; populate the position display and referral link from that response, then show the confirmation state. Handle a failure by showing the inline error instead of advancing.
Each signup gets a unique referral code embedded in their share link. When someone joins via that link, your backend credits the referrer and recalculates queue positions (e.g. each referral moves them up N spots, or ranks by referral count). The position and ranking logic must live server-side and be authoritative — client-side codes and counts can be forged, so never trust them for the actual ordering.
A visible "thousands waiting" count reduces the risk of joining an empty list, and a concrete position number gives the user a personal stake. Together they drive both the initial signup and the subsequent sharing — the position is the thing referrals improve, so it's the hook that makes the referral link compelling rather than a generic "tell your friends."
Use navigator.clipboard.writeText() (the modern async API) with a fallback to selecting the input and document.execCommand('copy') for older browsers, as shown. Clipboard access requires a secure context (HTTPS or localhost); on insecure origins the execCommand fallback handles it. Always confirm the copy visually so the user knows it succeeded.
In React, hold the email, error, and submitted state in useState and conditionally render the signup vs confirmation, calling your waitlist API in the submit handler; in Vue, use ref()/reactive() with v-if; in Angular, use a reactive form with *ngIf. The validation regex and clipboard logic port unchanged — only the two-state toggle and the async API call move into the framework.
Waitlist Signup — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Waitlist Signup</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.wls-card{background:#fff;border-radius:20px;padding:30px 28px;width:100%;max-width:400px;box-shadow:0 24px 60px rgba(0,0,0,.35);text-align:center}
.wls-badge{display:inline-block;background:#eef2ff;color:#4f46e5;font-size:11px;font-weight:800;padding:5px 12px;border-radius:999px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:14px}
.wls-card h2{font-size:23px;font-weight:800;color:#0f172a;margin-bottom:8px}
.wls-card p{font-size:13px;color:#64748b;line-height:1.55}
.wls-avatars{display:flex;align-items:center;justify-content:center;gap:0;margin:16px 0 18px}
.wls-avatars i{width:26px;height:26px;border-radius:50%;border:2px solid #fff;margin-left:-7px}
.wls-avatars i:first-child{margin-left:0}
.wls-avatars i:nth-child(1){background:linear-gradient(135deg,#6366f1,#8b5cf6)}
.wls-avatars i:nth-child(2){background:linear-gradient(135deg,#22c55e,#10b981)}
.wls-avatars i:nth-child(3){background:linear-gradient(135deg,#f59e0b,#f97316)}
.wls-avatars i:nth-child(4){background:linear-gradient(135deg,#ec4899,#db2777)}
.wls-avatars span{font-size:12px;font-weight:700;color:#94a3b8;margin-left:10px}
.wls-row{display:flex;gap:8px}
.wls-row input{flex:1;border:1.5px solid #e2e8f0;border-radius:10px;padding:12px 13px;font-size:14px;font-family:inherit;color:#0f172a}
.wls-row input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.wls-row button{background:#6366f1;color:#fff;border:none;border-radius:10px;padding:0 22px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s}
.wls-row button:hover{background:#4f46e5}
.wls-error{color:#dc2626;font-size:12px;font-weight:600;text-align:left;margin-top:8px}
.wls-check{width:54px;height:54px;border-radius:50%;background:#22c55e;color:#fff;font-size:26px;font-weight:800;display:flex;align-items:center;justify-content:center;margin:0 auto 14px}
.wls-pos{font-size:14px;color:#475569;margin-top:6px}
.wls-pos b{color:#6366f1;font-size:18px;font-weight:800}
.wls-refbox{margin-top:20px;background:#f8fafc;border-radius:12px;padding:15px}
.wls-refbox p{font-size:12px;color:#475569;margin-bottom:10px;font-weight:600}
.wls-ref{display:flex;gap:7px}
.wls-ref input{flex:1;border:1.5px solid #e2e8f0;border-radius:8px;padding:9px 11px;font-size:12px;color:#475569;font-family:inherit;background:#fff}
.wls-ref button{background:#0f172a;color:#fff;border:none;border-radius:8px;padding:0 16px;font-size:12.5px;font-weight:700;cursor:pointer;min-width:64px}
.wls-ref button.copied{background:#16a34a}
</style>
</head>
<body>
<div class="wls-card">
<div class="wls-join" id="wlsJoin">
<span class="wls-badge">🚀 Early access</span>
<h2>Join the waitlist</h2>
<p>Be first to try Nimbus when we launch. We're onboarding in batches.</p>
<div class="wls-avatars"><i></i><i></i><i></i><i></i><span>2,847 people waiting</span></div>
<form id="wlsForm" novalidate>
<div class="wls-row">
<input type="email" id="wlsEmail" placeholder="[email protected]" autocomplete="email">
<button type="submit">Join</button>
</div>
<p class="wls-error" id="wlsError" hidden></p>
</form>
</div>
<div class="wls-done" id="wlsDone" hidden>
<div class="wls-check">✓</div>
<h2>You're on the list!</h2>
<p class="wls-pos">You're <b id="wlsPos">#1,284</b> in line</p>
<div class="wls-refbox">
<p>Skip ahead — share your link. Each friend who joins moves you up.</p>
<div class="wls-ref">
<input type="text" id="wlsRef" readonly>
<button type="button" id="wlsCopy">Copy</button>
</div>
</div>
</div>
</div>
<script>
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var join = document.getElementById('wlsJoin');
var done = document.getElementById('wlsDone');
document.getElementById('wlsForm').addEventListener('submit', function (e) {
e.preventDefault();
var input = document.getElementById('wlsEmail');
var error = document.getElementById('wlsError');
var email = input.value.trim();
if (!EMAIL_RE.test(email)) {
error.textContent = 'Please enter a valid email address.';
error.hidden = false;
return;
}
error.hidden = true;
// In production: POST the email to your waitlist API, which returns the real
// queue position and a unique referral code. Here we simulate both.
var position = 1284;
var code = btoa(email).replace(/[^a-z0-9]/gi, '').slice(0, 8).toLowerCase();
document.getElementById('wlsPos').textContent = '#' + position.toLocaleString();
document.getElementById('wlsRef').value = 'nimbus.app/?ref=' + code;
join.hidden = true;
done.hidden = false;
});
document.getElementById('wlsCopy').addEventListener('click', function () {
var ref = document.getElementById('wlsRef');
navigator.clipboard.writeText(ref.value).catch(function () {
ref.select(); try { document.execCommand('copy'); } catch (e) {}
});
var btn = this;
btn.textContent = '✓ Copied';
btn.classList.add('copied');
setTimeout(function () { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Waitlist Signup</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.wls-card h2 {
font-size:23px;font-weight:800;color:#0f172a;margin-bottom:8px
}
.wls-card p {
font-size:13px;color:#64748b;line-height:1.55
}
.wls-avatars i {
width:26px;height:26px;border-radius:50%;border:2px solid #fff;margin-left:-7px
}
.wls-avatars i:first-child {
margin-left:0
}
.wls-avatars i:nth-child(1) {
background:linear-gradient(135deg,#6366f1,#8b5cf6)
}
.wls-avatars i:nth-child(2) {
background:linear-gradient(135deg,#22c55e,#10b981)
}
.wls-avatars i:nth-child(3) {
background:linear-gradient(135deg,#f59e0b,#f97316)
}
.wls-avatars i:nth-child(4) {
background:linear-gradient(135deg,#ec4899,#db2777)
}
.wls-avatars span {
font-size:12px;font-weight:700;color:#94a3b8;margin-left:10px
}
.wls-row input {
flex:1;border:1.5px solid #e2e8f0;border-radius:10px;padding:12px 13px;font-size:14px;font-family:inherit;color:#0f172a
}
.wls-row input:focus {
outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)
}
.wls-row button {
background:#6366f1;color:#fff;border:none;border-radius:10px;padding:0 22px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s
}
.wls-row button:hover {
background:#4f46e5
}
.wls-pos b {
color:#6366f1;font-size:18px;font-weight:800
}
.wls-refbox p {
font-size:12px;color:#475569;margin-bottom:10px;font-weight:600
}
.wls-ref input {
flex:1;border:1.5px solid #e2e8f0;border-radius:8px;padding:9px 11px;font-size:12px;color:#475569;font-family:inherit;background:#fff
}
.wls-ref button {
background:#0f172a;color:#fff;border:none;border-radius:8px;padding:0 16px;font-size:12.5px;font-weight:700;cursor:pointer;min-width:64px
}
.wls-ref button.copied {
background:#16a34a
}
</style>
</head>
<body>
<div class="wls-card bg-[#fff] rounded-[20px] py-[30px] px-7 w-full max-w-[400px] shadow-[0_24px_60px_rgba(0,0,0,.35)] text-center">
<div class="wls-join" id="wlsJoin">
<span class="inline-block bg-[#eef2ff] text-[#4f46e5] text-[11px] font-extrabold py-[5px] px-3 rounded-[999px] uppercase tracking-[.04em] mb-3.5">🚀 Early access</span>
<h2>Join the waitlist</h2>
<p>Be first to try Nimbus when we launch. We're onboarding in batches.</p>
<div class="wls-avatars flex items-center justify-center gap-0 mt-4 mx-0 mb-[18px]"><i></i><i></i><i></i><i></i><span>2,847 people waiting</span></div>
<form id="wlsForm" novalidate>
<div class="wls-row flex gap-2">
<input type="email" id="wlsEmail" placeholder="[email protected]" autocomplete="email">
<button type="submit">Join</button>
</div>
<p class="text-[#dc2626] text-xs font-semibold text-left mt-2" id="wlsError" hidden></p>
</form>
</div>
<div class="wls-done" id="wlsDone" hidden>
<div class="w-[54px] h-[54px] rounded-full bg-[#22c55e] text-[#fff] text-[26px] font-extrabold flex items-center justify-center mt-0 mx-auto mb-3.5">✓</div>
<h2>You're on the list!</h2>
<p class="wls-pos text-sm text-[#475569] mt-1.5">You're <b id="wlsPos">#1,284</b> in line</p>
<div class="wls-refbox mt-5 bg-[#f8fafc] rounded-xl p-[15px]">
<p>Skip ahead — share your link. Each friend who joins moves you up.</p>
<div class="wls-ref flex gap-[7px]">
<input type="text" id="wlsRef" readonly>
<button type="button" id="wlsCopy">Copy</button>
</div>
</div>
</div>
</div>
<script>
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var join = document.getElementById('wlsJoin');
var done = document.getElementById('wlsDone');
document.getElementById('wlsForm').addEventListener('submit', function (e) {
e.preventDefault();
var input = document.getElementById('wlsEmail');
var error = document.getElementById('wlsError');
var email = input.value.trim();
if (!EMAIL_RE.test(email)) {
error.textContent = 'Please enter a valid email address.';
error.hidden = false;
return;
}
error.hidden = true;
// In production: POST the email to your waitlist API, which returns the real
// queue position and a unique referral code. Here we simulate both.
var position = 1284;
var code = btoa(email).replace(/[^a-z0-9]/gi, '').slice(0, 8).toLowerCase();
document.getElementById('wlsPos').textContent = '#' + position.toLocaleString();
document.getElementById('wlsRef').value = 'nimbus.app/?ref=' + code;
join.hidden = true;
done.hidden = false;
});
document.getElementById('wlsCopy').addEventListener('click', function () {
var ref = document.getElementById('wlsRef');
navigator.clipboard.writeText(ref.value).catch(function () {
ref.select(); try { document.execCommand('copy'); } catch (e) {}
});
var btn = this;
btn.textContent = '✓ Copied';
btn.classList.add('copied');
setTimeout(function () { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
});
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to WaitlistSignup.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.wls-card{background:#fff;border-radius:20px;padding:30px 28px;width:100%;max-width:400px;box-shadow:0 24px 60px rgba(0,0,0,.35);text-align:center}
.wls-badge{display:inline-block;background:#eef2ff;color:#4f46e5;font-size:11px;font-weight:800;padding:5px 12px;border-radius:999px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:14px}
.wls-card h2{font-size:23px;font-weight:800;color:#0f172a;margin-bottom:8px}
.wls-card p{font-size:13px;color:#64748b;line-height:1.55}
.wls-avatars{display:flex;align-items:center;justify-content:center;gap:0;margin:16px 0 18px}
.wls-avatars i{width:26px;height:26px;border-radius:50%;border:2px solid #fff;margin-left:-7px}
.wls-avatars i:first-child{margin-left:0}
.wls-avatars i:nth-child(1){background:linear-gradient(135deg,#6366f1,#8b5cf6)}
.wls-avatars i:nth-child(2){background:linear-gradient(135deg,#22c55e,#10b981)}
.wls-avatars i:nth-child(3){background:linear-gradient(135deg,#f59e0b,#f97316)}
.wls-avatars i:nth-child(4){background:linear-gradient(135deg,#ec4899,#db2777)}
.wls-avatars span{font-size:12px;font-weight:700;color:#94a3b8;margin-left:10px}
.wls-row{display:flex;gap:8px}
.wls-row input{flex:1;border:1.5px solid #e2e8f0;border-radius:10px;padding:12px 13px;font-size:14px;font-family:inherit;color:#0f172a}
.wls-row input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.wls-row button{background:#6366f1;color:#fff;border:none;border-radius:10px;padding:0 22px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s}
.wls-row button:hover{background:#4f46e5}
.wls-error{color:#dc2626;font-size:12px;font-weight:600;text-align:left;margin-top:8px}
.wls-check{width:54px;height:54px;border-radius:50%;background:#22c55e;color:#fff;font-size:26px;font-weight:800;display:flex;align-items:center;justify-content:center;margin:0 auto 14px}
.wls-pos{font-size:14px;color:#475569;margin-top:6px}
.wls-pos b{color:#6366f1;font-size:18px;font-weight:800}
.wls-refbox{margin-top:20px;background:#f8fafc;border-radius:12px;padding:15px}
.wls-refbox p{font-size:12px;color:#475569;margin-bottom:10px;font-weight:600}
.wls-ref{display:flex;gap:7px}
.wls-ref input{flex:1;border:1.5px solid #e2e8f0;border-radius:8px;padding:9px 11px;font-size:12px;color:#475569;font-family:inherit;background:#fff}
.wls-ref button{background:#0f172a;color:#fff;border:none;border-radius:8px;padding:0 16px;font-size:12.5px;font-weight:700;cursor:pointer;min-width:64px}
.wls-ref button.copied{background:#16a34a}
`;
export default function WaitlistSignup() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var join = document.getElementById('wlsJoin');
var done = document.getElementById('wlsDone');
document.getElementById('wlsForm').addEventListener('submit', function (e) {
e.preventDefault();
var input = document.getElementById('wlsEmail');
var error = document.getElementById('wlsError');
var email = input.value.trim();
if (!EMAIL_RE.test(email)) {
error.textContent = 'Please enter a valid email address.';
error.hidden = false;
return;
}
error.hidden = true;
// In production: POST the email to your waitlist API, which returns the real
// queue position and a unique referral code. Here we simulate both.
var position = 1284;
var code = btoa(email).replace(/[^a-z0-9]/gi, '').slice(0, 8).toLowerCase();
document.getElementById('wlsPos').textContent = '#' + position.toLocaleString();
document.getElementById('wlsRef').value = 'nimbus.app/?ref=' + code;
join.hidden = true;
done.hidden = false;
});
document.getElementById('wlsCopy').addEventListener('click', function () {
var ref = document.getElementById('wlsRef');
navigator.clipboard.writeText(ref.value).catch(function () {
ref.select(); try { document.execCommand('copy'); } catch (e) {}
});
var btn = this;
btn.textContent = '✓ Copied';
btn.classList.add('copied');
setTimeout(function () { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="wls-card">
<div className="wls-join" id="wlsJoin">
<span className="wls-badge">🚀 Early access</span>
<h2>Join the waitlist</h2>
<p>Be first to try Nimbus when we launch. We're onboarding in batches.</p>
<div className="wls-avatars"><i></i><i></i><i></i><i></i><span>2,847 people waiting</span></div>
<form id="wlsForm" novalidate>
<div className="wls-row">
<input type="email" id="wlsEmail" placeholder="[email protected]" autocomplete="email" />
<button type="submit">Join</button>
</div>
<p className="wls-error" id="wlsError" hidden></p>
</form>
</div>
<div className="wls-done" id="wlsDone" hidden>
<div className="wls-check">✓</div>
<h2>You're on the list!</h2>
<p className="wls-pos">You're <b id="wlsPos">#1,284</b> in line</p>
<div className="wls-refbox">
<p>Skip ahead — share your link. Each friend who joins moves you up.</p>
<div className="wls-ref">
<input type="text" id="wlsRef" readonly />
<button type="button" id="wlsCopy">Copy</button>
</div>
</div>
</div>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function WaitlistSignup() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var join = document.getElementById('wlsJoin');
var done = document.getElementById('wlsDone');
document.getElementById('wlsForm').addEventListener('submit', function (e) {
e.preventDefault();
var input = document.getElementById('wlsEmail');
var error = document.getElementById('wlsError');
var email = input.value.trim();
if (!EMAIL_RE.test(email)) {
error.textContent = 'Please enter a valid email address.';
error.hidden = false;
return;
}
error.hidden = true;
// In production: POST the email to your waitlist API, which returns the real
// queue position and a unique referral code. Here we simulate both.
var position = 1284;
var code = btoa(email).replace(/[^a-z0-9]/gi, '').slice(0, 8).toLowerCase();
document.getElementById('wlsPos').textContent = '#' + position.toLocaleString();
document.getElementById('wlsRef').value = 'nimbus.app/?ref=' + code;
join.hidden = true;
done.hidden = false;
});
document.getElementById('wlsCopy').addEventListener('click', function () {
var ref = document.getElementById('wlsRef');
navigator.clipboard.writeText(ref.value).catch(function () {
ref.select(); try { document.execCommand('copy'); } catch (e) {}
});
var btn = this;
btn.textContent = '✓ Copied';
btn.classList.add('copied');
setTimeout(function () { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.wls-card h2 {
font-size:23px;font-weight:800;color:#0f172a;margin-bottom:8px
}
.wls-card p {
font-size:13px;color:#64748b;line-height:1.55
}
.wls-avatars i {
width:26px;height:26px;border-radius:50%;border:2px solid #fff;margin-left:-7px
}
.wls-avatars i:first-child {
margin-left:0
}
.wls-avatars i:nth-child(1) {
background:linear-gradient(135deg,#6366f1,#8b5cf6)
}
.wls-avatars i:nth-child(2) {
background:linear-gradient(135deg,#22c55e,#10b981)
}
.wls-avatars i:nth-child(3) {
background:linear-gradient(135deg,#f59e0b,#f97316)
}
.wls-avatars i:nth-child(4) {
background:linear-gradient(135deg,#ec4899,#db2777)
}
.wls-avatars span {
font-size:12px;font-weight:700;color:#94a3b8;margin-left:10px
}
.wls-row input {
flex:1;border:1.5px solid #e2e8f0;border-radius:10px;padding:12px 13px;font-size:14px;font-family:inherit;color:#0f172a
}
.wls-row input:focus {
outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)
}
.wls-row button {
background:#6366f1;color:#fff;border:none;border-radius:10px;padding:0 22px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s
}
.wls-row button:hover {
background:#4f46e5
}
.wls-pos b {
color:#6366f1;font-size:18px;font-weight:800
}
.wls-refbox p {
font-size:12px;color:#475569;margin-bottom:10px;font-weight:600
}
.wls-ref input {
flex:1;border:1.5px solid #e2e8f0;border-radius:8px;padding:9px 11px;font-size:12px;color:#475569;font-family:inherit;background:#fff
}
.wls-ref button {
background:#0f172a;color:#fff;border:none;border-radius:8px;padding:0 16px;font-size:12.5px;font-weight:700;cursor:pointer;min-width:64px
}
.wls-ref button.copied {
background:#16a34a
}
`}</style>
<div className="wls-card bg-[#fff] rounded-[20px] py-[30px] px-7 w-full max-w-[400px] shadow-[0_24px_60px_rgba(0,0,0,.35)] text-center">
<div className="wls-join" id="wlsJoin">
<span className="inline-block bg-[#eef2ff] text-[#4f46e5] text-[11px] font-extrabold py-[5px] px-3 rounded-[999px] uppercase tracking-[.04em] mb-3.5">🚀 Early access</span>
<h2>Join the waitlist</h2>
<p>Be first to try Nimbus when we launch. We're onboarding in batches.</p>
<div className="wls-avatars flex items-center justify-center gap-0 mt-4 mx-0 mb-[18px]"><i></i><i></i><i></i><i></i><span>2,847 people waiting</span></div>
<form id="wlsForm" novalidate>
<div className="wls-row flex gap-2">
<input type="email" id="wlsEmail" placeholder="[email protected]" autocomplete="email" />
<button type="submit">Join</button>
</div>
<p className="text-[#dc2626] text-xs font-semibold text-left mt-2" id="wlsError" hidden></p>
</form>
</div>
<div className="wls-done" id="wlsDone" hidden>
<div className="w-[54px] h-[54px] rounded-full bg-[#22c55e] text-[#fff] text-[26px] font-extrabold flex items-center justify-center mt-0 mx-auto mb-3.5">✓</div>
<h2>You're on the list!</h2>
<p className="wls-pos text-sm text-[#475569] mt-1.5">You're <b id="wlsPos">#1,284</b> in line</p>
<div className="wls-refbox mt-5 bg-[#f8fafc] rounded-xl p-[15px]">
<p>Skip ahead — share your link. Each friend who joins moves you up.</p>
<div className="wls-ref flex gap-[7px]">
<input type="text" id="wlsRef" readonly />
<button type="button" id="wlsCopy">Copy</button>
</div>
</div>
</div>
</div>
</>
);
}<template>
<div class="wls-card">
<div class="wls-join" id="wlsJoin">
<span class="wls-badge">🚀 Early access</span>
<h2>Join the waitlist</h2>
<p>Be first to try Nimbus when we launch. We're onboarding in batches.</p>
<div class="wls-avatars"><i></i><i></i><i></i><i></i><span>2,847 people waiting</span></div>
<form id="wlsForm" novalidate>
<div class="wls-row">
<input type="email" id="wlsEmail" placeholder="[email protected]" autocomplete="email">
<button type="submit">Join</button>
</div>
<p class="wls-error" id="wlsError" hidden></p>
</form>
</div>
<div class="wls-done" id="wlsDone" hidden>
<div class="wls-check">✓</div>
<h2>You're on the list!</h2>
<p class="wls-pos">You're <b id="wlsPos">#1,284</b> in line</p>
<div class="wls-refbox">
<p>Skip ahead — share your link. Each friend who joins moves you up.</p>
<div class="wls-ref">
<input type="text" id="wlsRef" readonly>
<button type="button" id="wlsCopy">Copy</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
let join;
let done;
onMounted(() => {
join = document.getElementById('wlsJoin');
done = document.getElementById('wlsDone');
document.getElementById('wlsForm').addEventListener('submit', function (e) {
e.preventDefault();
var input = document.getElementById('wlsEmail');
var error = document.getElementById('wlsError');
var email = input.value.trim();
if (!EMAIL_RE.test(email)) {
error.textContent = 'Please enter a valid email address.';
error.hidden = false;
return;
}
error.hidden = true;
// In production: POST the email to your waitlist API, which returns the real
// queue position and a unique referral code. Here we simulate both.
var position = 1284;
var code = btoa(email).replace(/[^a-z0-9]/gi, '').slice(0, 8).toLowerCase();
document.getElementById('wlsPos').textContent = '#' + position.toLocaleString();
document.getElementById('wlsRef').value = 'nimbus.app/?ref=' + code;
join.hidden = true;
done.hidden = false;
});
document.getElementById('wlsCopy').addEventListener('click', function () {
var ref = document.getElementById('wlsRef');
navigator.clipboard.writeText(ref.value).catch(function () {
ref.select(); try { document.execCommand('copy'); } catch (e) {}
});
var btn = this;
btn.textContent = '✓ Copied';
btn.classList.add('copied');
setTimeout(function () { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
});
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.wls-card{background:#fff;border-radius:20px;padding:30px 28px;width:100%;max-width:400px;box-shadow:0 24px 60px rgba(0,0,0,.35);text-align:center}
.wls-badge{display:inline-block;background:#eef2ff;color:#4f46e5;font-size:11px;font-weight:800;padding:5px 12px;border-radius:999px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:14px}
.wls-card h2{font-size:23px;font-weight:800;color:#0f172a;margin-bottom:8px}
.wls-card p{font-size:13px;color:#64748b;line-height:1.55}
.wls-avatars{display:flex;align-items:center;justify-content:center;gap:0;margin:16px 0 18px}
.wls-avatars i{width:26px;height:26px;border-radius:50%;border:2px solid #fff;margin-left:-7px}
.wls-avatars i:first-child{margin-left:0}
.wls-avatars i:nth-child(1){background:linear-gradient(135deg,#6366f1,#8b5cf6)}
.wls-avatars i:nth-child(2){background:linear-gradient(135deg,#22c55e,#10b981)}
.wls-avatars i:nth-child(3){background:linear-gradient(135deg,#f59e0b,#f97316)}
.wls-avatars i:nth-child(4){background:linear-gradient(135deg,#ec4899,#db2777)}
.wls-avatars span{font-size:12px;font-weight:700;color:#94a3b8;margin-left:10px}
.wls-row{display:flex;gap:8px}
.wls-row input{flex:1;border:1.5px solid #e2e8f0;border-radius:10px;padding:12px 13px;font-size:14px;font-family:inherit;color:#0f172a}
.wls-row input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.wls-row button{background:#6366f1;color:#fff;border:none;border-radius:10px;padding:0 22px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s}
.wls-row button:hover{background:#4f46e5}
.wls-error{color:#dc2626;font-size:12px;font-weight:600;text-align:left;margin-top:8px}
.wls-check{width:54px;height:54px;border-radius:50%;background:#22c55e;color:#fff;font-size:26px;font-weight:800;display:flex;align-items:center;justify-content:center;margin:0 auto 14px}
.wls-pos{font-size:14px;color:#475569;margin-top:6px}
.wls-pos b{color:#6366f1;font-size:18px;font-weight:800}
.wls-refbox{margin-top:20px;background:#f8fafc;border-radius:12px;padding:15px}
.wls-refbox p{font-size:12px;color:#475569;margin-bottom:10px;font-weight:600}
.wls-ref{display:flex;gap:7px}
.wls-ref input{flex:1;border:1.5px solid #e2e8f0;border-radius:8px;padding:9px 11px;font-size:12px;color:#475569;font-family:inherit;background:#fff}
.wls-ref button{background:#0f172a;color:#fff;border:none;border-radius:8px;padding:0 16px;font-size:12.5px;font-weight:700;cursor:pointer;min-width:64px}
.wls-ref button.copied{background:#16a34a}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-waitlist-signup',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="wls-card">
<div class="wls-join" id="wlsJoin">
<span class="wls-badge">🚀 Early access</span>
<h2>Join the waitlist</h2>
<p>Be first to try Nimbus when we launch. We're onboarding in batches.</p>
<div class="wls-avatars"><i></i><i></i><i></i><i></i><span>2,847 people waiting</span></div>
<form id="wlsForm" novalidate>
<div class="wls-row">
<input type="email" id="wlsEmail" placeholder="[email protected]" autocomplete="email">
<button type="submit">Join</button>
</div>
<p class="wls-error" id="wlsError" hidden></p>
</form>
</div>
<div class="wls-done" id="wlsDone" hidden>
<div class="wls-check">✓</div>
<h2>You're on the list!</h2>
<p class="wls-pos">You're <b id="wlsPos">#1,284</b> in line</p>
<div class="wls-refbox">
<p>Skip ahead — share your link. Each friend who joins moves you up.</p>
<div class="wls-ref">
<input type="text" id="wlsRef" readonly>
<button type="button" id="wlsCopy">Copy</button>
</div>
</div>
</div>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.wls-card{background:#fff;border-radius:20px;padding:30px 28px;width:100%;max-width:400px;box-shadow:0 24px 60px rgba(0,0,0,.35);text-align:center}
.wls-badge{display:inline-block;background:#eef2ff;color:#4f46e5;font-size:11px;font-weight:800;padding:5px 12px;border-radius:999px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:14px}
.wls-card h2{font-size:23px;font-weight:800;color:#0f172a;margin-bottom:8px}
.wls-card p{font-size:13px;color:#64748b;line-height:1.55}
.wls-avatars{display:flex;align-items:center;justify-content:center;gap:0;margin:16px 0 18px}
.wls-avatars i{width:26px;height:26px;border-radius:50%;border:2px solid #fff;margin-left:-7px}
.wls-avatars i:first-child{margin-left:0}
.wls-avatars i:nth-child(1){background:linear-gradient(135deg,#6366f1,#8b5cf6)}
.wls-avatars i:nth-child(2){background:linear-gradient(135deg,#22c55e,#10b981)}
.wls-avatars i:nth-child(3){background:linear-gradient(135deg,#f59e0b,#f97316)}
.wls-avatars i:nth-child(4){background:linear-gradient(135deg,#ec4899,#db2777)}
.wls-avatars span{font-size:12px;font-weight:700;color:#94a3b8;margin-left:10px}
.wls-row{display:flex;gap:8px}
.wls-row input{flex:1;border:1.5px solid #e2e8f0;border-radius:10px;padding:12px 13px;font-size:14px;font-family:inherit;color:#0f172a}
.wls-row input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.wls-row button{background:#6366f1;color:#fff;border:none;border-radius:10px;padding:0 22px;font-size:14px;font-weight:700;cursor:pointer;transition:background .15s}
.wls-row button:hover{background:#4f46e5}
.wls-error{color:#dc2626;font-size:12px;font-weight:600;text-align:left;margin-top:8px}
.wls-check{width:54px;height:54px;border-radius:50%;background:#22c55e;color:#fff;font-size:26px;font-weight:800;display:flex;align-items:center;justify-content:center;margin:0 auto 14px}
.wls-pos{font-size:14px;color:#475569;margin-top:6px}
.wls-pos b{color:#6366f1;font-size:18px;font-weight:800}
.wls-refbox{margin-top:20px;background:#f8fafc;border-radius:12px;padding:15px}
.wls-refbox p{font-size:12px;color:#475569;margin-bottom:10px;font-weight:600}
.wls-ref{display:flex;gap:7px}
.wls-ref input{flex:1;border:1.5px solid #e2e8f0;border-radius:8px;padding:9px 11px;font-size:12px;color:#475569;font-family:inherit;background:#fff}
.wls-ref button{background:#0f172a;color:#fff;border:none;border-radius:8px;padding:0 16px;font-size:12.5px;font-weight:700;cursor:pointer;min-width:64px}
.wls-ref button.copied{background:#16a34a}
`]
})
export class WaitlistSignupComponent implements AfterViewInit {
ngAfterViewInit(): void {
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var join = document.getElementById('wlsJoin');
var done = document.getElementById('wlsDone');
document.getElementById('wlsForm').addEventListener('submit', function (e) {
e.preventDefault();
var input = document.getElementById('wlsEmail');
var error = document.getElementById('wlsError');
var email = input.value.trim();
if (!EMAIL_RE.test(email)) {
error.textContent = 'Please enter a valid email address.';
error.hidden = false;
return;
}
error.hidden = true;
// In production: POST the email to your waitlist API, which returns the real
// queue position and a unique referral code. Here we simulate both.
var position = 1284;
var code = btoa(email).replace(/[^a-z0-9]/gi, '').slice(0, 8).toLowerCase();
document.getElementById('wlsPos').textContent = '#' + position.toLocaleString();
document.getElementById('wlsRef').value = 'nimbus.app/?ref=' + code;
join.hidden = true;
done.hidden = false;
});
document.getElementById('wlsCopy').addEventListener('click', function () {
var ref = document.getElementById('wlsRef');
navigator.clipboard.writeText(ref.value).catch(function () {
ref.select(); try { document.execCommand('copy'); } catch (e) {}
});
var btn = this;
btn.textContent = '✓ Copied';
btn.classList.add('copied');
setTimeout(function () { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
});
}
}