← Buttons

3D Flip Button

Hover the button and it tips over in 3D, like a card turning on a hinge through its middle, to show a second label on its dark back. Move away and it flips back. On a phone a tap flips it over and it flips back a moment later.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-flip-button to a button, with the label for its back in data-back.

Options

Add any of these attributes to the same element.

data-backLabel on the back. Default the front label.
data-colorColour of the front. Default #6a0dad.
data-back-colorColour of the back. Default #18181b.
data-axisx to tip over top to bottom, or y to turn side to side. Default x.
data-durationHow long a flip takes. Default 700 ms.
data-autoplayIt flips over and back every couple of seconds while in view, until someone uses it. Handy for demos.

The button takes the size of its longer label, so neither face is cut off. Screen readers hear the front label. Keyboard focus flips it too. Size it with font-size.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // 3D Flip Button: hover the button and it flips over in 3D, like a card
    // on a hinge through its middle, to show a second label on its back.
    // Move away and it flips back. On touch screens a tap flips it over and
    // it flips back a moment later. Keyboard focus flips it too.
    // Usage: <button data-flip-button data-back="Let's go →">Get started</button>
    // Optional: data-back (label on the back, default the front label),
    // data-color (front colour, default #6a0dad), data-back-color (back
    // colour, default #18181b), data-axis ("x" to tip over top to bottom,
    // the default, or "y" to turn side to side), data-duration (ms, default
    // 700), data-autoplay (it flips over and back by itself every couple of
    // seconds while in view, until someone uses it; handy for demos).
    var DEFAULTS = { color: '#6a0dad', back: '#18181b', axis: 'x', duration: 700, every: 1800 };

    var css =
      '.fbn{position:relative;display:inline-grid;padding:0;border:0;background:none;font:inherit;font-weight:700;color:#fff;cursor:pointer;' +
      'perspective:40em;-webkit-tap-highlight-color:transparent}' +
      '.fbn:focus-visible{outline:none}' +
      '.fbn:focus-visible .fbn-face{outline:3px solid var(--fbn-c);outline-offset:3px}' +
      '.fbn-inner{display:grid;transform-style:preserve-3d}' +
      '.fbn-face{grid-area:1/1;display:flex;align-items:center;justify-content:center;gap:.4em;white-space:nowrap;' +
      'padding:.85em 1.7em;border-radius:.7em;-webkit-backface-visibility:hidden;backface-visibility:hidden}' +
      '.fbn-front{background:var(--fbn-c);box-shadow:0 .5em 1.3em color-mix(in srgb,var(--fbn-c) 30%,transparent),inset 0 1px 0 rgba(255,255,255,.22)}' +
      '.fbn-back{background:var(--fbn-b);box-shadow:0 .5em 1.3em rgba(15,15,30,.25),inset 0 1px 0 rgba(255,255,255,.12)}' +
      '.fbn-x .fbn-back{transform:rotateX(180deg)}' +
      '.fbn-y .fbn-back{transform:rotateY(180deg)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function setup(el) {
      var d = el.dataset;
      var axis = d.axis === 'y' ? 'y' : DEFAULTS.axis;
      var prop = axis === 'y' ? 'rotateY' : 'rotateX';
      var duration = +d.duration || DEFAULTS.duration;
      var frontText = el.innerHTML.trim();
      var backText = d.back || el.textContent.trim();
      el.classList.add('fbn', 'fbn-' + axis);
      el.style.setProperty('--fbn-c', d.color || DEFAULTS.color);
      el.style.setProperty('--fbn-b', d.backColor || DEFAULTS.back);
      el.setAttribute('aria-label', el.textContent.trim());
      el.innerHTML = '';
      var inner = document.createElement('span');
      inner.className = 'fbn-inner';
      inner.setAttribute('aria-hidden', 'true');
      var front = document.createElement('span');
      front.className = 'fbn-face fbn-front';
      front.innerHTML = frontText;
      var back = document.createElement('span');
      back.className = 'fbn-face fbn-back';
      back.textContent = backText;
      inner.appendChild(front);
      inner.appendChild(back);
      el.appendChild(inner);
      var over = false;

      function flip(state) {
        if (state === over) return;
        over = state;
        var props = { duration: reduced() ? 0 : duration, ease: 'outBack(1.3)' };
        props[prop] = state ? 180 : 0;
        anime.animate(inner, props);
        if (!reduced()) anime.animate(inner, { scale: [1, 0.92, 1], duration: duration * 0.8, ease: 'inOut(2)' });
      }

      var userTouched = false, touchTimer = null;
      el.addEventListener('pointerenter', function (e) {
        if (e.pointerType === 'touch') return;
        userTouched = true;
        flip(true);
      });
      el.addEventListener('pointerleave', function (e) {
        if (e.pointerType === 'touch') return;
        flip(false);
      });
      el.addEventListener('pointerdown', function (e) {
        if (e.pointerType !== 'touch') return;
        userTouched = true;
        clearTimeout(touchTimer);
        flip(true);
        touchTimer = setTimeout(function () { flip(false); }, 1600);
      });
      el.addEventListener('focus', function () { if (el.matches(':focus-visible')) flip(true); });
      el.addEventListener('blur', function () { if (!el.matches(':hover')) flip(false); });

      if (!reduced() && d.autoplay !== undefined && d.autoplay !== 'false') {
        var timer = null;
        var loop = function () {
          if (userTouched) return;
          flip(!over);
          timer = setTimeout(loop, DEFAULTS.every);
        };
        new IntersectionObserver(function (entries) {
          clearTimeout(timer);
          if (entries[0].isIntersecting) timer = setTimeout(loop, 700);
        }).observe(el);
      }
    }

    function init() {
      document.querySelectorAll('[data-flip-button]').forEach(function (el) {
        if (el._fbn) return;
        el._fbn = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<button data-flip-button data-back="Let's go →">Get started</button>

<!-- turning side to side, in pink: -->
<button data-flip-button data-axis="y" data-color="#db2777" data-back="£9 a month">See pricing</button>