← Text effects

Holographic Foil Text

The letters look stamped in rainbow foil, like a trading card or a gift wrap. Move the pointer, scroll the page or tilt your phone and the colours slide across them, a bright glare follows the light and the text tips gently towards you.

Holographic

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-holographic-foil-text to a big, bold heading. The foil shows best on a dark background.

Options

Add any of these attributes to the same element.

data-tiltHow far the text tips with the light, in degrees. Default 8; 0 keeps it flat.
data-colorsFoil colours, separated by commas. Default a pastel rainbow.
data-autoplayLet the light drift around by itself whenever no real pointer is moving. Handy for demos and touch screens.

On most Android phones the foil also follows how the phone is held. iPhones ask permission for that, so there it follows touches and scrolling.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Holographic Foil Text: the letters look stamped in rainbow foil. The
    // colours slide across them, a bright glare follows the light and the text
    // tilts a little, all steered by the pointer, the page scroll and (on
    // phones that allow it) the way the device is held.
    // Usage: <h1 data-holographic-foil-text>Holographic</h1>
    // Optional: data-tilt (how far the text tilts, in degrees; default 8; 0
    // keeps it flat), data-colors (comma-separated foil colours; default a
    // pastel rainbow), data-autoplay (the light drifts around by itself while
    // no real pointer is moving; handy for demos and touch screens).
    var DEFAULTS = {
      tilt: 8,
      colors: ['#ff8fcf', '#ffd879', '#9dffbf', '#7fd4ff', '#b79bff', '#ff8fcf']
    };

    var css =
      '.hof-face{display:inline-block;color:transparent;-webkit-background-clip:text;background-clip:text;' +
      '-webkit-text-fill-color:transparent;transform:perspective(600px) rotateX(var(--hof-rx,0deg)) rotateY(var(--hof-ry,0deg));' +
      'background-image:radial-gradient(circle at var(--hof-gx,50%) var(--hof-gy,40%),rgba(255,255,255,.95),rgba(255,255,255,0) 38%),' +
      'repeating-linear-gradient(62deg,rgba(255,255,255,0) 0 .05em,rgba(255,255,255,.28) .05em .075em),' +
      'var(--hof-rainbow);background-blend-mode:normal,overlay,normal;' +
      'filter:drop-shadow(0 .03em .05em rgba(0,0,0,.25));will-change:transform}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    // A repeating rainbow, 3.2em per cycle, slid along by `shift` em.
    function rainbow(colors, shift) {
      var off = ((shift % 3.2) + 3.2) % 3.2;
      var stops = colors.map(function (c, i) {
        return c + ' ' + (off + i * 3.2 / (colors.length - 1)).toFixed(3) + 'em';
      });
      return 'repeating-linear-gradient(115deg,' + stops.join(',') + ')';
    }

    function setup(el) {
      var d = el.dataset;
      var tilt = d.tilt !== undefined ? +d.tilt : DEFAULTS.tilt;
      var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : DEFAULTS.colors;
      if (colors[0] !== colors[colors.length - 1]) colors = colors.concat(colors[0]);
      var autoplay = d.autoplay !== undefined && d.autoplay !== 'false';

      // The foil goes on an inner span so the element keeps its own display.
      var face = document.createElement('span');
      face.className = 'hof-face';
      while (el.firstChild) face.appendChild(el.firstChild);
      el.appendChild(face);

      // Paint the foil for a light direction (x, y), each from -1 to 1, plus a
      // scroll-driven slide s (in em).
      function paint(x, y, s) {
        face.style.setProperty('--hof-rainbow', rainbow(colors, x * 1.6 + s));
        face.style.setProperty('--hof-gx', (50 + x * 45).toFixed(1) + '%');
        face.style.setProperty('--hof-gy', (45 + y * 45).toFixed(1) + '%');
        face.style.setProperty('--hof-ry', (x * tilt).toFixed(2) + 'deg');
        face.style.setProperty('--hof-rx', (-y * tilt).toFixed(2) + 'deg');
      }

      if (reduced()) { paint(-0.35, -0.3, 0); return; }

      var target = { x: 0, y: 0 };
      var cur = { x: 0, y: 0 };
      var lastReal = -1e9;
      var clock = 0;

      function onPointer(e) {
        var r = el.getBoundingClientRect();
        if (r.bottom < 0 || r.top > innerHeight) return;
        var reach = Math.max(160, Math.min(r.width, innerWidth) * 0.6);
        target.x = Math.max(-1, Math.min(1, (e.clientX - (r.left + r.width / 2)) / reach));
        target.y = Math.max(-1, Math.min(1, (e.clientY - (r.top + r.height / 2)) / reach));
        lastReal = performance.now();
      }
      window.addEventListener('pointermove', onPointer, { passive: true });
      window.addEventListener('pointerdown', onPointer, { passive: true });
      // Phones that report orientation without asking (most Android browsers).
      window.addEventListener('deviceorientation', function (e) {
        if (e.gamma == null || e.beta == null) return;
        target.x = Math.max(-1, Math.min(1, e.gamma / 30));
        target.y = Math.max(-1, Math.min(1, (e.beta - 45) / 30));
        lastReal = performance.now();
      });

      var prev = 0;
      var ticker = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = prev ? Math.min(50, now - prev) : 16;
          prev = now;
          clock += dt;
          if (autoplay && now - lastReal > 2000) {
            target.x = Math.sin(clock * 0.0009) * 0.9;
            target.y = Math.sin(clock * 0.0014 + 1) * 0.6;
          }
          var ease = 1 - Math.pow(0.9, dt / 16);
          cur.x += (target.x - cur.x) * ease;
          cur.y += (target.y - cur.y) * ease;
          paint(cur.x, cur.y, window.scrollY * 0.004);
        }
      });
      paint(0, 0, 0);

      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { prev = 0; ticker.play(); }
        else ticker.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-holographic-foil-text]').forEach(function (el) {
        if (el._hof) return;
        el._hof = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-holographic-foil-text>Holographic</h1>

<!-- gold and silver foil, no tilt: -->
<h1 data-holographic-foil-text data-tilt="0" data-colors="#f7e7a1,#d4af37,#fff6d5,#c0c0c0">Limited edition</h1>