← Broadcast

Lower Third

A name and title bar for the bottom of a video frame. A coloured edge grows up, the name wipes out from behind it, the title follows underneath, it holds, then everything tucks back into the edge.

Maya OkaforClimate correspondent

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-lower-third to an element holding two children: the name, then the title.
  3. Position it over your video yourself (for example absolutely, bottom left) and size it with font-size.

Options

Add any of these attributes to the same element.

data-colorColour of the edge and the title bar. Default #a855f7.
data-holdHow long it stays on screen before leaving. Default 3200 ms.
data-loopCome back again after a pause, over and over.
data-gapTime off screen between loops. Default 1200 ms.
data-delayWait before the first entrance. Default 300 ms.

Call animacious.lowerThird(el) to play it again, for example when the speaker changes.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Lower Third: a name and title bar for the bottom of a video frame. A
    // coloured edge grows up, the name bar wipes out from behind it with the
    // name sliding in, the title follows underneath, it holds, then it all
    // wipes away in reverse.
    // Usage: <div data-lower-third><strong>Ada Lovelace</strong><span>Mathematician</span></div>
    // The first child is the name, the second (optional) the title. Position
    // the element yourself (e.g. absolute, bottom left of your frame) and size
    // it with font-size.
    // Optional: data-color (edge and title bar colour, default #a855f7),
    // data-hold (ms on screen before leaving, default 3200), data-loop (come
    // back again after a pause), data-gap (ms off screen between loops,
    // default 1200), data-delay (ms before the first entrance, default 300).
    // Script use: animacious.lowerThird(el) plays it once more.
    var DEFAULTS = { color: '#a855f7', hold: 3200, gap: 1200, delay: 300 };

    var css =
      '.lth{display:inline-flex;align-items:stretch;gap:0;text-align:left;line-height:1.2;--lth-c:' + DEFAULTS.color + '}' +
      '.lth-edge{flex:none;width:0.32em;background:var(--lth-c);transform-origin:50% 100%;border-radius:2px}' +
      '.lth-box{display:flex;flex-direction:column;align-items:flex-start}' +
      '.lth-row{position:relative;display:block;overflow:hidden}' +
      '.lth-bg{position:absolute;inset:0;transform-origin:0 50%}' +
      '.lth-name .lth-bg{background:#fff;box-shadow:0 0.3em 1.2em rgba(0,0,0,.25)}' +
      '.lth-title .lth-bg{background:var(--lth-c)}' +
      '.lth-in{position:relative;display:block;white-space:nowrap}' +
      '.lth-name .lth-in{padding:0.38em 0.9em 0.32em 0.7em;color:#111827;font-weight:800;font-size:1.25em;letter-spacing:-0.01em}' +
      '.lth-title .lth-in{padding:0.3em 0.9em 0.32em 0.85em;color:#fff;font-weight:600;font-size:0.8em;letter-spacing:0.08em;text-transform:uppercase}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function row(cls, src) {
      var r = document.createElement('span');
      r.className = 'lth-row ' + cls;
      var bg = document.createElement('span');
      bg.className = 'lth-bg';
      var inner = document.createElement('span');
      inner.className = 'lth-in';
      inner.textContent = src.textContent.trim();
      r.appendChild(bg);
      r.appendChild(inner);
      return { row: r, bg: bg, text: inner };
    }

    function setup(el) {
      var d = el.dataset;
      var kids = Array.prototype.slice.call(el.children);
      var name = kids[0] || el;
      var title = kids[1];
      var label = name.textContent.trim() + (title ? ', ' + title.textContent.trim() : '');
      el.classList.add('lth');
      el.style.setProperty('--lth-c', d.color || DEFAULTS.color);
      el.setAttribute('role', 'group');
      el.setAttribute('aria-label', label);

      var edge = document.createElement('span');
      edge.className = 'lth-edge';
      var box = document.createElement('span');
      box.className = 'lth-box';
      var n = row('lth-name', name);
      var t = title ? row('lth-title', title) : null;
      box.appendChild(n.row);
      if (t) box.appendChild(t.row);
      el.textContent = '';
      el.appendChild(edge);
      el.appendChild(box);
      edge.setAttribute('aria-hidden', 'true');
      box.setAttribute('aria-hidden', 'true');
      el._lth = { edge: edge, n: n, t: t };
      if (!reduced()) hide(el);
    }

    function hide(el) {
      var s = el._lth;
      anime.utils.set(s.edge, { scaleY: 0 });
      [s.n, s.t].forEach(function (r) {
        if (!r) return;
        anime.utils.set(r.bg, { scaleX: 0 });
        anime.utils.set(r.text, { x: '-110%', opacity: 0 });
      });
    }

    function play(el) {
      var s = el._lth, d = el.dataset;
      if (s.tl) s.tl.pause();
      hide(el);
      var loop = d.loop !== undefined && d.loop !== 'false';
      var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
      var gap = d.gap !== undefined ? +d.gap : DEFAULTS.gap;
      var tl = anime.createTimeline({
        delay: d.delay !== undefined ? +d.delay : DEFAULTS.delay,
        loop: loop,
        loopDelay: gap
      });
      // In: edge grows, bars wipe out from behind it, text follows.
      tl.add(s.edge, { scaleY: [0, 1], duration: 380, ease: 'out(4)' })
        .add(s.n.bg, { scaleX: [0, 1], duration: 520, ease: 'inOut(3)' }, '-=120')
        .add(s.n.text, { x: ['-110%', '0%'], opacity: [0, 1], duration: 620, ease: 'out(4)' }, '-=300');
      if (s.t) {
        tl.add(s.t.bg, { scaleX: [0, 1], duration: 460, ease: 'inOut(3)' }, '-=520')
          .add(s.t.text, { x: ['-110%', '0%'], opacity: [0, 1], duration: 560, ease: 'out(4)' }, '-=280');
      }
      // Out: everything slides back into the edge, title first.
      var rows = s.t ? [s.t, s.n] : [s.n];
      tl.add(rows.map(function (r) { return r.text; }), {
        x: '-110%', opacity: 0, duration: 380, ease: 'in(3)', delay: anime.stagger(90)
      }, '+=' + hold)
        .add(rows.map(function (r) { return r.bg; }), {
          scaleX: 0, duration: 420, ease: 'inOut(3)', delay: anime.stagger(90)
        }, '-=240')
        .add(s.edge, { scaleY: 0, duration: 300, ease: 'in(3)' }, '-=120');
      s.tl = tl;
    }

    window.animacious = window.animacious || {};
    window.animacious.lowerThird = function (el) { if (el && el._lth && !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-lower-third]').forEach(function (el) {
        if (el._lth) return;
        setup(el);
        if (!reduced()) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div class="frame" style="position: relative">
  <video src="interview.mp4" autoplay muted loop></video>
  <div data-lower-third style="position: absolute; left: 5%; bottom: 10%">
    <strong>Maya Okafor</strong>
    <span>Climate correspondent</span>
  </div>
</div>