โ† Overlays

Announcement Bar Rotator

A slim bar across the top of the page takes turns with its messages: each one slides up and out as the next slides in, with a thin line filling up until the next. Hover over it, or tab into it, and it waits for you.

Free delivery on orders over ยฃ40 Shop now

Gift cards are here Shop now

Spring sale ends Sunday Shop now

Bloom

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-announcement-bar-rotator to your bar and put each message in it as its own element, such as a <p>.

Options

Add any of these attributes to the same element.

data-intervalHow long each message stays. Default 3500 ms.
data-durationTime for the slide between messages. Default 650 ms.
data-directionup or left. Default up.
data-arrowsAdds previous and next buttons at the ends.
data-progressShows a thin line along the bottom that fills up until the next message.
data-labelIts name for screen readers. Default “Announcements”.

The bar is as tall as its longest message, so nothing below it jumps. It pauses while the pointer is over it, while focus is inside, and when it’s off screen. With reduced motion it stays on the first message and the arrows still work. From code: animacious.announcementBar(el, 1) shows the second message.

1 ยท Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Announcement Bar Rotator: a slim bar at the top of the page that shows
    // its messages one after another, each sliding in as the last slides out.
    // It pauses while you hover or focus it.
    // Usage:
    //   <div data-announcement-bar-rotator>
    //     <p>Free delivery on orders over ยฃ40</p>
    //     <p>New: gift cards are here <a href="/gifts">Shop now</a></p>
    //     <p>Spring sale ends Sunday</p>
    //   </div>
    // Optional: data-interval (ms each message stays, default 3500),
    // data-duration (ms for the slide, default 650), data-direction ("up",
    // default, or "left"), data-arrows (adds previous and next buttons),
    // data-progress (a thin line along the bottom fills up until the next
    // message), data-label (accessible name, default "Announcements").
    // Script use: animacious.announcementBar(el, 1) shows the second message.
    var DEFAULTS = { interval: 3500, duration: 650, direction: 'up', label: 'Announcements' };

    var css =
      '.abr{position:relative;display:flex;align-items:center;gap:6px;box-sizing:border-box}' +
      ':where(.abr){padding:9px 12px;background:#1a1625;color:#fff;font:600 14px/1.4 system-ui,sans-serif}' +
      '.abr-track{position:relative;flex:1;min-width:0;display:grid;overflow:hidden}' +
      '.abr-msg{grid-area:1/1;margin:0;min-width:0;text-align:center}' +
      '.abr-msg:not(.abr-on){visibility:hidden}' +
      '.abr-btn{flex:none;display:grid;place-items:center;width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:inherit;font:inherit;font-size:18px;line-height:1;opacity:.75;cursor:pointer}' +
      '.abr-btn:hover,.abr-btn:focus-visible{opacity:1;background:rgba(127,127,127,.22)}' +
      '.abr-bar{position:absolute;left:0;right:0;bottom:0;height:2px;background:currentColor;opacity:0;transform-origin:0 50%;transform:scaleX(0);pointer-events:none}' +
      '.abr-prog .abr-bar{opacity:.45}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function on(v) { return v !== undefined && v !== 'false'; }

    function setup(el) {
      var d = el.dataset;
      var r = {
        el: el, i: 0, timer: null, visible: false, hover: false, focus: false,
        interval: +d.interval || DEFAULTS.interval,
        duration: +d.duration || DEFAULTS.duration,
        left: (d.direction || DEFAULTS.direction) === 'left',
        deco: !!el.closest('[aria-hidden="true"]')
      };
      el._abr = r;
      el.classList.add('abr');
      if (on(d.progress)) el.classList.add('abr-prog');
      var track = document.createElement('div');
      track.className = 'abr-track';
      r.msgs = Array.from(el.children);
      r.msgs.forEach(function (m) { m.classList.add('abr-msg'); track.appendChild(m); });
      el.appendChild(track);
      r.track = track;
      r.bar = document.createElement('span');
      r.bar.className = 'abr-bar';
      r.bar.setAttribute('aria-hidden', 'true');
      el.appendChild(r.bar);
      if (!r.msgs.length) return r;
      r.msgs[0].classList.add('abr-on');
      if (!r.deco) {
        if (!el.getAttribute('role')) el.setAttribute('role', 'region');
        if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', d.label || DEFAULTS.label);
        r.msgs.forEach(function (m, k) { if (k) m.setAttribute('aria-hidden', 'true'); });
      }
      if (on(d.arrows) && r.msgs.length > 1) {
        el.insertBefore(arrow(r, -1, '&lsaquo;', 'Previous announcement'), track);
        el.appendChild(arrow(r, 1, '&rsaquo;', 'Next announcement'));
      }
      el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') { r.hover = true; sync(r); } });
      el.addEventListener('pointerleave', function () { r.hover = false; sync(r); });
      el.addEventListener('focusin', function () { r.focus = true; sync(r); });
      el.addEventListener('focusout', function (e) { if (!el.contains(e.relatedTarget)) { r.focus = false; sync(r); } });
      if (r.msgs.length > 1 && !reduced()) {
        new IntersectionObserver(function (entries) {
          r.visible = entries[entries.length - 1].isIntersecting;
          sync(r);
        }).observe(el);
      }
      return r;
    }

    function arrow(r, step, html, label) {
      var b = document.createElement(r.deco ? 'span' : 'button');
      b.className = 'abr-btn';
      b.innerHTML = html;
      if (!r.deco) { b.type = 'button'; b.setAttribute('aria-label', label); }
      b.addEventListener('click', function () { go(r, r.i + step, step); });
      return b;
    }

    // Run or pause the countdown to the next message.
    function sync(r) {
      var run = r.visible && !r.hover && !r.focus && !reduced();
      if (!r.timer) {
        if (!run) return;
        anime.utils.set(r.bar, { scaleX: 0 });
        r.timer = anime.animate(r.bar, {
          scaleX: [0, 1], duration: r.interval, ease: 'linear',
          onComplete: function () { r.timer = null; go(r, r.i + 1, 1); }
        });
      } else if (run) r.timer.resume();
      else r.timer.pause();
    }

    function go(r, n, dir) {
      var len = r.msgs.length;
      n = ((n % len) + len) % len;
      if (r.timer) { r.timer.pause(); r.timer = null; }
      if (n === r.i) return sync(r);
      var out = r.msgs[r.i], inn = r.msgs[n];
      r.i = n;
      out.classList.remove('abr-on');
      inn.classList.add('abr-on');
      if (!r.deco) { out.setAttribute('aria-hidden', 'true'); inn.removeAttribute('aria-hidden'); }
      if (out.contains(document.activeElement)) document.activeElement.blur();
      anime.utils.remove([out, inn]);
      if (reduced()) {
        anime.utils.set([out, inn], { x: 0, y: 0, opacity: 1 });
      } else {
        var dist = r.left ? r.track.clientWidth * 0.5 : r.track.clientHeight + 4;
        var prop = r.left ? 'x' : 'y';
        var a = { opacity: [1, 0], duration: r.duration * 0.7, ease: 'in(2)' };
        a[prop] = [0, -dist * dir];
        out.style.visibility = 'visible';
        a.onComplete = function () { out.style.visibility = ''; anime.utils.set(out, { x: 0, y: 0, opacity: 1 }); };
        anime.animate(out, a);
        var b = { opacity: [0, 1], duration: r.duration, delay: r.duration * 0.15, ease: 'out(4)' };
        b[prop] = [dist * dir, 0];
        anime.animate(inn, b);
      }
      sync(r);
    }

    function init() {
      document.querySelectorAll('[data-announcement-bar-rotator]').forEach(function (el) {
        if (el._abr) return;
        setup(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.announcementBar = function (el, n) {
      var r = el._abr || setup(el);
      go(r, n, n >= r.i ? 1 : -1);
    };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 ยท Your HTML
<div data-announcement-bar-rotator data-progress>
  <p>Free delivery on orders over ยฃ40</p>
  <p>New: gift cards are here <a href="/gifts">Shop now</a></p>
  <p>Spring sale ends Sunday</p>
</div>