← Broadcast

Breaking News Banner

A banner slams in big and lands with a white flash and a shudder, the headline slides up into its bar, and the red “Breaking” tag keeps pulsing, with a blinking dot, for as long as it’s up.

Storm closes every bridge across the city

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-breaking-news-banner to an element holding the headline. It fills the width of its container; size it with font-size.

Options

Add any of these attributes to the same element.

data-tagText of the tag. Default “Breaking”.
data-subSmaller line under the tag. Default “News”; set it empty for none.
data-colorTag colour. Default #e11d48.
data-loopLeave after a while and slam in again.
data-holdHow long it stays before leaving, when looping. Default 4200 ms.

Call animacious.breakingNews(el) to slam it in again, for example after changing the headline.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Breaking News Banner: a banner slams in with a white flash and a little
    // shudder, the headline slides up into the bar, and a "Breaking" tag keeps
    // pulsing red while it's on screen.
    // Usage: <div data-breaking-news-banner>Storm closes every bridge in the city</div>
    // Size it with font-size; it fills the width of its container.
    // Optional: data-tag (tag text, default "Breaking"), data-sub (smaller line
    // under the tag, default "News"; data-sub="" for none), data-color (tag
    // colour, default #e11d48), data-loop (leave and slam in again),
    // data-hold (ms on screen before leaving when looping, default 4200).
    // Script use: animacious.breakingNews(el) slams it in again.
    var DEFAULTS = { tag: 'Breaking', sub: 'News', color: '#e11d48', hold: 4200 };

    var css =
      '.bnb{display:block;text-align:left;line-height:1.15;--bnb-c:' + DEFAULTS.color + '}' +
      '.bnb-in{position:relative;display:flex;align-items:stretch}' +
      '.bnb-tag{position:relative;flex:none;display:flex;flex-direction:column;justify-content:center;padding:0.45em 0.8em;background:var(--bnb-c);color:#fff;text-transform:uppercase;z-index:1;box-shadow:0 0 0 rgba(225,29,72,0)}' +
      '.bnb-tag b{font-weight:900;font-size:1.05em;letter-spacing:0.04em;display:flex;align-items:center;gap:0.4em}' +
      '.bnb-tag b::before{content:"";width:0.5em;height:0.5em;border-radius:50%;background:#fff;animation:bnb-blink 1s steps(1) infinite}' +
      '.bnb-tag b,.bnb-tag small{position:relative}' +
      '.bnb-tag small{font-size:0.62em;font-weight:700;letter-spacing:0.3em;opacity:.85}' +
      '.bnb-glow{position:absolute;inset:0;background:#fff;opacity:0;pointer-events:none}' +
      '.bnb-body{position:relative;flex:1;min-width:0;display:flex;align-items:center;overflow:hidden;background:#fff;color:#111827;padding:0.4em 0.9em;transform-origin:0 50%}' +
      '.bnb-text{display:block;font-weight:800;letter-spacing:-0.01em}' +
      '.bnb-flash{position:absolute;inset:-0.2em;background:#fff;opacity:0;pointer-events:none;z-index:2}' +
      '@keyframes bnb-blink{50%{opacity:0}}' +
      '@media (prefers-reduced-motion:reduce){.bnb-tag b::before{animation:none}}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function setup(el) {
      var d = el.dataset;
      var text = el.textContent.replace(/\s+/g, ' ').trim();
      var tagText = d.tag !== undefined ? d.tag : DEFAULTS.tag;
      var subText = d.sub !== undefined ? d.sub : DEFAULTS.sub;
      el.classList.add('bnb');
      el.style.setProperty('--bnb-c', d.color || DEFAULTS.color);
      el.setAttribute('role', 'status');
      el.setAttribute('aria-label', [tagText, subText].filter(Boolean).join(' ') + ': ' + text);
      el.textContent = '';
      el.innerHTML = '<span class="bnb-in">' +
        '<span class="bnb-tag" aria-hidden="true"><span class="bnb-glow"></span><b></b>' +
        (subText ? '<small></small>' : '') + '</span>' +
        '<span class="bnb-body" aria-hidden="true"><span class="bnb-text"></span></span>' +
        '<span class="bnb-flash" aria-hidden="true"></span></span>';
      el.querySelector('.bnb-tag b').textContent = tagText;
      if (subText) el.querySelector('.bnb-tag small').textContent = subText;
      el.querySelector('.bnb-text').textContent = text;
      el._bnb = {
        tag: el.querySelector('.bnb-tag'),
        glow: el.querySelector('.bnb-glow'),
        body: el.querySelector('.bnb-body'),
        text: el.querySelector('.bnb-text'),
        flash: el.querySelector('.bnb-flash')
      };
      if (!reduced()) {
        anime.utils.set(el, { opacity: 0 });
      }
    }

    function play(el) {
      var s = el._bnb, d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
      if (s.tl) s.tl.pause();
      if (s.pulse) s.pulse.pause();
      anime.utils.set(el, { x: 0 });
      anime.utils.set(s.body, { scaleX: 0 });
      anime.utils.set(s.text, { y: '120%' });

      var tl = anime.createTimeline({
        onComplete: function () { if (loop) setTimeout(function () { play(el); }, 900); }
      });
      // Slam: drops in big and lands hard, with a flash and a shudder.
      tl.add(el, { opacity: [0, 1], scale: [1.35, 1], duration: 260, ease: 'in(3)' })
        .add(s.flash, { opacity: [0.95, 0], duration: 520, ease: 'out(2)' }, '-=10')
        .add(el, { x: [0, -10, 8, -5, 3, 0], duration: 360, ease: 'linear' }, '<<')
        .add(s.body, { scaleX: [0, 1], duration: 440, ease: 'out(4)' }, '-=380')
        .add(s.text, { y: ['120%', '0%'], duration: 520, ease: 'out(4)' }, '-=220');
      if (loop) {
        tl.add(s.text, { y: '-120%', duration: 320, ease: 'in(3)' }, '+=' + hold)
          .add(s.body, { scaleX: 0, duration: 360, ease: 'inOut(3)' }, '-=120')
          .add(el, { opacity: 0, x: '-6%', duration: 300, ease: 'in(3)' }, '-=120');
      }
      s.tl = tl;

      // The tag throbs: a glow that swells and fades on a heartbeat.
      s.pulse = anime.createTimeline({ loop: true, delay: 500 })
        .add(s.glow, { opacity: [0, 0.28, 0], duration: 900, ease: 'inOut(2)' })
        .add(s.tag, { scale: [1, 1.06, 1], duration: 900, ease: 'inOut(2)' }, 0);
    }

    window.animacious = window.animacious || {};
    window.animacious.breakingNews = function (el) { if (el && el._bnb && !reduced()) play(el); };

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-breaking-news-banner]').forEach(function (el) {
        if (el._bnb) return;
        setup(el);
        if (!reduced()) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-breaking-news-banner>
  Storm closes every bridge across the city
</div>