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 Poll Widget widget by haynes lab — https://hayneslab.dev/tools/widgets/poll-widget -->
<script data-hl-config='{"question":"What's the best part of a Friday?","options":["Logging off early","Takeout night","Weekend plans kicking in","Absolutely all of it"]}'>
/*!
* haynes lab — Poll Widget widget (free)
* Docs & embed code: https://hayneslab.dev/tools/widgets/poll-widget
*/
(function () {
'use strict';
var PREFIX = 'hlw-poll-widget';
var STYLE_ID = 'hlw-poll-widget-styles';
function findScript() {
return document.currentScript ||
document.querySelector('script[src*="widgets/poll-widget.js"]');
}
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
var css =
'.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
'.' + PREFIX + ' { position: relative; max-width: 480px; margin: 0 auto; padding: 24px 24px 38px;' +
' 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 + '__question { margin: 0 0 16px; font-size: 18px; font-weight: 600; color: #111827; }' +
'.' + PREFIX + '__options { display: flex; flex-direction: column; gap: 8px; }' +
'.' + PREFIX + '__option { display: block; width: 100%; padding: 12px 16px; font: inherit; text-align: left;' +
' color: #1f2937; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; cursor: pointer;' +
' transition: border-color 0.15s ease, background-color 0.15s ease; }' +
'.' + PREFIX + '__option:hover { border-color: var(--hlw-accent); background: #fafafa; }' +
'.' + PREFIX + '__option:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
'.' + PREFIX + '__row { display: block; width: 100%; padding: 4px 2px; }' +
'.' + PREFIX + '__rowhead { display: flex; justify-content: space-between; align-items: baseline; gap: 12px;' +
' font-size: 15px; margin-bottom: 6px; }' +
'.' + PREFIX + '__label { color: #1f2937; }' +
'.' + PREFIX + '__label--picked { font-weight: 600; color: var(--hlw-accent); }' +
'.' + PREFIX + '__pct { font-variant-numeric: tabular-nums; color: #6b7280; }' +
'.' + PREFIX + '__pct--picked { font-weight: 600; color: var(--hlw-accent); }' +
'.' + PREFIX + '__track { height: 8px; border-radius: 999px; background: #f3f4f6; overflow: hidden; }' +
'.' + PREFIX + '__fill { height: 100%; width: 0; border-radius: 999px; background: #d1d5db;' +
' transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1); }' +
'.' + PREFIX + '__fill--picked { background: var(--hlw-accent); }' +
'.' + PREFIX + '__meta { margin: 14px 0 0; font-size: 12px; color: #9ca3af; }' +
'.' + 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; }';
var style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = css;
document.head.appendChild(style);
}
function parseConfig(script) {
var raw = script.getAttribute('data-hl-config');
if (!raw) {
console.warn('[haynes lab poll-widget] Missing required data-hl-config attribute (JSON {"question", "options": [...]}). Nothing rendered.');
return null;
}
var cfg;
try {
cfg = JSON.parse(raw);
} catch (e) {
console.warn('[haynes lab poll-widget] data-hl-config is not valid JSON. Nothing rendered.', e);
return null;
}
if (!cfg || typeof cfg.question !== 'string' || !cfg.question ||
!Array.isArray(cfg.options)) {
console.warn('[haynes lab poll-widget] data-hl-config needs a "question" string and an "options" array. Nothing rendered.');
return null;
}
var options = cfg.options.filter(function (o) {
return typeof o === 'string' && o;
});
if (options.length < 2) {
console.warn('[haynes lab poll-widget] data-hl-config needs at least 2 non-empty option strings. Nothing rendered.');
return null;
}
return { question: cfg.question, options: options };
}
function el(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text != null) node.textContent = text;
return node;
}
// Small deterministic string hash (djb2) — used for the storage key and
// stable sample vote counts so bars look alive without a backend.
function hash(str) {
var h = 5381;
for (var i = 0; i < str.length; i++) {
h = ((h << 5) + h + str.charCodeAt(i)) >>> 0;
}
return h;
}
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 cfg = parseConfig(script);
if (!cfg) return;
var accent = script.getAttribute('data-hl-accent') || '#f97316';
injectStyles();
var key = PREFIX + ':' + hash(cfg.question + '|' + cfg.options.join('|'));
var picked = null;
try {
var stored = window.localStorage.getItem(key);
if (stored !== null) {
var n = parseInt(stored, 10);
if (!isNaN(n) && n >= 0 && n < cfg.options.length) picked = n;
}
} catch (e) { /* private mode etc. — vote simply won't persist */ }
// Baseline counts are deterministic sample numbers derived from the
// question/options — there is no backend, so only the visitor's own
// vote (stored in their browser) is ever added on top.
var counts = cfg.options.map(function (option, i) {
return 8 + (hash(cfg.question + '|' + option + '|' + i) % 45);
});
if (picked !== null) counts[picked] += 1;
var root = el('div', PREFIX);
root.setAttribute('role', 'group');
root.setAttribute('aria-label', 'Poll');
root.style.setProperty('--hlw-accent', accent);
root.appendChild(el('p', PREFIX + '__question', cfg.question));
var optionsBox = el('div', PREFIX + '__options');
root.appendChild(optionsBox);
function renderResults() {
var total = counts.reduce(function (sum, c) { return sum + c; }, 0);
optionsBox.setAttribute('aria-live', 'polite');
counts.forEach(function (count, i) {
var isPicked = i === picked;
var pct = total ? Math.round((count / total) * 100) : 0;
var row = el('div', PREFIX + '__row');
var head = el('div', PREFIX + '__rowhead');
head.appendChild(el('span',
PREFIX + '__label' + (isPicked ? ' ' + PREFIX + '__label--picked' : ''),
cfg.options[i] + (isPicked ? ' ✓' : '')));
head.appendChild(el('span',
PREFIX + '__pct' + (isPicked ? ' ' + PREFIX + '__pct--picked' : ''),
pct + '%'));
row.appendChild(head);
var track = el('div', PREFIX + '__track');
var fill = el('div', PREFIX + '__fill' + (isPicked ? ' ' + PREFIX + '__fill--picked' : ''));
track.appendChild(fill);
row.appendChild(track);
optionsBox.appendChild(row);
// Trigger the width transition on the next frame so bars animate in.
window.requestAnimationFrame(function () {
window.requestAnimationFrame(function () {
fill.style.width = pct + '%';
});
});
});
root.appendChild(el('p', PREFIX + '__meta',
total + ' votes · results are stored on this device only'));
}
if (picked !== null) {
renderResults();
} else {
cfg.options.forEach(function (option, i) {
var btn = el('button', PREFIX + '__option', option);
btn.type = 'button';
btn.addEventListener('click', function () {
picked = i;
counts[i] += 1;
try { window.localStorage.setItem(key, String(i)); } catch (e) { /* non-persistent */ }
while (optionsBox.firstChild) optionsBox.removeChild(optionsBox.firstChild);
renderResults();
});
optionsBox.appendChild(btn);
});
}
var credit = el('a', PREFIX + '__credit');
credit.appendChild(logoSvg());
credit.appendChild(document.createTextNode('Powered by haynes lab'));
credit.href = 'https://hayneslab.dev/tools/widgets/poll-widget';
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 poll-widget] Could not locate its own script tag. Nothing rendered.');
return;
}
build(script);
}
if (document.body) {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
</script>