← Filters & retro

CRT Screen

Turn any section into an old monitor. Fine scanlines, curved glass with a dark vignette, a soft bright band rolling down and a gentle flicker. When it scrolls into view the picture switches on like a warming tube: a bright dot, a line, then the whole image.

> LOAD "ANIMACIOUS" READY. HELLO, WORLD

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-crt-screen to a section. Give it a dark background and a bright text colour, like a phosphor green.
  3. Its content sits centred on the screen.

Options

Add any of these attributes to the same element.

data-loopSwitch off and on again every few seconds.
data-holdHow long the picture stays on between loops. Default 4200 ms.
data-flickerHow much it flickers, from 0 to 1. Default 0.5.
data-scanlinesPixels between scanlines. Default 3.
data-curvefalse for flat glass: no vignette or rounded corners.
data-powerfalse skips the switch-on; the picture is simply there.

With reduced motion the screen keeps its scanlines and glass but doesn’t flicker or switch on.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // CRT Screen: turns any section into an old monitor. Fine scanlines, a
    // curved-glass vignette with rounded corners, a soft rolling bright band and
    // a gentle flicker. When it scrolls into view the picture switches on like a
    // tube warming up: a bright line that opens out into the image.
    // Usage: <div data-crt-screen style="background:#04130a;color:#4ade80">…</div>
    // The content sits centred on the screen; the section's background and
    // padding move onto the screen with it.
    // Optional: data-loop (switch off and on again every few seconds),
    // data-hold (ms the picture stays on between loops, default 4200),
    // data-flicker (0 to 1, default 0.5), data-scanlines (px between lines,
    // default 3), data-curve="false" (flat glass: no vignette or rounded corners),
    // data-power="false" (no switch-on effect; the picture is simply there).
    var DEFAULTS = { hold: 4200, flicker: 0.5, scanlines: 3 };

    var css =
      '.crt{position:relative;overflow:hidden;isolation:isolate;display:grid;grid-template:1fr/1fr;background:#080808}' +
      '.crt-curve{border-radius:22px/18px}' +
      '.crt-inner{grid-area:1/1;position:relative;display:grid;place-items:center;align-self:stretch;justify-self:stretch;min-width:0;transform-origin:50% 50%}' +
      '.crt-layer{position:absolute;inset:0;pointer-events:none;z-index:2}' +
      '.crt-lines{background:repeating-linear-gradient(to bottom,rgba(0,0,0,.32) 0,rgba(0,0,0,.32) 1px,transparent 1px,transparent var(--crt-gap,3px))}' +
      '.crt-glass{box-shadow:inset 0 0 60px rgba(0,0,0,.65),inset 0 0 14px rgba(0,0,0,.6);' +
        'background:radial-gradient(120% 95% at 50% 50%,transparent 58%,rgba(0,0,0,.55) 100%),' +
        'radial-gradient(70% 45% at 30% 12%,rgba(255,255,255,.09),transparent 70%)}' +
      '.crt-roll{top:-30%;bottom:auto;height:30%;background:linear-gradient(to bottom,transparent,rgba(255,255,255,.06) 50%,transparent);mix-blend-mode:screen}' +
      '.crt-flick{background:#fff;opacity:0;mix-blend-mode:overlay}' +
      '.crt-dot{inset:auto;left:50%;top:50%;width:10px;height:10px;margin:-5px 0 0 -5px;border-radius:50%;background:#fff;box-shadow:0 0 18px 6px rgba(255,255,255,.7);opacity:0;z-index:3}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function layer(el, cls) {
      var d = document.createElement('div');
      d.className = 'crt-layer ' + cls;
      d.setAttribute('aria-hidden', 'true');
      el.appendChild(d);
      return d;
    }

    function setup(el) {
      var d = el.dataset;
      el.classList.add('crt');
      if (d.curve !== 'false') el.classList.add('crt-curve');

      // Move the content (and the background) into an inner screen, so it can be
      // squashed for the power-on while the glass behind stays dark.
      var inner = document.createElement('div');
      inner.className = 'crt-inner';
      var cs = getComputedStyle(el);
      inner.style.backgroundColor = cs.backgroundColor;
      inner.style.backgroundImage = cs.backgroundImage;
      inner.style.padding = cs.padding;
      el.style.background = '#080808';
      el.style.padding = '0';
      while (el.firstChild) inner.appendChild(el.firstChild);
      el.appendChild(inner);

      var lines = layer(el, 'crt-lines');
      lines.style.setProperty('--crt-gap', (+d.scanlines || DEFAULTS.scanlines) + 'px');
      var roll = layer(el, 'crt-roll');
      var flick = layer(el, 'crt-flick');
      if (d.curve !== 'false') layer(el, 'crt-glass');
      var dot = layer(el, 'crt-dot');

      el._crt = { inner: inner, roll: roll, flick: flick, dot: dot };
    }

    function play(el) {
      var s = el._crt, d = el.dataset;
      var flicker = d.flicker !== undefined ? +d.flicker : DEFAULTS.flicker;
      var hold = +d.hold || DEFAULTS.hold;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var power = d.power !== 'false';

      // The rolling band drifts down the screen, over and over.
      var rollAnim = anime.animate(s.roll, { top: ['-30%', '100%'], duration: 6500, ease: 'linear', loop: true });

      // Flicker: a faint random shimmer, with the odd brief dip.
      var on = !power;
      var flickTimer = anime.createTimer({
        loop: true, duration: 1000,
        onUpdate: function () {
          if (!on || !flicker) return;
          var v = Math.random() * 0.05 * flicker;
          if (Math.random() < 0.006 * flicker) v = 0.2 * flicker;
          s.flick.style.opacity = v;
          s.inner.style.opacity = 1 - Math.random() * 0.04 * flicker;
        }
      });

      function switchOn() {
        var tl = anime.createTimeline({ onComplete: function () { on = true; } });
        anime.utils.set(s.inner, { scaleY: 0.006, scaleX: 0, filter: 'brightness(4)' });
        tl.add(s.dot, { opacity: [0, 1], scale: [0.2, 1], duration: 160, ease: 'out(2)' })
          .add(s.inner, { scaleX: 1, duration: 260, ease: 'out(3)' }, '-=40')
          .add(s.dot, { opacity: 0, duration: 200 }, '<<')
          .add(s.inner, { scaleY: 1, duration: 420, ease: 'outElastic(1, .7)' }, '-=60')
          .add(s.inner, { filter: 'brightness(1)', duration: 700, ease: 'out(2)' }, '<<');
        return tl;
      }

      function switchOff() {
        on = false;
        s.flick.style.opacity = 0;
        var tl = anime.createTimeline();
        tl.add(s.inner, { scaleY: 0.006, filter: 'brightness(3)', duration: 220, ease: 'in(3)' })
          .add(s.inner, { scaleX: 0, duration: 200, ease: 'in(2)' })
          .add(s.dot, { opacity: [1, 0], scale: [1, 0.1], duration: 450, ease: 'out(2)' }, '-=60');
        return tl;
      }

      var timers = [];
      function cycle() {
        switchOn();
        if (!loop) return;
        timers.push(setTimeout(function () {
          switchOff();
          timers.push(setTimeout(cycle, 1400));
        }, hold + 1000));
      }
      if (power) cycle();

      // Pause the continuous parts while off screen.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { rollAnim.play(); flickTimer.play(); }
        else { rollAnim.pause(); flickTimer.pause(); }
      }).observe(el);
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-crt-screen]').forEach(function (el) {
        if (el._crt) return;
        setup(el);
        if (!reduced()) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-crt-screen style="background:#04130a; color:#4ade80">
  <h2>READY.</h2>
</section>