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.
<!-- Free Newsletter Popup widget by haynes lab — https://hayneslab.dev/tools/widgets/newsletter-popup -->
<script data-hl-action="https://hayneslab.dev/api/newsletter-demo" data-hl-title="Get the good stuff first" data-hl-message="Join 2,000+ readers getting one practical web tip every week. Free forever." data-hl-delay="4000" data-hl-scroll="40">
/*!
* haynes lab — Newsletter Popup widget (free)
* Docs & embed code: https://hayneslab.dev/tools/widgets/newsletter-popup
*/
(function () {
'use strict';
var PREFIX = 'hlw-newsletter-popup';
var STYLE_ID = 'hlw-newsletter-popup-styles';
function findScript() {
return document.currentScript ||
document.querySelector('script[src*="widgets/newsletter-popup.js"]');
}
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
var css =
'.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
'.' + PREFIX + ' { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
' font-size: 16px; line-height: 1.5; color: #1f2937; }' +
'.' + PREFIX + '__overlay { position: fixed; inset: 0; z-index: 99999; display: flex;' +
' align-items: center; justify-content: center; padding: 20px;' +
' background: rgba(17, 24, 39, 0.6); }' +
'.' + PREFIX + '__overlay--staged { position: absolute; z-index: 10; }' +
'.' + PREFIX + '__dialog { position: relative; width: 100%; max-width: 420px; padding: 36px 32px 44px;' +
' background: #ffffff; border: 1px solid #e5e7eb; border-radius: 12px; text-align: center;' +
' box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25); }' +
'.' + PREFIX + '__close { position: absolute; top: 10px; right: 10px; width: 32px; height: 32px;' +
' border: 0; border-radius: 50%; background: transparent; color: #9ca3af; font-size: 20px;' +
' line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; }' +
'.' + PREFIX + '__close:hover { background: #f3f4f6; color: #4b5563; }' +
'.' + PREFIX + '__close:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
'.' + PREFIX + '__title { margin: 0 0 10px; font-size: 22px; font-weight: 700; color: #111827; }' +
'.' + PREFIX + '__message { margin: 0 0 20px; font-size: 15px; color: #4b5563; }' +
'.' + PREFIX + '__form { display: flex; gap: 8px; }' +
'.' + PREFIX + '__input { flex: 1 1 auto; min-width: 0; padding: 11px 14px; border: 1px solid #d1d5db;' +
' border-radius: 8px; font: inherit; font-size: 15px; color: #111827; background: #ffffff; }' +
'.' + PREFIX + '__input:focus { outline: 2px solid var(--hlw-accent); outline-offset: 1px; border-color: var(--hlw-accent); }' +
'.' + PREFIX + '__submit { flex: 0 0 auto; padding: 11px 20px; border: 0; border-radius: 8px;' +
' background: var(--hlw-accent); color: #ffffff; font: inherit; font-size: 15px; font-weight: 600; cursor: pointer; }' +
'.' + PREFIX + '__submit:hover { filter: brightness(0.92); }' +
'.' + PREFIX + '__submit:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
'.' + PREFIX + '__submit[disabled] { opacity: 0.6; cursor: default; }' +
'.' + PREFIX + '__status { margin: 12px 0 0; font-size: 14px; color: #4b5563; }' +
'.' + PREFIX + '__status--error { color: #dc2626; }' +
'.' + PREFIX + '__preview { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px;' +
' border: 1px dashed #d1d5db; border-radius: 8px; background: #f9fafb; color: #4b5563;' +
' font-size: 13px; cursor: pointer; }' +
'.' + PREFIX + '__preview:hover { border-color: var(--hlw-accent); color: var(--hlw-accent); }' +
'.' + PREFIX + '__preview:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
'.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 8px; 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 + '__dialog { padding: 28px 20px 42px; }' +
' .' + PREFIX + '__title { font-size: 19px; }' +
' .' + PREFIX + '__form { flex-direction: column; } }';
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 newsletter-popup] Missing required data-hl-action attribute (form POST endpoint). Nothing rendered.');
return;
}
var title = script.getAttribute('data-hl-title') || 'Join our newsletter';
var message = script.getAttribute('data-hl-message') ||
'Get tips, updates, and offers delivered to your inbox. No spam, unsubscribe anytime.';
var delay = parseInt(script.getAttribute('data-hl-delay'), 10);
if (isNaN(delay) || delay < 0) delay = 5000;
var scroll = parseInt(script.getAttribute('data-hl-scroll'), 10);
if (isNaN(scroll) || scroll < 1 || scroll > 100) scroll = 0; // 0 = scroll trigger off
var accent = script.getAttribute('data-hl-accent') || '#f97316';
injectStyles();
var stage = script.closest ? script.closest('.widget-demo__stage') : null;
var storageKey = 'hlw-newsletter-popup-shown:' + action;
var overlay = el('div', PREFIX + ' ' + PREFIX + '__overlay' + (stage ? ' ' + PREFIX + '__overlay--staged' : ''));
overlay.style.setProperty('--hlw-accent', accent);
overlay.setAttribute('hidden', '');
overlay.style.display = 'none';
var dialog = el('div', PREFIX + '__dialog');
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.setAttribute('aria-label', title);
var close = el('button', PREFIX + '__close', '×');
close.type = 'button';
close.setAttribute('aria-label', 'Close popup');
dialog.appendChild(close);
dialog.appendChild(el('h2', PREFIX + '__title', title));
dialog.appendChild(el('p', PREFIX + '__message', message));
var form = el('form', PREFIX + '__form');
var input = el('input', PREFIX + '__input');
input.type = 'email';
input.name = 'email';
input.required = true;
input.placeholder = '[email protected]';
input.setAttribute('aria-label', 'Email address');
var submit = el('button', PREFIX + '__submit', 'Subscribe');
submit.type = 'submit';
form.appendChild(input);
form.appendChild(submit);
dialog.appendChild(form);
var status = el('p', PREFIX + '__status');
status.setAttribute('aria-live', 'polite');
var credit = el('a', PREFIX + '__credit');
credit.appendChild(logoSvg());
credit.appendChild(document.createTextNode('Powered by haynes lab'));
credit.href = 'https://hayneslab.dev/tools/widgets/newsletter-popup';
credit.target = '_blank';
credit.rel = 'noopener';
dialog.appendChild(credit);
overlay.appendChild(dialog);
function markShown() {
try { sessionStorage.setItem(storageKey, '1'); } catch (e) { /* storage unavailable */ }
}
function hide() {
overlay.style.display = 'none';
overlay.setAttribute('hidden', '');
document.removeEventListener('keydown', onKeydown);
}
function onKeydown(e) {
if (e.key === 'Escape') hide();
}
function show(mark) {
if (mark) markShown();
overlay.removeAttribute('hidden');
overlay.style.display = 'flex';
document.addEventListener('keydown', onKeydown);
input.focus();
}
form.addEventListener('submit', function (e) {
e.preventDefault();
submit.disabled = true;
status.textContent = '';
status.className = PREFIX + '__status';
fetch(action, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ email: input.value })
}).then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
form.style.display = 'none';
status.textContent = 'Thanks for subscribing!';
dialog.appendChild(status);
markShown();
setTimeout(hide, 2500);
}).catch(function () {
status.textContent = 'Something went wrong. Please try again.';
status.className = PREFIX + '__status ' + PREFIX + '__status--error';
if (!status.parentNode) dialog.appendChild(status);
submit.disabled = false;
});
});
close.addEventListener('click', function () { markShown(); hide(); });
overlay.addEventListener('click', function (e) {
if (e.target === overlay) { markShown(); hide(); }
});
if (stage) {
// Landing-page demo: never auto-trigger, offer a preview button instead.
stage.appendChild(overlay);
var preview = el('button', PREFIX + ' ' + PREFIX + '__preview', 'Preview popup');
preview.type = 'button';
preview.style.setProperty('--hlw-accent', accent);
preview.addEventListener('click', function () { show(false); });
stage.appendChild(preview);
return;
}
// Real page: trigger once per session, on data-hl-delay or scroll depth —
// whichever comes first.
document.body.appendChild(overlay);
var alreadyShown = false;
try { alreadyShown = sessionStorage.getItem(storageKey) === '1'; } catch (e) { /* ignore */ }
if (alreadyShown) return;
var fired = false;
function trigger() {
if (fired) return;
fired = true;
window.removeEventListener('scroll', onScroll);
show(true);
}
function onScroll() {
var doc = document.documentElement;
var max = doc.scrollHeight - window.innerHeight;
if (max <= 0) return;
if ((window.pageYOffset || doc.scrollTop) / max * 100 >= scroll) trigger();
}
setTimeout(trigger, delay);
if (scroll) window.addEventListener('scroll', onScroll, { passive: true });
}
function init() {
var script = findScript();
if (!script) {
console.warn('[haynes lab newsletter-popup] Could not locate its own script tag. Nothing rendered.');
return;
}
build(script);
}
if (document.body) {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
</script>