Features Pricing
Start My Free Trial

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>
*
embed-plain.js is ideal when you need the form to match your site's existing design system, or when you're building inside a framework like Tailwind CSS where you want full control over every class.

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.