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.
How to use it
- Paste the snippet just before
</head>. - Add
data-lower-thirdto an element holding two children: the name, then the title. - 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-color | Colour of the edge and the title bar. Default #a855f7. |
data-hold | How long it stays on screen before leaving. Default 3200 ms. |
data-loop | Come back again after a pause, over and over. |
data-gap | Time off screen between loops. Default 1200 ms. |
data-delay | Wait before the first entrance. Default 300 ms. |
Call animacious.lowerThird(el) to play it again, for example when the speaker changes.
<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><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>