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
- Paste the snippet just before
</head>. - Add
data-crt-screento a section. Give it a dark background and a bright text colour, like a phosphor green. - Its content sits centred on the screen.
Options
Add any of these attributes to the same element.
data-loop | Switch off and on again every few seconds. |
data-hold | How long the picture stays on between loops. Default 4200 ms. |
data-flicker | How much it flickers, from 0 to 1. Default 0.5. |
data-scanlines | Pixels between scanlines. Default 3. |
data-curve | false for flat glass: no vignette or rounded corners. |
data-power | false 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.
<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><section data-crt-screen style="background:#04130a; color:#4ade80">
<h2>READY.</h2>
</section>