← Hover & cursor

3D Tilt Card

A card leans towards the pointer like a real object in your hand, with a soft glare that follows it, then springs back when the pointer leaves.

Move your pointer over me

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-tilt-card to any card, image or panel.

Options

Add any of these attributes to the same element.

data-maxLargest tilt, in degrees. Default 12.
data-scaleHow much it grows while hovered. Default 1.03.
data-glare="false"Turn off the moving glare.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // 3D Tilt Card: a card leans towards the pointer like a physical object, with
    // a soft glare that follows it, then springs back when the pointer leaves.
    // Usage: <div class="card" data-tilt-card>…</div>
    // Optional: data-max (degrees, default 12), data-scale (hover scale, default
    // 1.03), data-glare="false" to turn the glare off.
    var DEFAULTS = { max: 12, scale: 1.03 };

    var css =
      '.tc-glare{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;' +
      'background:radial-gradient(circle at var(--tc-x,50%) var(--tc-y,50%),rgba(255,255,255,.55),rgba(255,255,255,0) 60%);mix-blend-mode:soft-light}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function setup(el) {
      var d = el.dataset;
      var max = +d.max || DEFAULTS.max;
      var scale = +d.scale || DEFAULTS.scale;
      if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
      var glare = null;
      if (d.glare !== 'false') {
        glare = document.createElement('div');
        glare.className = 'tc-glare';
        el.appendChild(glare);
      }
      anime.utils.set(el, { perspective: '900px' });

      el.addEventListener('pointermove', function (e) {
        var r = el.getBoundingClientRect();
        var px = (e.clientX - r.left) / r.width;   // 0 … 1
        var py = (e.clientY - r.top) / r.height;
        anime.animate(el, {
          rotateY: (px - 0.5) * 2 * max,
          rotateX: (0.5 - py) * 2 * max,
          scale: scale,
          duration: 500,
          ease: 'out(3)'
        });
        if (glare) {
          glare.style.setProperty('--tc-x', px * 100 + '%');
          glare.style.setProperty('--tc-y', py * 100 + '%');
          anime.animate(glare, { opacity: 1, duration: 300 });
        }
      });
      el.addEventListener('pointerleave', function () {
        anime.animate(el, { rotateX: 0, rotateY: 0, scale: 1, duration: 1200, ease: 'outElastic(1, .5)' });
        if (glare) anime.animate(glare, { opacity: 0, duration: 400 });
      });
    }

    function init() {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      document.querySelectorAll('[data-tilt-card]').forEach(function (el) {
        if (el._tc) return;
        el._tc = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div class="card" data-tilt-card>
  <img src="product.jpg" alt="">
  <h3>Product</h3>
</div>