Forms & Lead Capture

Free Waitlist Signup Widget

Capture emails for your launch with a spot counter and one-line install.

A compact waitlist form that captures an email address (and an optional name) and POSTs it to your own endpoint. A spot counter builds urgency by showing how many people have already joined. Paste one script tag where you want the form to appear and point it at any URL that accepts JSON.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/waitlist-signup.js" data-hl-action="https://hayneslab.dev/api/widget-demo/waitlist" data-hl-count="248" data-hl-button-text="Reserve my spot" async></script>
<!-- Free Waitlist Signup widget by haynes lab — https://hayneslab.dev/tools/widgets/waitlist-signup -->
Inline (self-contained)html
<!-- Free Waitlist Signup widget by haynes lab — https://hayneslab.dev/tools/widgets/waitlist-signup -->
<script data-hl-action="https://hayneslab.dev/api/widget-demo/waitlist" data-hl-count="248" data-hl-button-text="Reserve my spot">
/*!
 * haynes lab — Waitlist Signup widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/waitlist-signup
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-waitlist-signup';
  var STYLE_ID = 'hlw-waitlist-signup-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/waitlist-signup.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 440px; margin: 0 auto; padding: 32px 28px 34px;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.5; color: #1f2937; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; text-align: center; }' +
      '.' + PREFIX + '__title { margin: 0 0 6px; font-size: 22px; font-weight: 700; color: #111827; }' +
      '.' + PREFIX + '__count { margin: 0 0 18px; font-size: 14px; color: #6b7280; }' +
      '.' + PREFIX + '__count strong { color: var(--hlw-accent); font-weight: 700; }' +
      '.' + PREFIX + '__form { display: flex; flex-direction: column; gap: 10px; }' +
      '.' + PREFIX + '__input { width: 100%; padding: 11px 14px; font: inherit; font-size: 15px;' +
      ' color: #1f2937; background: #ffffff; border: 1px solid #d1d5db; border-radius: 8px; }' +
      '.' + PREFIX + '__input:focus { outline: 2px solid var(--hlw-accent); outline-offset: 1px;' +
      ' border-color: var(--hlw-accent); }' +
      '.' + PREFIX + '__input[aria-invalid="true"] { border-color: #dc2626; }' +
      '.' + PREFIX + '__button { width: 100%; padding: 12px 16px; font: inherit; font-size: 15px;' +
      ' font-weight: 600; color: #ffffff; background: var(--hlw-accent); border: 0;' +
      ' border-radius: 8px; cursor: pointer; transition: opacity 0.15s ease; }' +
      '.' + PREFIX + '__button:hover { opacity: 0.9; }' +
      '.' + PREFIX + '__button:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__button:disabled { opacity: 0.6; cursor: default; }' +
      '.' + PREFIX + '__message { margin: 12px 0 0; font-size: 14px; }' +
      '.' + PREFIX + '__message--error { color: #dc2626; }' +
      '.' + PREFIX + '__success { padding: 24px 0 8px; }' +
      '.' + PREFIX + '__success-title { margin: 0 0 6px; font-size: 18px; font-weight: 700; color: #111827; }' +
      '.' + PREFIX + '__success-text { margin: 0; font-size: 14px; color: #6b7280; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 6px; font-size: 11px;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }' +
      '@media (max-width: 560px) { .' + PREFIX + ' { padding: 24px 20px 32px; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function isValidEmail(value) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
  }

  function build(script) {
    var action = script.getAttribute('data-hl-action');
    if (!action) {
      console.warn('[haynes lab waitlist-signup] Missing required data-hl-action attribute (form POST URL). Nothing rendered.');
      return;
    }

    var title = script.getAttribute('data-hl-title') || 'Join the waitlist';
    var buttonText = script.getAttribute('data-hl-button-text') || 'Join waitlist';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';
    var bg = script.getAttribute('data-hl-bg') || '#ffffff';
    var count = parseInt(script.getAttribute('data-hl-count'), 10);

    injectStyles();

    var root = el('div', PREFIX);
    root.style.setProperty('--hlw-accent', accent);
    root.style.background = bg;

    root.appendChild(el('h3', PREFIX + '__title', title));

    if (!isNaN(count) && count > 0) {
      var countEl = el('p', PREFIX + '__count');
      countEl.appendChild(document.createTextNode('Join '));
      var strong = el('strong', null, String(count));
      countEl.appendChild(strong);
      countEl.appendChild(document.createTextNode(' others already on the list'));
      root.appendChild(countEl);
    }

    var form = el('form', PREFIX + '__form');
    form.setAttribute('novalidate', 'novalidate');

    var nameInput = el('input', PREFIX + '__input');
    nameInput.type = 'text';
    nameInput.name = 'name';
    nameInput.placeholder = 'Name (optional)';
    nameInput.setAttribute('aria-label', 'Name (optional)');
    nameInput.autocomplete = 'name';

    var emailInput = el('input', PREFIX + '__input');
    emailInput.type = 'email';
    emailInput.name = 'email';
    emailInput.placeholder = 'Email address';
    emailInput.setAttribute('aria-label', 'Email address');
    emailInput.required = true;
    emailInput.autocomplete = 'email';

    var button = el('button', PREFIX + '__button', buttonText);
    button.type = 'submit';

    var message = el('p', PREFIX + '__message');
    message.setAttribute('role', 'alert');
    message.hidden = true;

    form.appendChild(nameInput);
    form.appendChild(emailInput);
    form.appendChild(button);
    root.appendChild(form);
    root.appendChild(message);

    function showError(text) {
      message.textContent = text;
      message.className = PREFIX + '__message ' + PREFIX + '__message--error';
      message.hidden = false;
    }

    function showSuccess() {
      form.remove();
      message.remove();
      var success = el('div', PREFIX + '__success');
      success.setAttribute('role', 'status');
      success.appendChild(el('p', PREFIX + '__success-title', 'You’re on the list!'));
      success.appendChild(el('p', PREFIX + '__success-text', 'We’ll email you as soon as it’s your turn.'));
      root.insertBefore(success, credit);
    }

    form.addEventListener('submit', function (e) {
      e.preventDefault();
      var email = emailInput.value.trim();
      if (!isValidEmail(email)) {
        emailInput.setAttribute('aria-invalid', 'true');
        emailInput.focus();
        showError('Please enter a valid email address.');
        return;
      }
      emailInput.removeAttribute('aria-invalid');
      message.hidden = true;

      var payload = { email: email };
      var name = nameInput.value.trim();
      if (name) payload.name = name;

      button.disabled = true;
      button.textContent = 'Joining…';

      fetch(action, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
        body: JSON.stringify(payload)
      }).then(function (res) {
        if (!res.ok) throw new Error('Request failed: ' + res.status);
        showSuccess();
      }).catch(function (err) {
        button.disabled = false;
        button.textContent = buttonText;
        showError('Something went wrong. Please try again.');
        console.warn('[haynes lab waitlist-signup] Signup request failed.', err);
      });
    });

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/waitlist-signup';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab waitlist-signup] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-action(required)The endpoint the signup is POSTed to as JSON ({email, name}).
data-hl-count(none)Number shown in the "Join N others" line; omitted when not set.
data-hl-title"Join the waitlist"The heading displayed above the form.
data-hl-button-text"Join waitlist"The label on the submit button.
data-hl-accent"#f97316"Color of the button, counter number, and focus outlines.
data-hl-bg"#ffffff"Background color of the widget card.

Frequently asked questions

How do I install the waitlist signup?

Paste the script tag where you want the form to appear and set data-hl-action to your endpoint. The widget renders inline right after the tag and POSTs {email, name} as JSON on submit.

Can I change the text, colors, and spot counter?

Yes — use data-hl-title and data-hl-button-text for the copy, data-hl-count for the "Join N others" counter, and data-hl-accent / data-hl-bg to match your brand colors.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies, so it runs anywhere you can paste a script tag, including WordPress, Shopify, Squarespace, Webflow, and plain HTML sites.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch