HTML / Vanilla JS Integration
Step-by-step guide to adding a MakeEmWait waitlist to any website with plain HTML and JavaScript.
This guide covers every way to add a MakeEmWait waitlist to a plain HTML page — from a single script tag to a fully custom JavaScript integration.
Quick Start — Embed Widget
The fastest way to add a waitlist. Drop one script tag wherever you want the form to appear:
<script
src="https://makeemwait.com/assets/js/embed.js"
data-waitlist-id="YOUR_WAITLIST_ID"
data-theme="dark">
</script>
That’s it. The widget renders automatically with built-in styling, form validation, referral tracking, UTM passthrough, returning-visitor detection, and success states. It works on any origin thanks to wildcard CORS on all public API routes.
Configuration Options
Customize the widget with data- attributes on the script tag:
| Attribute | Required | Default | Description |
|---|---|---|---|
data-waitlist-id |
Yes | — | Your waitlist ID from the dashboard (UUID, e.g., a1b2c3d4-e5f6-7890-abcd-ef1234567890) |
data-theme |
No | "dark" |
"dark", "light", "glass", or "auto" |
data-api-url |
No | "https://makeemwait.com/api/v1" |
Override the API base URL |
data-compact |
No | "false" |
"true" for inline email + button layout |
Your waitlist ID is available on the Dashboard. Click Embed on any waitlist card to copy the full embed snippet.
Theme Examples
Dark (default) — dark background with light text, suited for dark-themed sites:
<script
src="https://makeemwait.com/assets/js/embed.js"
data-waitlist-id="YOUR_WAITLIST_ID"
data-theme="dark">
</script>
Light — white background with dark text, suited for light-themed sites:
<script
src="https://makeemwait.com/assets/js/embed.js"
data-waitlist-id="YOUR_WAITLIST_ID"
data-theme="light">
</script>
Glass — semi-transparent dark background with a 16px blur effect and subtle indigo glow. Looks best over images or gradient backgrounds:
<script
src="https://makeemwait.com/assets/js/embed.js"
data-waitlist-id="YOUR_WAITLIST_ID"
data-theme="glass">
</script>
Auto — follows the visitor’s system color scheme via the prefers-color-scheme media query. Shows the light theme in light mode and the dark theme in dark mode:
<script
src="https://makeemwait.com/assets/js/embed.js"
data-waitlist-id="YOUR_WAITLIST_ID"
data-theme="auto">
</script>
You can combine any theme with compact mode:
<script
src="https://makeemwait.com/assets/js/embed.js"
data-waitlist-id="YOUR_WAITLIST_ID"
data-theme="auto"
data-compact="true">
</script>
Compact mode places the email input and submit button on the same row. It works well in headers, sidebars, and inline call-to-action sections.
Custom Styling with embed-plain.js
For full design control, use the unstyled variant. It renders plain HTML with mew-* CSS classes and no Shadow DOM, so your page’s styles apply directly:
<script
src="https://makeemwait.com/assets/js/embed-plain.js"
data-waitlist-id="YOUR_WAITLIST_ID">
</script>
embed-plain.js supports only two attributes:
| Attribute | Required | Default | Description |
|---|---|---|---|
data-waitlist-id |
Yes | — | Your waitlist ID |
data-api-url |
No | "https://makeemwait.com/api/v1" |
Override API URL |
There is no data-theme or data-compact attribute. You write all the CSS yourself.
CSS Classes Reference
These are the class names rendered by embed-plain.js. All element IDs are suffixed with the waitlist ID (e.g., mew-email-abc123) to avoid conflicts when multiple forms exist on the same page.
| Class | Element | Description |
|---|---|---|
.mew-embed |
<div> |
Outer container wrapping the entire widget |
.mew-form-section |
<div> |
Wrapper around the form (hidden on success) |
.mew-form |
<form> |
The form element |
.mew-field |
<div> |
Wrapper around each field (label + input pair) |
.mew-label |
<label> |
Field labels |
.mew-input |
<input> |
Text, email, and tel input fields |
.mew-select |
<select> |
Dropdown select fields (custom questions) |
.mew-name-row |
<div> |
Wrapper around first name and last name fields |
.mew-checkbox-field |
<div> |
Wrapper around checkbox-type custom questions |
.mew-checkbox |
<input> |
Checkbox inputs |
.mew-submit |
<button> |
The submit button |
.mew-error |
<div> |
Error message container |
.mew-success |
<div> |
Success state wrapper |
.mew-branding |
<div> |
“Powered by MakeEmWait” link |
Complete Styling Example
Here is a full dark-theme CSS example for embed-plain.js:
<style>
.mew-embed {
max-width: 400px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #e4e5e9;
}
.mew-field {
margin-bottom: 0.75rem;
}
.mew-label {
display: block;
font-size: 0.8rem;
font-weight: 500;
color: #8b8d97;
margin-bottom: 0.25rem;
}
.mew-input,
.mew-select {
width: 100%;
padding: 0.625rem 0.75rem;
background: #242736;
color: #e4e5e9;
border: 1px solid #2d3044;
border-radius: 6px;
font-size: 0.9rem;
box-sizing: border-box;
outline: none;
}
.mew-input:focus,
.mew-select:focus {
border-color: #6366f1;
}
.mew-input::placeholder {
color: #8b8d97;
}
.mew-name-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.mew-checkbox-field {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.mew-submit {
width: 100%;
padding: 0.625rem;
background: #6366f1;
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
margin-top: 0.25rem;
}
.mew-submit:hover {
background: #818cf8;
}
.mew-submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.mew-error {
color: #ef4444;
font-size: 0.8rem;
margin-top: 0.5rem;
}
.mew-success {
text-align: center;
padding: 1rem 0;
}
.mew-success h3 {
color: #22c55e;
margin-bottom: 0.5rem;
}
.mew-branding {
text-align: center;
font-size: 0.7rem;
margin-top: 0.75rem;
}
.mew-branding a {
color: #8b8d97;
text-decoration: none;
}
.mew-branding a:hover {
color: #6366f1;
}
</style>
<script
src="https://makeemwait.com/assets/js/embed-plain.js"
data-waitlist-id="YOUR_WAITLIST_ID">
</script>
Using the API Directly
For maximum control over the form markup, layout, and behavior, call the MakeEmWait API with plain JavaScript. This approach gives you complete ownership of the HTML and lets you integrate the waitlist into any existing form or UI flow.
Basic Form
<form id="waitlist-form">
<input type="email" id="email" placeholder="you@example.com" required>
<input type="text" id="first_name" placeholder="First name">
<input type="text" id="last_name" placeholder="Last name">
<button type="submit">Join the Waitlist</button>
<p id="message"></p>
</form>
<script>
const WAITLIST_ID = 'YOUR_WAITLIST_ID';
const API_URL = 'https://makeemwait.com/api/v1';
document.getElementById('waitlist-form').addEventListener('submit', async (e) => {
e.preventDefault();
const messageEl = document.getElementById('message');
const button = e.target.querySelector('button');
button.disabled = true;
button.textContent = 'Joining...';
try {
const response = await fetch(`${API_URL}/public/waitlists/${WAITLIST_ID}/signups`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: document.getElementById('email').value,
first_name: document.getElementById('first_name').value,
last_name: document.getElementById('last_name').value,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
})
});
const data = await response.json();
if (!response.ok) {
messageEl.textContent = data.error;
button.disabled = false;
button.textContent = 'Join the Waitlist';
return;
}
messageEl.textContent = `You're #${data.position} on the waitlist!`;
button.textContent = 'Joined';
} catch (err) {
messageEl.textContent = 'Something went wrong. Please try again.';
button.disabled = false;
button.textContent = 'Join the Waitlist';
}
});
</script>
The API returns a JSON response with the signup’s position, referral_token, referral_count, total_signups, and created_at. If the waitlist has a redirect URL configured (Advanced+), the response also includes redirect_url.
Adding Referral Tracking
MakeEmWait’s viral loop works through referral tokens. When someone signs up, they get a unique referral_token. They share a link with ?ref=TOKEN appended. When the next person signs up through that link, the referrer moves up in line.
To wire this up, read the ref query parameter from the URL and include it in the signup request:
<script>
const WAITLIST_ID = 'YOUR_WAITLIST_ID';
const API_URL = 'https://makeemwait.com/api/v1';
// Read the referral token from the URL (?ref=abc123)
const urlParams = new URLSearchParams(window.location.search);
const referredByToken = urlParams.get('ref');
document.getElementById('waitlist-form').addEventListener('submit', async (e) => {
e.preventDefault();
const messageEl = document.getElementById('message');
const payload = {
email: document.getElementById('email').value,
first_name: document.getElementById('first_name').value,
last_name: document.getElementById('last_name').value,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
};
// Include referral token if the user came from a referral link
if (referredByToken) {
payload.referred_by_token = referredByToken;
}
try {
const response = await fetch(`${API_URL}/public/waitlists/${WAITLIST_ID}/signups`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
messageEl.textContent = data.error;
return;
}
// Build the new user's referral link so they can share it
const referralLink = `${window.location.origin}${window.location.pathname}?ref=${data.referral_token}`;
messageEl.innerHTML = `You're #${data.position} on the waitlist!<br>
Share your link to move up: <a href="${referralLink}">${referralLink}</a>`;
} catch (err) {
messageEl.textContent = 'Something went wrong. Please try again.';
}
});
</script>
Adding UTM Tracking
If you’re driving traffic from ads or email campaigns, pass UTM parameters through to MakeEmWait so you can see which sources drive the most signups. Read them from the page URL and include them in the POST body:
<script>
const WAITLIST_ID = 'YOUR_WAITLIST_ID';
const API_URL = 'https://makeemwait.com/api/v1';
document.getElementById('waitlist-form').addEventListener('submit', async (e) => {
e.preventDefault();
const messageEl = document.getElementById('message');
const payload = {
email: document.getElementById('email').value,
first_name: document.getElementById('first_name').value,
last_name: document.getElementById('last_name').value,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
};
// Read UTM parameters from the page URL
const urlParams = new URLSearchParams(window.location.search);
const utmFields = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
for (const field of utmFields) {
const value = urlParams.get(field);
if (value) {
payload[field] = value;
}
}
// Include referral token if present
const ref = urlParams.get('ref');
if (ref) {
payload.referred_by_token = ref;
}
try {
const response = await fetch(`${API_URL}/public/waitlists/${WAITLIST_ID}/signups`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
messageEl.textContent = data.error;
return;
}
messageEl.textContent = `You're #${data.position} on the waitlist!`;
} catch (err) {
messageEl.textContent = 'Something went wrong. Please try again.';
}
});
</script>
When someone visits your page at https://yoursite.com/beta?utm_source=twitter&utm_campaign=launch, those UTM values are included with their signup and visible in your dashboard analytics (Pro plan).
Full Example with Everything
Here is a complete, production-ready example that combines referral tracking, UTM passthrough, honeypot bot protection, custom answers, and error handling:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Join the Waitlist</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f1117;
color: #e4e5e9;
display: flex;
justify-content: center;
padding: 4rem 1rem;
}
.waitlist-card {
max-width: 400px;
width: 100%;
background: #1a1d27;
border: 1px solid #2d3044;
border-radius: 8px;
padding: 2rem;
}
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
p.subtitle { color: #8b8d97; margin-bottom: 1.5rem; }
label { display: block; font-size: 0.8rem; color: #8b8d97; margin-bottom: 0.25rem; }
input {
width: 100%; padding: 0.625rem 0.75rem;
background: #242736; color: #e4e5e9;
border: 1px solid #2d3044; border-radius: 6px;
font-size: 0.9rem; margin-bottom: 0.75rem;
box-sizing: border-box; outline: none;
}
input:focus { border-color: #6366f1; }
input::placeholder { color: #8b8d97; }
.name-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
button {
width: 100%; padding: 0.625rem;
background: #6366f1; color: #fff; border: none;
border-radius: 6px; font-size: 0.9rem; font-weight: 600;
cursor: pointer;
}
button:hover { background: #818cf8; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
.error { color: #ef4444; font-size: 0.85rem; margin-top: 0.5rem; }
.success { color: #22c55e; margin-top: 1rem; }
.success a { color: #6366f1; }
.hidden { display: none; }
/* Honeypot field — invisible to real users */
.hp-field { position: absolute; left: -9999px; }
</style>
</head>
<body>
<div class="waitlist-card">
<h1>Get Early Access</h1>
<p class="subtitle">Be the first to try our product.</p>
<form id="waitlist-form">
<label for="email">Email *</label>
<input type="email" id="email" placeholder="you@example.com" required>
<div class="name-row">
<div>
<label for="first_name">First name</label>
<input type="text" id="first_name" placeholder="Jane">
</div>
<div>
<label for="last_name">Last name</label>
<input type="text" id="last_name" placeholder="Doe">
</div>
</div>
<!-- Honeypot: bots fill this, humans don't see it -->
<div class="hp-field">
<input type="text" id="website" tabindex="-1" autocomplete="off">
</div>
<button type="submit" id="submit-btn">Join the Waitlist</button>
</form>
<div id="error-msg" class="error hidden"></div>
<div id="success-msg" class="success hidden"></div>
</div>
<script>
const WAITLIST_ID = 'YOUR_WAITLIST_ID';
const API_URL = 'https://makeemwait.com/api/v1';
const urlParams = new URLSearchParams(window.location.search);
document.getElementById('waitlist-form').addEventListener('submit', async (e) => {
e.preventDefault();
const errorEl = document.getElementById('error-msg');
const successEl = document.getElementById('success-msg');
const button = document.getElementById('submit-btn');
errorEl.classList.add('hidden');
successEl.classList.add('hidden');
button.disabled = true;
button.textContent = 'Joining...';
// Build the request payload
const payload = {
email: document.getElementById('email').value.trim(),
first_name: document.getElementById('first_name').value.trim(),
last_name: document.getElementById('last_name').value.trim(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
};
// Honeypot — bots that fill the hidden field get a fake success
const honeypot = document.getElementById('website').value;
if (honeypot) {
payload.website = honeypot;
}
// Referral tracking
const ref = urlParams.get('ref');
if (ref) {
payload.referred_by_token = ref;
}
// UTM tracking
for (const field of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']) {
const value = urlParams.get(field);
if (value) {
payload[field] = value;
}
}
try {
const response = await fetch(`${API_URL}/public/waitlists/${WAITLIST_ID}/signups`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
errorEl.textContent = data.error || 'Something went wrong.';
errorEl.classList.remove('hidden');
button.disabled = false;
button.textContent = 'Join the Waitlist';
return;
}
// Hide the form and show the success message
document.getElementById('waitlist-form').classList.add('hidden');
const referralLink = `${window.location.origin}${window.location.pathname}?ref=${data.referral_token}`;
successEl.innerHTML = `
<h3>You're #${data.position} on the waitlist!</h3>
<p>Share your link to move up in line:</p>
<p><a href="${referralLink}">${referralLink}</a></p>
`;
successEl.classList.remove('hidden');
} catch (err) {
errorEl.textContent = 'Something went wrong. Please try again.';
errorEl.classList.remove('hidden');
button.disabled = false;
button.textContent = 'Join the Waitlist';
}
});
</script>
</body>
</html>
Troubleshooting
Widget not appearing
Check that data-waitlist-id is set correctly and that the waitlist exists. Open the browser console — the widget logs MakeEmWait: data-waitlist-id is required if the attribute is missing, and a warning if the config fetch fails.
CORS errors
All public endpoints (/public/*) return Access-Control-Allow-Origin: *, so cross-origin requests work from any domain. If you see CORS errors, make sure you’re hitting a /public/ route. Authenticated endpoints (those requiring a JWT or API key) do not allow wildcard origins.
Form submits but no success state
Open the browser console and check for API errors in the response. Common causes: the waitlist is closed (status: "closed"), a required field is missing (returns 400), or a server error (returns 500). Note that duplicate signups return 200 with the existing signup data, so they won’t cause errors.
Styles conflict with embed.js
The embed widget uses Shadow DOM, so parent page styles should not leak in. If they do, check for !important rules targeting :host or global styles on * selectors. Switching to embed-plain.js gives you full control over styling instead.
Duplicate signups return 200
If an email is already on the waitlist, the API returns 200 OK with the existing signup data (position, referral token, etc.). This makes form submissions idempotent — refreshing the page or submitting again won’t create duplicates or show errors.
Related
- Embed Widget — full reference for both embed.js and embed-plain.js
- API Reference — complete REST API documentation for all endpoints
- React Integration — using MakeEmWait in React applications
- Next.js Integration — server-side and client-side patterns for Next.js