Marketing & Social

Free Announcement Bar Widget

A slim banner for promos, launches, and urgent notices.

A lightweight top-of-page bar that announces sales, launches, or service updates. One script tag adds it to any page — no plugins, no build step. Dismissible, mobile-friendly, and styled to match your brand with a couple of attributes.

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/announcement-bar.js" 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" async></script>
<!-- Free Announcement Bar widget by haynes lab — https://hayneslab.dev/tools/widgets/announcement-bar -->
Inline (self-contained)html
<!-- 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>

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-message"Welcome!"Text shown in the bar. Plain text, keep it under ~90 characters.
data-hl-link(none)Optional URL the call-to-action button points to. Omit to render the bar without a button.
data-hl-link-text"Learn more"Label of the call-to-action button. Only used when data-hl-link is set.
data-hl-bg"#f97316"Any CSS color for the bar background.
data-hl-text-color"#ffffff"Any CSS color for the message text.
data-hl-font-size"14"Message font size in pixels. The button and close icon scale with it.
data-hl-button-bg"#ffffff"Any CSS color for the call-to-action button background.
data-hl-button-text-color"#f97316"Any CSS color for the call-to-action button text and border.
data-hl-dismissible"true"When "true", a close button lets visitors hide the bar (remembered for the session).

Frequently asked questions

How do I install the announcement bar?

Copy the embed snippet and paste it just before the closing </body> tag of your page (or in your platform's "custom code" area). The script renders the bar at the top of the page automatically — no other setup needed.

How do I customize the colors and text?

Everything is controlled through data-hl-* attributes on the script tag itself. Change data-hl-message for the text, data-hl-bg for the background, and data-hl-text-color for the font color. No CSS file editing required.

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 theme footer), Shopify (theme.liquid), Squarespace (code injection), 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