Team Line-up
The starting eleven for a match graphic. The pitch markings draw themselves, then the players run in a line at a time, from the goalkeeper forwards, and slide into their places in the formation. Each shirt bumps as it lands and the name plate wipes in under it.
OkaforSilvaBrandtKimMoreauDialloRossiParkNovakReyesLarsen
How to use it
- Paste the snippet just before
</head>. - Add
data-team-line-upto an element holding the eleven names (one<span>each), goalkeeper first, then each line from the back. - Pick the shape with
data-formation. It fills its container’s width at a pitch’s proportions unless you give it a height; size the names withfont-size.
Options
Add any of these attributes to the same element.
data-formation | Outfield lines from the back, such as 4-4-2 or 3-5-2. Default 4-3-3. |
data-numbers | Comma-separated shirt numbers in the same order. Default 1 to 11. |
data-color | Shirt colour. Default #ef4444; the goalkeeper wears yellow. |
data-team | A team name tag in the corner. |
data-loop | Players run off and line up again, over and over. |
data-hold | How long the line-up stays before looping. Default 3200 ms. |
The pitch lies on its side with your goal on the left, so within each line the names run from top to bottom. Call animacious.teamLineUp(el) to play it again.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Team Line-up: a football pitch draws its lines, then the players run in
// line by line, from the goalkeeper forwards, and slide into their places in
// the formation. Each shirt shows a number, and the name plate wipes in
// under it.
// Usage: <div data-team-line-up data-formation="4-3-3">
// <span>Okafor</span><span>Silva</span>… (11 names)
// </div>
// Give the names goalkeeper first, then each line from the back, each line
// from left to right as you look at it. It fills its container's width at a
// pitch's proportions unless you give it a height. Size names with font-size.
// Optional: data-formation (outfield lines from the back, default "4-3-3"),
// data-numbers (comma-separated shirt numbers, default 1 to 11),
// data-color (shirt colour, default #ef4444), data-team (a team name tag in
// the corner), data-loop (players run off and line up again), data-hold
// (ms the line-up stays before looping, default 3200).
// Script use: animacious.teamLineUp(el) plays it again.
var DEFAULTS = { formation: '4-3-3', color: '#ef4444', hold: 3200 };
var css =
'.tlu{position:relative;overflow:hidden;line-height:1.15;color:#fff;--tlu-c:' + DEFAULTS.color + ';' +
'background:radial-gradient(70% 90% at 50% 50%,rgba(255,255,255,.08),transparent 70%),' +
'repeating-linear-gradient(90deg,rgba(255,255,255,.05) 0 10%,transparent 10% 20%),linear-gradient(#166534,#15803d 50%,#166534)}' +
'.tlu-lines{position:absolute;left:4%;top:4%;width:92%;height:92%;overflow:visible;fill:none;stroke:rgba(255,255,255,.55);stroke-width:1.5}' +
'.tlu-lines *{vector-effect:non-scaling-stroke}' +
'.tlu-team{position:absolute;left:3%;top:4%;z-index:2;display:inline-flex;align-items:center;gap:0.5em;padding:0.3em 0.7em 0.3em 0.35em;border-radius:0.4em;' +
'background:rgba(2,6,23,.75);font-size:0.95em;font-weight:900;letter-spacing:0.06em;text-transform:uppercase}' +
'.tlu-team i{display:block;width:1.1em;height:1.1em;border-radius:0.25em;background:var(--tlu-c)}' +
'.tlu-p{position:absolute;left:0;top:0;width:0;height:0;z-index:1}' +
'.tlu-shirt{position:absolute;left:-1.05em;top:-1.05em;width:2.1em;height:2.1em;display:grid;place-items:center;border-radius:50%;font-weight:900;font-size:1em;' +
'background:radial-gradient(circle at 35% 30%,color-mix(in srgb,var(--tlu-c) 60%,#fff),var(--tlu-c) 70%);border:0.12em solid #fff;box-shadow:0 0.25em 0.6em rgba(0,0,0,.4)}' +
'.tlu-gk .tlu-shirt{background:radial-gradient(circle at 35% 30%,#fef08a,#eab308 70%);color:#422006}' +
'.tlu-name{position:absolute;left:50%;top:1.25em;padding:0.12em 0.45em;border-radius:0.25em;background:rgba(2,6,23,.8);font-size:0.78em;font-weight:700;white-space:nowrap;' +
'transform:translateX(-50%);clip-path:inset(0 100% 0 0)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// Pitch markings in a 105 x 68 box, our goal on the left.
var LINES =
'<svg class="tlu-lines" viewBox="0 0 105 68" preserveAspectRatio="none" aria-hidden="true">' +
'<rect x="0" y="0" width="105" height="68" rx="0.5"/><path d="M52.5 0v68"/><circle cx="52.5" cy="34" r="9.15"/>' +
'<path d="M0 13.85h16.5v40.3H0M0 24.85h5.5v18.3H0M105 13.85H88.5v40.3H105M105 24.85h-5.5v18.3h5.5"/>' +
'<path d="M16.5 26.7a9.15 9.15 0 0 1 0 14.6M88.5 26.7a9.15 9.15 0 0 0 0 14.6"/>' +
'</svg>';
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// Positions in % of the pitch, per line (goalkeeper first).
function layout(formation) {
var lines = [1].concat(formation.split('-').map(function (n) { return Math.max(1, +n || 1); }));
var out = [];
var outfield = lines.length - 1;
lines.forEach(function (count, li) {
var x = li === 0 ? 7 : 22 + (li - 1) * (60 / Math.max(1, outfield - 1));
if (outfield === 1 && li === 1) x = 40;
for (var j = 0; j < count; j++) {
// left to right as you look at it = top to bottom of this sideways pitch
var y = count === 1 ? 50 : 14 + j * (72 / (count - 1));
out.push({ x: x, y: y, line: li });
}
});
return out;
}
function setup(el) {
var d = el.dataset;
var names = [].slice.call(el.children).map(function (c) { return c.textContent.trim(); });
var pos = layout(d.formation || DEFAULTS.formation);
var nums = (d.numbers || '').split(',').map(function (s) { return s.trim(); });
el.classList.add('tlu');
el.style.setProperty('--tlu-c', d.color || DEFAULTS.color);
if (!el.style.height && el.clientHeight < 20) el.style.aspectRatio = '105 / 68';
el.setAttribute('role', 'img');
el.setAttribute('aria-label', (d.team ? d.team + ' ' : '') + 'line-up, ' + (d.formation || DEFAULTS.formation) + ': ' + names.join(', '));
el.innerHTML = LINES + (d.team ? '<span class="tlu-team" aria-hidden="true"><i></i><span></span></span>' : '');
if (d.team) el.querySelector('.tlu-team span').textContent = d.team;
var players = pos.map(function (p, i) {
var div = document.createElement('div');
div.className = 'tlu-p' + (i === 0 ? ' tlu-gk' : '');
div.setAttribute('aria-hidden', 'true');
div.style.left = (4 + p.x * 0.92) + '%';
div.style.top = (4 + p.y * 0.92) + '%';
div.innerHTML = '<span class="tlu-shirt"></span>' + (names[i] ? '<span class="tlu-name"></span>' : '');
div.querySelector('.tlu-shirt').textContent = nums[i] || String(i + 1);
if (names[i]) div.querySelector('.tlu-name').textContent = names[i];
el.appendChild(div);
return { el: div, shirt: div.querySelector('.tlu-shirt'), name: div.querySelector('.tlu-name'), line: p.line };
});
var shapes = [].slice.call(el.querySelectorAll('.tlu-lines rect, .tlu-lines path, .tlu-lines circle'));
el._tlu = { players: players, shapes: shapes, team: el.querySelector('.tlu-team'), vis: false };
if (reduced()) {
players.forEach(function (p) { if (p.name) p.name.style.clipPath = 'none'; });
return true;
}
hide(el);
return true;
}
function hide(el) {
var s = el._tlu;
s.players.forEach(function (p) {
anime.utils.set(p.el, { opacity: 0, x: -el.clientWidth * 0.5 });
if (p.name) p.name.style.clipPath = 'inset(0 100% 0 0)';
});
if (s.team) anime.utils.set(s.team, { opacity: 0, x: '-1em' });
}
function play(el) {
var s = el._tlu;
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();
if (!s.drawn) {
s.drawn = true;
tl.add(anime.svg.createDrawable(s.shapes), { draw: ['0 0', '0 1'], duration: 900, ease: 'inOut(2)', delay: anime.stagger(60) }, 0);
}
if (s.team) tl.add(s.team, { opacity: [0, 1], x: ['-1em', '0em'], duration: 500, ease: 'out(3)' }, 200);
var W = el.clientWidth;
var start = 500;
s.players.forEach(function (p, i) {
var at = start + p.line * 260 + i * 40;
tl.add(p.el, { opacity: [{ to: 1, duration: 150 }], x: [-W * 0.5, 0], duration: 800, ease: 'out(4)' }, at)
.add(p.shirt, { scale: [0.8, 1.15, 1], duration: 450, ease: 'out(2)' }, at + 620);
if (p.name) tl.add(p.name, { clipPath: ['inset(0 100% 0 0)', 'inset(0 0% 0 0)'], duration: 380, ease: 'out(3)' }, at + 700);
});
if (loop) {
var last = start + s.players[s.players.length - 1].line * 260 + s.players.length * 40 + 1100;
var out = last + hold;
tl.add(s.players.map(function (p) { return p.el; }), { x: W * 0.6, opacity: 0, duration: 600, ease: 'in(3)', delay: anime.stagger(25) }, out);
if (s.team) tl.add(s.team, { opacity: 0, duration: 300 }, out);
tl.then(function () {
s.wait = setTimeout(function () {
if (s.vis) play(el); else s.pending = true;
}, 300);
});
}
s.tl = tl;
}
window.animacious = window.animacious || {};
window.animacious.teamLineUp = function (el) {
if (el && el._tlu && !reduced()) play(el);
};
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._tlu;
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-team-line-up]').forEach(function (el) {
if (el._tlu) return;
if (!setup(el)) return;
if (!reduced()) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-team-line-up data-formation="4-3-3" data-team="Rovers">
<span>Okafor</span>
<span>Silva</span> <span>Brandt</span> <span>Kim</span> <span>Moreau</span>
<span>Diallo</span> <span>Rossi</span> <span>Park</span>
<span>Novak</span> <span>Reyes</span> <span>Larsen</span>
</div>