Forms & Lead Capture

Free SMS Opt-in Form Widget

Capture phone numbers with a compliant consent checkbox.

A compact sign-up card that collects phone numbers for your SMS list, with a required consent checkbox so subscribers explicitly opt in. Submissions POST straight to your own endpoint — a form backend, Zapier, or your SMS platform. One script tag adds it to any page.

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/sms-optin-form.js" data-hl-action="https://formspree.io/f/your-form-id" data-hl-title="Get 10% off your first order — by text" data-hl-consent-text="I agree to receive promotional text messages from Acme Co. at the number provided. Msg & data rates may apply. Reply STOP to opt out." data-hl-button-text="Text me the code" async></script>
<!-- Free SMS Opt-in Form widget by haynes lab — https://hayneslab.dev/tools/widgets/sms-optin-form -->
Inline (self-contained)html
<!-- Free SMS Opt-in Form widget by haynes lab — https://hayneslab.dev/tools/widgets/sms-optin-form -->
<script data-hl-action="https://formspree.io/f/your-form-id" data-hl-title="Get 10% off your first order — by text" data-hl-consent-text="I agree to receive promotional text messages from Acme Co. at the number provided. Msg & data rates may apply. Reply STOP to opt out." data-hl-button-text="Text me the code">
/*!
 * haynes lab — SMS Opt-in Form widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/sms-optin-form
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-sms-optin-form';
  var STYLE_ID = 'hlw-sms-optin-form-styles';

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

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 420px; margin: 0 auto; padding: 28px 24px 30px;' +
      ' 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; }' +
      '.' + PREFIX + '__title { margin: 0 0 6px; font-size: 20px; font-weight: 600; color: #111827; }' +
      '.' + PREFIX + '__form { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }' +
      '.' + PREFIX + '__label { display: block; font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 4px; }' +
      '.' + PREFIX + '__input { width: 100%; padding: 10px 12px; font-size: 15px; font-family: inherit;' +
      ' color: #111827; 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 + '__consent { display: flex; align-items: flex-start; gap: 8px; font-size: 13px;' +
      ' line-height: 1.45; color: #4b5563; cursor: pointer; }' +
      '.' + PREFIX + '__consent input { flex: 0 0 auto; width: 16px; height: 16px; margin: 1px 0 0;' +
      ' accent-color: var(--hlw-accent); cursor: pointer; }' +
      '.' + PREFIX + '__button { width: 100%; padding: 11px 16px; font-size: 15px; font-weight: 600;' +
      ' font-family: inherit; color: #ffffff; background: var(--hlw-accent); border: 0;' +
      ' border-radius: 8px; cursor: pointer; transition: filter 0.15s ease; }' +
      '.' + PREFIX + '__button:hover:not(:disabled) { filter: brightness(0.92); }' +
      '.' + PREFIX + '__button:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__button:disabled { opacity: 0.55; cursor: not-allowed; }' +
      '.' + PREFIX + '__error { font-size: 13px; color: #b91c1c; margin: 0; }' +
      '.' + PREFIX + '__success { text-align: center; padding: 8px 0 2px; }' +
      '.' + PREFIX + '__success-icon { display: inline-flex; align-items: center; justify-content: center;' +
      ' width: 40px; height: 40px; margin-bottom: 10px; border-radius: 50%; color: #ffffff;' +
      ' background: var(--hlw-accent); font-size: 20px; line-height: 1; }' +
      '.' + PREFIX + '__success-text { margin: 0; font-size: 15px; color: #374151; }' +
      '.' + PREFIX + '__credit { display: inline-flex; align-items: center; gap: 4px; margin-top: 16px;' +
      ' font-size: 11px; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }';
    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 build(script) {
    var action = script.getAttribute('data-hl-action');
    if (!action) {
      console.warn('[haynes lab sms-optin-form] Missing required data-hl-action attribute (endpoint URL to POST the phone number to). Nothing rendered.');
      return;
    }

    var title = script.getAttribute('data-hl-title') || 'Get updates by text';
    var consentText = script.getAttribute('data-hl-consent-text') ||
      'I agree to receive text messages at the number provided. Message and data rates may apply. Reply STOP to opt out.';
    var buttonText = script.getAttribute('data-hl-button-text') || 'Sign up';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    injectStyles();

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

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

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

    var field = el('div');
    var inputId = PREFIX + '__phone';
    var label = el('label', PREFIX + '__label', 'Phone number');
    label.setAttribute('for', inputId);
    var input = el('input', PREFIX + '__input');
    input.type = 'tel';
    input.id = inputId;
    input.name = 'phone';
    input.placeholder = '(555) 123-4567';
    input.setAttribute('autocomplete', 'tel');
    input.setAttribute('aria-label', 'Phone number');
    field.appendChild(label);
    field.appendChild(input);
    form.appendChild(field);

    var consent = el('label', PREFIX + '__consent');
    var checkbox = document.createElement('input');
    checkbox.type = 'checkbox';
    var consentSpan = el('span', null, consentText);
    consent.appendChild(checkbox);
    consent.appendChild(consentSpan);
    form.appendChild(consent);

    var error = el('p', PREFIX + '__error');
    error.setAttribute('role', 'alert');
    error.style.display = 'none';
    form.appendChild(error);

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

    checkbox.addEventListener('change', function () {
      button.disabled = !checkbox.checked;
      if (checkbox.checked) showError('');
    });

    function showError(msg) {
      error.textContent = msg;
      error.style.display = msg ? 'block' : 'none';
    }

    function showSuccess() {
      form.style.display = 'none';
      var success = el('div', PREFIX + '__success');
      success.setAttribute('role', 'status');
      success.appendChild(el('span', PREFIX + '__success-icon', '✓'));
      success.appendChild(el('p', PREFIX + '__success-text', 'You\'re signed up! Watch for a confirmation text.'));
      root.insertBefore(success, root.querySelector('.' + PREFIX + '__credit'));
    }

    form.addEventListener('submit', function (e) {
      e.preventDefault();
      var phone = input.value.replace(/[^\d+]/g, '');
      var digits = phone.replace(/\D/g, '');
      if (digits.length < 7 || digits.length > 15) {
        showError('Please enter a valid phone number.');
        input.focus();
        return;
      }
      if (!checkbox.checked) {
        showError('Please agree to receive text messages before signing up.');
        return;
      }
      showError('');
      button.disabled = true;
      button.textContent = 'Sending…';
      fetch(action, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ phone: input.value.trim(), consent: true })
      }).then(function (res) {
        if (!res.ok) throw new Error('Request failed: ' + res.status);
        showSuccess();
      }).catch(function (err) {
        console.warn('[haynes lab sms-optin-form] Signup request failed.', err);
        showError('Something went wrong. Please try again.');
        button.disabled = false;
        button.textContent = buttonText;
      });
    });

    root.appendChild(form);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/sms-optin-form';
    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 sms-optin-form] 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)URL the phone number is POSTed to as JSON ({ phone, consent }). Any form backend, webhook, or SMS platform endpoint works.
data-hl-title"Get updates by text"Heading displayed at the top of the form card.
data-hl-consent-text"I agree to receive text messages at the number provided. Message and data rates may apply. Reply STOP to opt out."Checkbox label visitors must tick before they can submit. Adjust to match your messaging terms.
data-hl-button-text"Sign up"Label of the submit button.
data-hl-accent"#f97316"Any CSS color used for the submit button, checkbox, and success icon.

Frequently asked questions

How do I install the SMS opt-in form?

Copy the embed snippet and paste it where you want the form to appear on your page — it renders inline right at that spot. Set data-hl-action to the endpoint that should receive signups (Formspree, a Zapier webhook, or your SMS platform's form URL).

How do I customize the text and colors?

Everything is controlled through data-hl-* attributes on the script tag: data-hl-title for the heading, data-hl-consent-text for the opt-in disclosure, data-hl-button-text for the button label, and data-hl-accent for the button and checkbox color.

Does it work on WordPress, Shopify, or Squarespace?

Yes. The widget is a dependency-free vanilla JS file, so it runs anywhere you can paste a script tag — WordPress (via a code snippet plugin or an HTML block), Shopify (theme.liquid or a custom liquid section), Squarespace (code injection or a code block), Webflow, or a plain HTML site.

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