← Broadcast

Versus Card

A face-off card for match-ups and battles. Two slanted panels race in from opposite sides and clash in the middle. The card shakes, a flash bursts from the seam and a big “VS” badge slams down with a ring of sparks, then each side’s name slides in.

NovaTeam Red
PixelTeam Blue

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-versus-card to an element with two children, one for each side. Give each a name in <strong> and, if you like, a smaller line after it.
  3. It fills the width of its container; size it with font-size.

Options

Add any of these attributes to the same element.

data-left-color, data-right-colorSide colours. Default #ef4444 and #3b82f6.
data-labelBadge text. Default “VS”.
data-loopSlide apart and clash again, over and over.
data-holdHow long it stays together before looping. Default 2600 ms.
data-gapTime apart between loops. Default 500 ms.

Call animacious.versusCard(el) to play it again. Each side gets a round badge with its initials.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Versus Card: two sides slide in from opposite edges and clash in the
    // middle. The card shakes on impact, a white flash bursts from the seam and
    // a big "VS" badge slams down with a ring of sparks.
    // Usage: <div data-versus-card>
    //          <div><strong>Nova</strong><span>Team Red</span></div>
    //          <div><strong>Pixel</strong><span>Team Blue</span></div>
    //        </div>
    // Each of the first two children is one side: a name, then an optional
    // smaller line. It fills the width of its container; size it with font-size.
    // Optional: data-left-color (default #ef4444), data-right-color (default
    // #3b82f6), data-label (badge text, default "VS"), data-loop (slide apart
    // and clash again, over and over), data-hold (ms it stays together before
    // looping, default 2600), data-gap (ms apart between loops, default 500).
    // Script use: animacious.versusCard(el) plays it again.
    var DEFAULTS = { left: '#ef4444', right: '#3b82f6', label: 'VS', hold: 2600, gap: 500 };
    var SPARKS = 12;

    var css =
      '.vsc{position:relative;display:block;width:100%;line-height:1.15;color:#fff;--vsc-l:' + DEFAULTS.left + ';--vsc-r:' + DEFAULTS.right + '}' +
      '.vsc-track{position:relative;display:flex;overflow:hidden;border-radius:0.5em}' +
      '.vsc-side{position:relative;flex:1 1 50%;min-width:0;display:flex;align-items:center;gap:0.7em;padding:1em 2.1em 1em 1.1em}' +
      '.vsc-side-l{margin-right:-0.9em;clip-path:polygon(0 0,100% 0,calc(100% - 1.8em) 100%,0 100%);' +
        'background:linear-gradient(100deg,color-mix(in srgb,var(--vsc-l) 55%,#000),var(--vsc-l))}' +
      '.vsc-side-r{margin-left:-0.9em;flex-direction:row-reverse;text-align:right;padding:1em 1.1em 1em 2.1em;clip-path:polygon(1.8em 0,100% 0,100% 100%,0 100%);' +
        'background:linear-gradient(260deg,color-mix(in srgb,var(--vsc-r) 55%,#000),var(--vsc-r))}' +
      '.vsc-side::after{content:"";position:absolute;inset:0;background:repeating-linear-gradient(115deg,rgba(255,255,255,.07) 0 0.25em,transparent 0.25em 0.9em);pointer-events:none}' +
      '.vsc-av{position:relative;z-index:1;flex:none;display:grid;place-items:center;width:2.4em;height:2.4em;border-radius:50%;font-weight:900;font-size:1em;' +
        'background:rgba(255,255,255,.18);border:0.12em solid rgba(255,255,255,.75);box-shadow:0 0.2em 0.6em rgba(0,0,0,.3)}' +
      '.vsc-txt{position:relative;z-index:1;display:flex;flex-direction:column;gap:0.15em;min-width:0}' +
      '.vsc-txt strong{font-size:1.35em;font-weight:900;letter-spacing:0.01em;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-shadow:0 0.08em 0.25em rgba(0,0,0,.35)}' +
      '.vsc-txt span{font-size:0.72em;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;opacity:.82;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
      '.vsc-flash{position:absolute;left:50%;top:50%;width:1.2em;height:160%;margin:-80% 0 0 -0.6em;background:linear-gradient(90deg,transparent,#fff,transparent);transform:skewX(-20deg);opacity:0;pointer-events:none}' +
      '.vsc-mid{position:absolute;left:50%;top:50%;width:0;height:0;z-index:2;pointer-events:none}' +
      '.vsc-badge{position:absolute;left:-1.45em;top:-1.45em;width:2.9em;height:2.9em;display:grid;place-items:center;border-radius:50%;' +
        'background:radial-gradient(circle at 35% 30%,#fff,#e5e7eb 60%,#9ca3af);color:#111827;font-weight:900;font-style:italic;font-size:1em;letter-spacing:-0.04em;' +
        'box-shadow:0 0 0 0.16em #111827,0 0 0 0.3em rgba(255,255,255,.85),0 0.4em 1.2em rgba(0,0,0,.5)}' +
      '.vsc-badge b{font-size:1.25em;line-height:1}' +
      '.vsc-ring{position:absolute;left:-1.45em;top:-1.45em;width:2.9em;height:2.9em;border-radius:50%;border:0.16em solid #fff;opacity:0}' +
      '.vsc-sp{position:absolute;left:-0.09em;top:-0.55em;width:0.18em;height:1.1em;border-radius:0.1em;background:#fde68a;opacity:0}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function num(el, k) { var v = el.dataset[k]; return v !== undefined && v !== '' ? +v : DEFAULTS[k]; }
    function initials(name) {
      return name.split(/\s+/).filter(Boolean).slice(0, 2).map(function (w) { return w.charAt(0); }).join('').toUpperCase();
    }

    function side(src, cls) {
      var name = src.querySelector('strong, b, h1, h2, h3') || src.firstElementChild;
      var nameText = (name ? name.textContent : src.textContent).trim();
      var subEl = name ? name.nextElementSibling : null;
      var sub = subEl ? subEl.textContent.trim() : '';
      var div = document.createElement('div');
      div.className = 'vsc-side ' + cls;
      div.innerHTML = '<span class="vsc-av"></span><span class="vsc-txt"><strong></strong>' + (sub ? '<span></span>' : '') + '</span>';
      div.querySelector('.vsc-av').textContent = initials(nameText);
      div.querySelector('strong').textContent = nameText;
      if (sub) div.querySelector('.vsc-txt span').textContent = sub;
      return { el: div, name: nameText };
    }

    function setup(el) {
      var d = el.dataset;
      var kids = el.children;
      if (kids.length < 2) return false;
      var a = side(kids[0], 'vsc-side-l');
      var b = side(kids[1], 'vsc-side-r');
      var label = d.label || DEFAULTS.label;
      el.classList.add('vsc');
      el.style.setProperty('--vsc-l', d.leftColor || DEFAULTS.left);
      el.style.setProperty('--vsc-r', d.rightColor || DEFAULTS.right);
      el.setAttribute('role', 'img');
      el.setAttribute('aria-label', a.name + ' ' + label.toLowerCase() + ' ' + b.name);
      el.innerHTML = '';
      var track = document.createElement('div');
      track.className = 'vsc-track';
      track.setAttribute('aria-hidden', 'true');
      track.appendChild(a.el);
      track.appendChild(b.el);
      track.insertAdjacentHTML('beforeend', '<span class="vsc-flash"></span>');
      var mid = document.createElement('div');
      mid.className = 'vsc-mid';
      mid.setAttribute('aria-hidden', 'true');
      var sparks = '';
      for (var i = 0; i < SPARKS; i++) sparks += '<span class="vsc-sp"></span>';
      mid.innerHTML = '<span class="vsc-ring"></span>' + sparks + '<span class="vsc-badge"><b></b></span>';
      mid.querySelector('b').textContent = label;
      el.appendChild(track);
      el.appendChild(mid);
      el._vsc = {
        track: track, l: a.el, r: b.el,
        lTxt: a.el.querySelector('.vsc-txt'), rTxt: b.el.querySelector('.vsc-txt'),
        lAv: a.el.querySelector('.vsc-av'), rAv: b.el.querySelector('.vsc-av'),
        flash: track.querySelector('.vsc-flash'), badge: mid.querySelector('.vsc-badge'),
        ring: mid.querySelector('.vsc-ring'), sparks: [].slice.call(mid.querySelectorAll('.vsc-sp')),
        vis: false, pending: false
      };
      if (!reduced()) hide(el);
      return true;
    }

    function hide(el) {
      var s = el._vsc;
      anime.utils.set(s.l, { x: '-105%' });
      anime.utils.set(s.r, { x: '105%' });
      anime.utils.set([s.lTxt, s.rTxt, s.lAv, s.rAv], { opacity: 0 });
      anime.utils.set(s.badge, { opacity: 0, scale: 3 });
    }

    function play(el) {
      var s = el._vsc;
      if (s.tl) s.tl.pause();
      clearTimeout(s.wait);
      s.pending = false;
      var loop = el.dataset.loop !== undefined && el.dataset.loop !== 'false';
      var hold = num(el, 'hold');
      hide(el);
      anime.utils.set([s.l, s.r, s.track], { rotate: 0, y: 0, scale: 1 });
      anime.utils.set(s.lTxt, { x: '-1.5em' });
      anime.utils.set(s.rTxt, { x: '1.5em' });
      anime.utils.set(s.ring, { scale: 1, opacity: 0 });
      anime.utils.set(s.flash, { opacity: 0, scaleX: 1 });
      anime.utils.set(s.sparks, { opacity: 0, x: 0, y: 0, scaleY: 1 });

      var hit = 520;
      var tl = anime.createTimeline();
      tl.add(s.l, { x: ['-105%', '0%'], duration: hit, ease: 'in(3)' }, 0)
        .add(s.r, { x: ['105%', '0%'], duration: hit, ease: 'in(3)' }, 0)
        // impact: both sides recoil a touch and the card shakes
        .add(s.l, { x: [{ to: '-3%', duration: 110, ease: 'out(3)' }, { to: '0%', duration: 380, ease: 'outElastic(1, .5)' }] }, hit)
        .add(s.r, { x: [{ to: '3%', duration: 110, ease: 'out(3)' }, { to: '0%', duration: 380, ease: 'outElastic(1, .5)' }] }, hit)
        .add(s.track, { y: ['0em', '0.12em', '-0.1em', '0.06em', '0em'], rotate: [0, -0.8, 0.6, -0.3, 0], duration: 360, ease: 'linear' }, hit)
        .add(s.flash, { opacity: [1, 0], scaleX: [1, 5], duration: 420, ease: 'out(3)' }, hit)
        .add(s.badge, { opacity: [0, 1], scale: [3, 1], rotate: [-25, 0], duration: 420, ease: 'outBack(2)' }, hit + 40)
        .add(s.ring, { opacity: [0.9, 0], scale: [1, 2.6], duration: 650, ease: 'out(3)' }, hit + 200);
      s.sparks.forEach(function (sp, i) {
        var a = (i / SPARKS) * 360 + anime.utils.random(-10, 10);
        var r = anime.utils.random(2.2, 3.4, 2);
        anime.utils.set(sp, { rotate: a + 90 });
        tl.add(sp, {
          x: Math.cos(a * Math.PI / 180) * r + 'em',
          y: Math.sin(a * Math.PI / 180) * r + 'em',
          scaleY: [1.2, 0.2],
          opacity: [{ to: 1, duration: 40 }, { to: 1, duration: 260 }, { to: 0, duration: 200 }],
          duration: 500, ease: 'out(3)'
        }, hit + 220);
      });
      tl.add([s.lAv, s.rAv], { opacity: [0, 1], scale: [0.4, 1], duration: 500, ease: 'outBack(2.4)' }, hit + 160)
        .add(s.lTxt, { opacity: [0, 1], x: ['-1.5em', '0em'], duration: 520, ease: 'out(4)' }, hit + 240)
        .add(s.rTxt, { opacity: [0, 1], x: ['1.5em', '0em'], duration: 520, ease: 'out(4)' }, hit + 240)
        // a small pulse on the badge while it holds
        .add(s.badge, { scale: [1, 1.12, 1], duration: 700, ease: 'inOut(2)' }, hit + 1300);
      if (loop) {
        var out = hit + 900 + hold;
        tl.add(s.badge, { opacity: 0, scale: 0.4, rotate: 20, duration: 300, ease: 'in(3)' }, out)
          .add([s.lTxt, s.rTxt, s.lAv, s.rAv], { opacity: 0, duration: 200 }, out)
          .add(s.l, { x: '-105%', duration: 480, ease: 'in(3)' }, out + 160)
          .add(s.r, { x: '105%', duration: 480, ease: 'in(3)' }, out + 160);
        tl.then(function () {
          s.wait = setTimeout(function () {
            if (s.vis) play(el); else s.pending = true;
          }, num(el, 'gap'));
        });
      }
      s.tl = tl;
    }

    window.animacious = window.animacious || {};
    window.animacious.versusCard = function (el) {
      if (el && el._vsc && !reduced()) play(el);
    };

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var s = e.target._vsc;
          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-versus-card]').forEach(function (el) {
        if (el._vsc) 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-versus-card>
  <div><strong>Nova</strong><span>Team Red</span></div>
  <div><strong>Pixel</strong><span>Team Blue</span></div>
</div>