← Filters & retro

Hologram

Beam any content in as a hologram. It flickers into being, tinted one glowing blue and cut by rolling scanlines, then floats gently on a cone of light from a little projector. Now and then the signal drops: it flickers, or tears sideways for a moment.

Incoming messageRelay 7 · 03:42

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-hologram to any element: a card, a heading, an image. Light content on a dark background looks best.

Options

Add any of these attributes to the same element.

data-colorThe hologram’s colour. Default #5fd4ff.
data-basefalse leaves out the projector and the cone of light.
data-glitchHow often the signal drops, from 0 to 2. Default 1; 0 for never.

With reduced motion it keeps the colour and the scanlines but stays still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Hologram: turns any content into a flickering blue hologram. It is tinted
    // one glowing colour, cut by rolling scanlines, floats gently up and down,
    // and now and then flickers or jumps sideways as the signal drops. A small
    // projector glows underneath with a cone of light rising from it.
    // Usage: <div data-hologram>…any content…</div>
    // Optional: data-color (the hologram's colour, default "#5fd4ff"),
    // data-base ("false" leaves out the projector and the light cone),
    // data-glitch (how often it glitches, 0 to 2, default 1; 0 for never).
    var DEFAULTS = { color: '#5fd4ff', glitch: 1 };

    var css =
      '.hlg{position:relative;isolation:isolate}' +
      '.hlg-on{padding-bottom:30px}' +
      '.hlg-body{position:relative;z-index:1;opacity:.9;' +
      '-webkit-mask-image:repeating-linear-gradient(0deg,#000 0 2px,rgba(0,0,0,.4) 2px 4px);mask-image:repeating-linear-gradient(0deg,#000 0 2px,rgba(0,0,0,.4) 2px 4px);' +
      '-webkit-mask-size:100% 8px;mask-size:100% 8px}' +
      '.hlg-roll .hlg-body{animation:hlg-roll 1.2s linear infinite}' +
      '.hlg-base{position:absolute;left:50%;bottom:6px;width:min(60%,180px);height:14px;transform:translateX(-50%);border-radius:50%;z-index:0;pointer-events:none}' +
      '.hlg-base::before{content:"";position:absolute;inset:0;border-radius:50%;background:radial-gradient(closest-side,#fff,var(--hlg-c) 45%,transparent);filter:blur(1px)}' +
      '.hlg-base::after{content:"";position:absolute;left:50%;bottom:50%;width:min(320%,500px);height:var(--hlg-h,200px);transform:translateX(-50%);' +
      'background:linear-gradient(to top,var(--hlg-c),transparent 85%);opacity:.22;clip-path:polygon(42% 100%,58% 100%,100% 0,0 0);filter:blur(3px)}' +
      '@keyframes hlg-roll{to{-webkit-mask-position:0 8px;mask-position:0 8px}}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function hex(c) {
      var m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec((c || '').trim());
      if (!m) return [95, 212, 255];
      var s = m[1].length === 3 ? m[1].replace(/./g, '$&$&') : m[1];
      return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
    }

    // One SVG filter per colour: brightness becomes the hologram colour, lifting the darks a little.
    var filters = {};
    function tint(color) {
      if (filters[color]) return filters[color];
      var c = hex(color).map(function (v) { return v / 255; });
      var id = 'hlg-f' + Object.keys(filters).length;
      var row = function (v) {
        var k = 1.35;
        return (v * 0.3 * k).toFixed(3) + ' ' + (v * 0.59 * k).toFixed(3) + ' ' + (v * 0.11 * k).toFixed(3) + ' 0 ' + (v * 0.12 + 0.04).toFixed(3);
      };
      var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('aria-hidden', 'true');
      svg.setAttribute('width', '0');
      svg.setAttribute('height', '0');
      svg.style.position = 'absolute';
      svg.innerHTML = '<filter id="' + id + '" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="' +
        row(c[0]) + ' ' + row(c[1]) + ' ' + row(c[2]) + ' 0 0 0 1 0"/></filter>';
      document.body.appendChild(svg);
      filters[color] = id;
      return id;
    }

    function setup(el) {
      var d = el.dataset;
      var color = d.color || DEFAULTS.color;
      var glitch = d.glitch !== undefined ? +d.glitch : DEFAULTS.glitch;
      var base = d.base !== 'false';
      el.style.setProperty('--hlg-c', color);

      var body = document.createElement('div');
      body.className = 'hlg-body';
      while (el.firstChild) body.appendChild(el.firstChild);
      el.appendChild(body);
      body.style.filter = 'url(#' + tint(color) + ') drop-shadow(0 0 6px ' + color + ')';

      if (base) {
        el.classList.add('hlg-on');
        var b = document.createElement('span');
        b.className = 'hlg-base';
        b.setAttribute('aria-hidden', 'true');
        el.appendChild(b);
        var fit = function () { b.style.setProperty('--hlg-h', Math.max(40, body.offsetHeight + 10) + 'px'); };
        fit();
        if (window.ResizeObserver) new ResizeObserver(fit).observe(body);
      }
      if (reduced()) return;
      el.classList.add('hlg-roll');

      var visible = false, started = false, glitchTimer = 0;
      var float = anime.animate(body, {
        y: [-4, 4], duration: 2600, ease: 'inOut(2)', loop: true, alternate: true, autoplay: false
      });

      // Signal trouble: a quick flicker, sometimes with a sideways tear.
      function trouble() {
        if (!visible || glitch <= 0) return;
        var tear = Math.random() < 0.45;
        anime.animate(body, {
          opacity: [0.9, 0.35, 0.85, 0.5, 0.9],
          x: tear ? [0, anime.utils.random(-10, 10), anime.utils.random(-5, 5), 0] : 0,
          skewX: tear ? [0, anime.utils.random(-12, 12), 0] : 0,
          duration: tear ? 260 : 200,
          ease: 'linear'
        });
        next();
      }
      function next() {
        clearTimeout(glitchTimer);
        glitchTimer = setTimeout(trouble, (1200 + Math.random() * 2600) / Math.max(0.2, glitch));
      }

      // A gentle, constant shimmer in brightness.
      var shimmer = anime.createTimer({
        loop: true, duration: 1000, autoplay: false,
        onUpdate: function () {
          if (Math.random() < 0.35) body.style.opacity = (0.82 + Math.random() * 0.1).toFixed(2);
        }
      });

      new IntersectionObserver(function (entries) {
        visible = entries[0].isIntersecting;
        if (visible) {
          if (!started) {
            // It flickers into being the first time.
            started = true;
            anime.animate(body, { opacity: [0, 0.6, 0.1, 0.9, 0.4, 0.9], scaleY: [0.02, 1], duration: 650, ease: 'out(3)' });
          }
          float.play();
          shimmer.play();
          next();
        } else {
          float.pause();
          shimmer.pause();
          clearTimeout(glitchTimer);
        }
      }, { threshold: 0.2 }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-hologram]').forEach(function (el) {
        if (el._hlg) return;
        el._hlg = true;
        el.classList.add('hlg');
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-hologram>
  <img src="avatar.png" alt="">
  <p>Incoming message</p>
</div>