Podium Reveal
A winners’ podium that builds itself. Gold, silver and bronze bars spring up with their places on the front, then the winners drop in from above one at a time, third place first, landing with a bounce. The winner gets a crown and a burst of confetti.
- Nova
- Pixel
- Larry
How to use it
- Paste the snippet just before
</head>. - Add
data-podium-revealto an<ol>of the three winners, first place first. - Add
data-scoreto an<li>for a small line under that name. Size it withfont-size.
Options
Add any of these attributes to the same element.
data-colors | Comma-separated colours for first, second and third. Default gold, silver and bronze. |
data-loop | Clear away and build again, over and over. |
data-hold | How long it stays built before looping. Default 3000 ms. |
Call animacious.podiumReveal(el) to play it again. The crown and confetti reach a little above the podium, so leave some room over it.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Podium Reveal: three bars rise into a podium (second, first, third), the
// place numbers count on their fronts, then the winners drop in from above
// one by one, third place first, and land with a bounce. The winner gets a
// crown and a burst of confetti.
// Usage: <ol data-podium-reveal>
// <li>Nova</li><li>Pixel</li><li>Larry</li>
// </ol>
// List the winners in order, first place first. Add data-score to an <li>
// for a small line under the name. Size it with font-size.
// Optional: data-loop (clear away and build again, over and over),
// data-hold (ms it stays built before looping, default 3000),
// data-colors (comma-separated colours for first, second and third,
// default gold, silver and bronze).
// Script use: animacious.podiumReveal(el) plays it again.
var DEFAULTS = { hold: 3000, colors: '#facc15,#cbd5e1,#f59e0b' };
var HEIGHTS = [5.4, 3.9, 2.8]; // bar heights in em, for 1st, 2nd and 3rd
var ORDER = [1, 0, 2]; // left-to-right: 2nd, 1st, 3rd
var CONFETTI = ['#facc15', '#f472b6', '#38bdf8', '#4ade80', '#ffffff', '#a78bfa'];
var css =
'.pdm{position:relative;display:flex;align-items:flex-end;justify-content:center;gap:0.35em;margin:0;padding:0;list-style:none;line-height:1.15;color:#fff}' +
'.pdm-col{position:relative;display:flex;flex-direction:column;align-items:center;width:5.6em}' +
'.pdm-who{position:relative;display:flex;flex-direction:column;align-items:center;gap:0.25em;margin-bottom:0.5em;width:100%;text-align:center}' +
'.pdm-av{position:relative;display:grid;place-items:center;width:2.3em;height:2.3em;border-radius:50%;font-weight:900;color:#111827;' +
'background:radial-gradient(circle at 35% 30%,#fff,var(--pdm-c) 75%);box-shadow:0 0 0 0.14em rgba(255,255,255,.9),0 0.3em 0.8em rgba(0,0,0,.35)}' +
'.pdm-crown{position:absolute;left:50%;top:-1.05em;width:1.5em;height:1.05em;margin-left:-0.75em;fill:#facc15;filter:drop-shadow(0 0.1em 0.2em rgba(0,0,0,.4))}' +
'.pdm-name{font-weight:800;font-size:0.95em;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.pdm-score{font-size:0.7em;font-weight:700;opacity:.75;margin-top:-0.2em}' +
'.pdm-bar{position:relative;width:100%;border-radius:0.35em 0.35em 0 0;transform-origin:50% 100%;display:flex;justify-content:center;padding-top:0.35em;' +
'background:linear-gradient(180deg,color-mix(in srgb,var(--pdm-c) 85%,#fff),var(--pdm-c) 30%,color-mix(in srgb,var(--pdm-c) 55%,#000));' +
'box-shadow:inset 0 0.15em 0 rgba(255,255,255,.5),0 0.4em 1em rgba(0,0,0,.3)}' +
'.pdm-bar::before{content:"";position:absolute;left:0;right:0;top:0;height:0.5em;border-radius:inherit;background:rgba(255,255,255,.3)}' +
'.pdm-n{position:relative;font-size:2em;font-weight:900;color:rgba(0,0,0,.55);text-shadow:0 0.04em 0 rgba(255,255,255,.35)}' +
'.pdm-n sup{font-size:0.4em;vertical-align:0.9em}' +
'.pdm-p{position:absolute;left:50%;top:1.1em;width:0.4em;height:0.6em;margin:-0.3em -0.2em;border-radius:0.08em;opacity:0;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var CROWN = '<svg class="pdm-crown" viewBox="0 0 30 21" aria-hidden="true"><path d="M2 6l7 6 6-10 6 10 7-6-3 15H5z"/></svg>';
var SUFFIX = ['st', 'nd', 'rd'];
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var items = [].slice.call(el.children).slice(0, 3);
if (items.length < 3) return false;
var colors = (el.dataset.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
var winners = items.map(function (li) {
return { name: li.textContent.trim(), score: li.dataset.score || '' };
});
el.classList.add('pdm');
el.setAttribute('role', 'img');
el.setAttribute('aria-label', winners.map(function (w, i) { return (i + 1) + SUFFIX[i] + ': ' + w.name; }).join(', '));
el.innerHTML = '';
var s = { cols: [], bars: [], whos: [], nums: [], parts: [], vis: false, pending: false };
ORDER.forEach(function (rank) {
var w = winners[rank];
var col = document.createElement('li');
col.className = 'pdm-col';
col.setAttribute('aria-hidden', 'true');
col.style.setProperty('--pdm-c', colors[rank] || colors[0]);
col.innerHTML =
'<span class="pdm-who"><span class="pdm-av"></span><span class="pdm-name"></span>' +
(w.score ? '<span class="pdm-score"></span>' : '') + '</span>' +
'<span class="pdm-bar" style="height:' + HEIGHTS[rank] + 'em"><span class="pdm-n"></span></span>';
col.querySelector('.pdm-av').textContent = w.name.charAt(0).toUpperCase();
if (rank === 0) col.querySelector('.pdm-av').insertAdjacentHTML('beforeend', CROWN);
col.querySelector('.pdm-name').textContent = w.name;
if (w.score) col.querySelector('.pdm-score').textContent = w.score;
col.querySelector('.pdm-n').innerHTML = (rank + 1) + '<sup>' + SUFFIX[rank] + '</sup>';
el.appendChild(col);
s.cols[rank] = col;
s.bars[rank] = col.querySelector('.pdm-bar');
s.whos[rank] = col.querySelector('.pdm-who');
s.nums[rank] = col.querySelector('.pdm-n');
if (rank === 0) {
s.crown = col.querySelector('.pdm-crown');
for (var i = 0; i < 18; i++) {
var p = document.createElement('span');
p.className = 'pdm-p';
p.style.background = CONFETTI[i % CONFETTI.length];
col.appendChild(p);
s.parts.push(p);
}
}
});
el._pdm = s;
if (!reduced()) hide(el);
return true;
}
function hide(el) {
var s = el._pdm;
anime.utils.set(s.bars, { scaleY: 0 });
anime.utils.set(s.nums, { opacity: 0, scale: 0.5 });
anime.utils.set(s.whos, { opacity: 0, y: '-6em' });
anime.utils.set(s.crown, { opacity: 0, scale: 0, rotate: -30 });
anime.utils.set(s.parts, { opacity: 0, x: 0, y: 0 });
}
function play(el) {
var s = el._pdm;
if (s.tl) s.tl.pause();
clearTimeout(s.wait);
s.pending = false;
hide(el);
var loop = el.dataset.loop !== undefined && el.dataset.loop !== 'false';
var hold = el.dataset.hold ? +el.dataset.hold : DEFAULTS.hold;
var tl = anime.createTimeline();
// bars rise: second, first, third
[1, 0, 2].forEach(function (rank, i) {
tl.add(s.bars[rank], { scaleY: [0, 1], duration: 750, ease: 'outElastic(1, .75)' }, i * 120)
.add(s.nums[rank], { opacity: [0, 1], scale: [0.5, 1], duration: 420, ease: 'outBack(2)' }, 380 + i * 120);
});
// winners drop in: third, second, then first
var t = 1000;
[2, 1, 0].forEach(function (rank) {
tl.add(s.whos[rank], {
opacity: [{ to: 1, duration: 120 }],
y: [{ to: '0.25em', duration: 420, ease: 'in(3)' }, { to: '0em', duration: 380, ease: 'outElastic(1, .45)' }]
}, t)
.add(s.bars[rank], { scaleY: [1, 0.94, 1], duration: 360, ease: 'out(2)' }, t + 400);
t += rank === 1 ? 760 : 520;
});
var land = t - 520 + 420;
tl.add(s.crown, { opacity: [0, 1], scale: [0, 1], rotate: [-30, 0], y: ['-0.6em', '0em'], duration: 600, ease: 'outBack(2.6)' }, land + 80);
s.parts.forEach(function (p, i) {
var a = -Math.PI / 2 + anime.utils.random(-1.25, 1.25, 2);
var r = anime.utils.random(2.4, 5, 2);
tl.add(p, {
x: [{ to: Math.cos(a) * r + 'em', duration: 700, ease: 'out(3)' }, { to: Math.cos(a) * r * 1.2 + 'em', duration: 700, ease: 'linear' }],
y: [{ to: Math.sin(a) * r + 'em', duration: 700, ease: 'out(3)' }, { to: Math.sin(a) * r + 3 + 'em', duration: 700, ease: 'in(2)' }],
rotate: anime.utils.random(-400, 400),
opacity: [{ to: 1, duration: 40 }, { to: 1, duration: 1000 }, { to: 0, duration: 360 }]
}, land + i * 6);
});
if (loop) {
var out = land + 1400 + hold;
tl.add(s.whos, { opacity: 0, y: '-1em', duration: 360, ease: 'in(2)', delay: anime.stagger(60) }, out)
.add(s.crown, { opacity: 0, duration: 200 }, out)
.add(s.nums, { opacity: 0, duration: 200 }, out + 200)
.add(s.bars, { scaleY: 0, duration: 420, ease: 'in(3)', delay: anime.stagger(70) }, out + 260);
tl.then(function () {
s.wait = setTimeout(function () {
if (s.vis) play(el); else s.pending = true;
}, 400);
});
}
s.tl = tl;
}
window.animacious = window.animacious || {};
window.animacious.podiumReveal = function (el) {
if (el && el._pdm && !reduced()) play(el);
};
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._pdm;
s.vis = e.isIntersecting;
if (!s.vis) return;
if (!s.started) { s.started = true; play(e.target); }
else if (s.pending) play(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-podium-reveal]').forEach(function (el) {
if (el._pdm) return;
if (!setup(el)) return;
if (!reduced()) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><ol data-podium-reveal>
<li data-score="2,480 pts">Nova</li>
<li data-score="2,115 pts">Pixel</li>
<li data-score="1,960 pts">Larry</li>
</ol>