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 Announcement Bar widget by haynes lab — https://hayneslab.dev/tools/widgets/announcement-bar -->
<script data-hl-message="Summer sale: 25% off all service packages booked before August 31" data-hl-link="https://hayneslab.dev/contact" data-hl-link-text="Book now">
/*!
* haynes lab — Announcement Bar widget (free)
* Docs & embed code: https://hayneslab.dev/tools/widgets/announcement-bar
*/
(function () {
'use strict';
var PREFIX = 'hlw-announcement-bar';
var STYLE_ID = 'hlw-announcement-bar-styles';
var STORAGE_KEY = 'hlw_announcement-bar_dismissed';
var script = document.currentScript ||
document.querySelector('script[src*="widgets/announcement-bar.js"]');
if (!script) {
console.warn('[hlw-announcement-bar] Could not find its own <script> tag; widget not rendered.');
return;
}
function attr(name) {
var v = script.getAttribute(name);
return v === null || v === '' ? null : v;
}
// Optional structured config: data-hl-config='{"message":"...","link":"..."}'
var jsonCfg = {};
var rawJson = attr('data-hl-config');
if (rawJson) {
try {
var parsed = JSON.parse(rawJson);
if (parsed && typeof parsed === 'object') jsonCfg = parsed;
} catch (e) {
console.warn('[hlw-announcement-bar] Ignoring malformed JSON in data-hl-config:', e.message);
}
}
function pick(flat, jsonVal, fallback) {
if (flat !== null && flat !== undefined) return flat;
if (jsonVal !== null && jsonVal !== undefined) return String(jsonVal);
return fallback;
}
var cfg = {
message: pick(attr('data-hl-message'), jsonCfg.message, 'Welcome!'),
link: pick(attr('data-hl-link'), jsonCfg.link, null),
linkText: pick(attr('data-hl-link-text'), jsonCfg.linkText, 'Learn more'),
bg: pick(attr('data-hl-bg'), jsonCfg.bg, '#f97316'),
textColor: pick(attr('data-hl-text-color'), jsonCfg.textColor, '#ffffff'),
fontSize: pick(attr('data-hl-font-size'), jsonCfg.fontSize, null),
buttonBg: pick(attr('data-hl-button-bg'), jsonCfg.buttonBg, '#ffffff'),
buttonTextColor: pick(attr('data-hl-button-text-color'), jsonCfg.buttonTextColor, '#f97316'),
dismissible: pick(attr('data-hl-dismissible'), jsonCfg.dismissible, 'true') !== 'false'
};
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
var css =
'.' + PREFIX + ' {' +
'position: fixed; top: 0; left: 0; right: 0; z-index: 2147483000;' +
'display: flex; align-items: center; justify-content: center;' +
'gap: 12px; padding: 10px 44px 10px 16px; min-height: 44px;' +
'font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
'font-size: var(--hlw-ab-font-size, 14px); line-height: 1.4; text-align: center;' +
'box-shadow: 0 1px 3px rgba(0,0,0,0.12);' +
'}' +
'.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
'.' + PREFIX + '__message { margin: 0; font-size: inherit; font-weight: 500; }' +
'.' + PREFIX + '__cta {' +
'flex: none; display: inline-block; padding: 4px 12px;' +
'border: 1px solid currentColor; border-radius: 999px;' +
'font: inherit; font-size: 0.93em; font-weight: 600;' +
'color: inherit; text-decoration: none; white-space: nowrap;' +
'}' +
'.' + PREFIX + '__cta:hover, .' + PREFIX + '__cta:focus-visible { opacity: 0.85; }' +
'.' + PREFIX + '__close {' +
'position: absolute; top: 50%; right: 8px; transform: translateY(-50%);' +
'display: flex; align-items: center; justify-content: center;' +
'width: 28px; height: 28px; padding: 0; margin: 0;' +
'border: 0; border-radius: 4px; background: transparent;' +
'color: inherit; font: inherit; font-size: 18px; line-height: 1; cursor: pointer;' +
'}' +
'.' + PREFIX + '__close:hover, .' + PREFIX + '__close:focus-visible { background: rgba(0,0,0,0.15); outline: none; }' +
'.' + PREFIX + '__powered {' +
'position: absolute; right: 40px; bottom: 1px;' +
'display: inline-flex; align-items: center; gap: 4px;' +
'font-size: 11px; line-height: 1; color: inherit; opacity: 0.6;' +
'text-decoration: none;' +
'}' +
'.' + PREFIX + '__powered svg { display: block; }' +
'.' + PREFIX + '__powered:hover, .' + PREFIX + '__powered:focus-visible { opacity: 0.9; text-decoration: underline; }' +
'@media (max-width: 600px) {' +
'.' + PREFIX + ' { flex-wrap: wrap; gap: 4px 8px; padding: 8px 40px 12px 12px; font-size: var(--hlw-ab-font-size, 13px); }' +
'.' + PREFIX + '__powered { position: static; width: 100%; justify-content: center; margin-top: 2px; }' +
'}';
var style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
function dismissed() {
try { return window.sessionStorage.getItem(STORAGE_KEY) === '1'; }
catch (e) { return false; }
}
function rememberDismissed() {
try { window.sessionStorage.setItem(STORAGE_KEY, '1'); }
catch (e) { /* private mode etc. — dismiss still works for this pageview */ }
}
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 render() {
// The script may run again (live demo re-injection, double embed) — tear
// down any previous instance so bars never stack.
if (typeof window.__hlwAnnouncementBarTeardown === 'function') {
window.__hlwAnnouncementBarTeardown();
}
if (dismissed()) return;
injectStyles();
var root = document.createElement('div');
root.className = PREFIX;
root.setAttribute('role', 'region');
root.setAttribute('aria-label', 'Announcement');
root.style.backgroundColor = cfg.bg;
root.style.color = cfg.textColor;
if (cfg.fontSize) {
var fontSizePx = parseInt(cfg.fontSize, 10);
if (!isNaN(fontSizePx) && fontSizePx > 0) {
root.style.setProperty('--hlw-ab-font-size', fontSizePx + 'px');
}
}
var msg = document.createElement('p');
msg.className = PREFIX + '__message';
msg.textContent = cfg.message;
root.appendChild(msg);
if (cfg.link) {
var cta = document.createElement('a');
cta.className = PREFIX + '__cta';
cta.href = cfg.link;
cta.textContent = cfg.linkText;
cta.style.backgroundColor = cfg.buttonBg;
cta.style.color = cfg.buttonTextColor;
root.appendChild(cta);
}
var powered = document.createElement('a');
powered.className = PREFIX + '__powered';
powered.href = 'https://hayneslab.dev/tools/widgets/announcement-bar';
powered.target = '_blank';
powered.rel = 'noopener';
powered.appendChild(logoSvg());
powered.appendChild(document.createTextNode('Powered by haynes lab'));
root.appendChild(powered);
function removeBar() {
document.documentElement.style.paddingTop = '';
if (root.parentNode) root.parentNode.removeChild(root);
window.removeEventListener('resize', syncPadding);
}
window.__hlwAnnouncementBarTeardown = removeBar;
function syncPadding() {
document.documentElement.style.paddingTop = root.offsetHeight + 'px';
}
if (cfg.dismissible) {
var close = document.createElement('button');
close.type = 'button';
close.className = PREFIX + '__close';
close.setAttribute('aria-label', 'Dismiss announcement');
close.textContent = '×';
close.addEventListener('click', function () {
rememberDismissed();
removeBar();
});
root.appendChild(close);
}
// Fixed-position widget: append to <body> and push page content down.
document.body.appendChild(root);
syncPadding();
window.addEventListener('resize', syncPadding);
}
if (document.body) {
render();
} else {
document.addEventListener('DOMContentLoaded', render);
}
})();
</script>