← AI

Translation Flip

Text turns over line by line into another language, each line flipping on its axis a beat after the one above, like a departures board. A small label flips along with it to name the language, and with several translations it keeps going round them all.

Hello and welcome

Lovely to see you

Shall we begin?

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-translation-flip to a box. Each child element is a line.
  3. Give each line its translation in data-to, or several separated by |.

Options

Add any of these attributes to the same element.

data-toOn each line: its translations, separated by |.
data-langsLanguage names separated by |, starting with the original. Shows them in a label that flips too.
data-intervalHow long each language shows. Default 2400 ms.
data-staggerGap between lines flipping. Default 140 ms.
data-durationHow long each flip takes. Default 700 ms.
data-loopKeep going round. Without it, it stops on the last language.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Translation Flip: text turns over line by line into another language,
    // each line flipping on its horizontal axis a beat after the one above,
    // like a departures board. Give it more than one translation and it keeps
    // flipping through them all.
    // Usage: <div data-translation-flip data-langs="English|Français">
    //          <p data-to="Bonjour et bienvenue">Hello and welcome</p>
    //          <p data-to="Ravi de vous voir">Lovely to see you</p>
    //        </div>
    // Each child element is a line; its data-to holds its translations,
    // separated by | when there are several.
    // Optional: data-langs (language names, separated by |, starting with the
    // original; shown in a small label that flips too), data-interval (ms each
    // language shows, default 2400), data-stagger (ms between lines, default
    // 140), data-duration (ms each flip takes, default 700), data-loop (keep
    // going round; without it, it stops on the last language).
    var DEFAULTS = { interval: 2400, stagger: 140, duration: 700 };

    var css =
      '.tfl-line{perspective:700px}' +
      '.tfl-card{display:grid;transform-style:preserve-3d;transform-origin:50% 50%}' +
      '.tfl-face{grid-area:1/1;backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
      '.tfl-b{transform:rotateX(180deg)}' +
      '.tfl-lang{display:inline-flex;align-items:center;gap:.45em;margin-bottom:.7em;padding:.2em .7em;border-radius:999px;font-size:.7em;' +
      'font-weight:650;letter-spacing:.03em;line-height:1.6;color:#7c3aed;background:rgba(124,58,237,.1)}' +
      '.tfl-lang svg{width:1.1em;height:1.1em;flex:none}' +
      '.tfl-lang .tfl-card{min-width:0}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var GLOBE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">' +
      '<path d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM3 12h18M12 3c2.5 2.6 3.7 5.6 3.7 9s-1.2 6.4-3.7 9c-2.5-2.6-3.7-5.6-3.7-9S9.5 5.6 12 3z"/></svg>';

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    // Turn an element into a two-faced card holding the given versions.
    function card(host, versions, firstHtml) {
      var c = document.createElement('span');
      c.className = 'tfl-card';
      c.innerHTML = '<span class="tfl-face tfl-a"></span><span class="tfl-face tfl-b" aria-hidden="true"></span>';
      var a = c.firstChild;
      if (firstHtml !== undefined) a.innerHTML = firstHtml; else a.textContent = versions[0];
      host.appendChild(c);
      return { card: c, faces: [a, c.lastChild], versions: versions, first: firstHtml, angle: 0 };
    }

    function show(face, f, i) {
      if (i === 0 && f.first !== undefined) face.innerHTML = f.first;
      else face.textContent = f.versions[i] !== undefined ? f.versions[i] : f.versions[0];
    }

    function setup(el) {
      var d = el.dataset;
      var lines = [].slice.call(el.children);
      var flips = [], n = 1;
      lines.forEach(function (line) {
        var tr = (line.dataset.to || '').split('|').map(function (t) { return t.trim(); });
        var html = line.innerHTML;
        var versions = [line.textContent.trim()].concat(tr);
        n = Math.max(n, versions.length);
        line.classList.add('tfl-line');
        line.textContent = '';
        flips.push(card(line, versions, html));
      });
      if (d.langs) {
        var names = d.langs.split('|').map(function (t) { return t.trim(); });
        var lab = document.createElement('div');
        lab.className = 'tfl-lang tfl-line';
        lab.innerHTML = GLOBE;
        el.insertBefore(lab, el.firstChild);
        var f = card(lab, names);
        f.label = true;
        flips.unshift(f);
      }
      el._tfl = { flips: flips, n: n, idx: 0, timer: 0 };
    }

    function step(el) {
      var st = el._tfl, d = el.dataset;
      var next = (st.idx + 1) % st.n;
      var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
      var dur = +d.duration || DEFAULTS.duration;
      st.idx = next;
      st.flips.forEach(function (f, i) {
        // Write the next language on the face that's turned away, then flip.
        var shown = Math.round(f.angle / 180) % 2;
        var hidden = f.faces[1 - shown];
        show(hidden, f, next);
        f.angle += 180;
        hidden.removeAttribute('aria-hidden');
        f.faces[shown].setAttribute('aria-hidden', 'true');
        anime.animate(f.card, { rotateX: f.angle, duration: dur, delay: i * stagger, ease: 'inOut(3)' });
      });
    }

    function cycle(el) {
      var st = el._tfl, d = el.dataset;
      var interval = d.interval !== undefined ? +d.interval : DEFAULTS.interval;
      if (st.n < 2) return;
      st.timer = setTimeout(function () {
        if (st.idx === st.n - 1 && d.loop === undefined) return;
        step(el);
        cycle(el);
      }, interval);
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          cycle(e.target);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-translation-flip]').forEach(function (el) {
        if (el._tfl) return;
        setup(el);
        if (reduced()) return;
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-translation-flip data-langs="English|Français" data-loop>
  <p data-to="Bonjour et bienvenue">Hello and welcome</p>
  <p data-to="Ravi de vous voir">Lovely to see you</p>
</div>