← Attention

Flip Once

The element lifts a little, flips right over in 3D like a tossed coin, and lands back where it started with a small bounce.

+50 coins

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-flip-once to any element. It flips once when it scrolls into view.
  3. Add data-loop to flip again every so often.

Options

Add any of these attributes to the same element.

data-axisy turns it sideways, x tumbles it forwards. Default y.
data-perspectiveHow deep the 3D looks, in px: smaller is more dramatic. Default 600.
data-durationHow long the flip takes. Default 1000 ms.
data-delayWait before the first flip. Default 0 ms.
data-loopFlip again every so often while it’s on screen.
data-everyRest between flips, when looping. Default 1600 ms.
data-hoverAlso play it when the pointer moves over the element or it’s tapped.

Halfway round you see the element from behind, mirrored, so it works best on things that look good either way, like icons and coins. Call animacious.flipOnce(el) to flip it from your own code.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Flip Once: the element lifts, flips over once in 3D and lands back where
    // it started, with a little bounce.
    // Usage: <span data-flip-once>…</span>
    // Optional: data-axis ("y", default, turns sideways; "x" tumbles forwards),
    // data-perspective (px, default 600: smaller looks deeper), data-duration
    // (ms, default 1000), data-delay (ms before the first flip), data-loop (flip
    // again every so often while in view), data-every (ms of rest between loops,
    // default 1600), data-hover (also flip on hover or tap).
    // Script use: animacious.flipOnce(el) flips it from code.
    var DEFAULTS = { perspective: 600, duration: 1000, delay: 0, every: 1600 };

    function on(v) { return v !== undefined && v !== 'false'; }
    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function play(el, done) {
      var s = el._flo;
      if (s.busy) { if (done) s.after = done; return; }
      if (reduced()) { if (done) done(); return; }
      s.busy = true;
      var d = el.dataset;
      var axis = d.axis === 'x' ? 'rotateX' : 'rotateY';
      var persp = +d.perspective || DEFAULTS.perspective;
      var p = { a: 0, lift: 0 };
      function draw() {
        // The element rises a little and grows as it turns, then drops back.
        el.style.transform = 'perspective(' + persp + 'px) translateY(' + (-p.lift * 10).toFixed(2) + 'px) scale(' +
          (1 + p.lift * 0.08).toFixed(3) + ') ' + axis + '(' + p.a.toFixed(2) + 'deg)';
      }
      function finish() {
        s.busy = false;
        el.style.transform = '';
        var after = s.after;
        s.after = null;
        if (done) done();
        if (after) after();
      }
      var dur = +d.duration || DEFAULTS.duration;
      anime.animate(p, { a: 360, duration: dur, ease: 'inOut(3)', onUpdate: draw, onComplete: finish });
      anime.animate(p, {
        keyframes: [
          { lift: 1, duration: dur * 0.45, ease: 'out(2)' },
          { lift: -0.25, duration: dur * 0.4, ease: 'in(2)' },
          { lift: 0, duration: dur * 0.15, ease: 'out(2)' }
        ]
      });
    }

    function cycle(el) {
      var s = el._flo;
      play(el, function () {
        if (!on(el.dataset.loop)) return;
        clearTimeout(s.timer);
        s.timer = setTimeout(function () {
          if (s.visible) cycle(el);
          else s.waiting = true;
        }, +el.dataset.every || DEFAULTS.every);
      });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var s = e.target._flo;
          s.visible = e.isIntersecting;
          if (!e.isIntersecting) return;
          if (!s.started) {
            s.started = true;
            setTimeout(function () { cycle(e.target); }, +e.target.dataset.delay || DEFAULTS.delay);
          } else if (s.waiting) {
            s.waiting = false;
            cycle(e.target);
          }
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-flip-once]').forEach(function (el) {
        if (el._flo) return;
        el._flo = {};
        if (on(el.dataset.hover)) {
          el.addEventListener('pointerenter', function () { play(el); });
          el.addEventListener('pointerdown', function () { play(el); });
        }
        if (!reduced()) io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.flipOnce = function (el) { if (el._flo) play(el); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<span class="coin" data-flip-once>…</span>

<!-- or tumbling forwards, repeating: -->
<span class="coin" data-flip-once data-axis="x" data-loop>…</span>