← Broadcast

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

  1. Paste the snippet just before </head>.
  2. Add data-team-line-up to an element holding the eleven names (one <span> each), goalkeeper first, then each line from the back.
  3. 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 with font-size.

Options

Add any of these attributes to the same element.

data-formationOutfield lines from the back, such as 4-4-2 or 3-5-2. Default 4-3-3.
data-numbersComma-separated shirt numbers in the same order. Default 1 to 11.
data-colorShirt colour. Default #ef4444; the goalkeeper wears yellow.
data-teamA team name tag in the corner.
data-loopPlayers run off and line up again, over and over.
data-holdHow 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.

1 · Snippet
<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>
2 · Your HTML
<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>