CRT Scanline Overlay
Scanlines, aperture grille, rolling refresh and vignette over any content.
- Category
- Backgrounds
- Added
- 2026-09-20
- Deps
- none
- Updated
- 2026-09-20
SYS / READY
CRT MODE
640 × 480 · 60Hz
Props
"use client";
import type { ReactNode } from "react";
export function CrtScanlineOverlay({
children,
lineGap = 3,
intensity = 30,
roll = true,
rollSeconds = 12,
grille = true,
}: {
children: ReactNode;
lineGap?: number;
intensity?: number;
roll?: boolean;
/** Seconds for one pass of the refresh bar. Lower is faster and more agitated. */
rollSeconds?: number;
/** Vertical RGB triads. This is what reads as a tube rather than stripes. */
grille?: boolean;
}) {
const opacity = intensity / 100;
return (
<div className="relative isolate overflow-hidden">
{/* Static keyframes — nothing interpolated in here. */}
<style>{"@keyframes crt-roll { 0% { transform: translateY(-100%); } 100% { transform: translateY(240%); } } @media (prefers-reduced-motion: reduce) { [data-crt-roll] { animation: none !important; } }"}</style>
{children}
{/* Horizontal scanlines. A repeating gradient, not an image: a few
hundred bytes, resolution-independent, crisp on retina. */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage:
"repeating-linear-gradient(to bottom, rgba(0,0,0," +
opacity +
") 0px, rgba(0,0,0," +
opacity +
") 1px, transparent 1px, transparent " +
lineGap +
"px)",
}}
/>
{/* Aperture grille: vertical RGB triads. This is the layer that makes it
read as a real tube rather than as generic stripes. */}
{grille && (
<div
aria-hidden
className="pointer-events-none absolute inset-0 mix-blend-overlay"
style={{
backgroundImage:
"repeating-linear-gradient(to right, rgba(255,0,0,0.10) 0px, rgba(0,255,0,0.10) 1px, rgba(0,0,255,0.10) 2px, transparent 3px)",
}}
/>
)}
{roll && (
<div
aria-hidden
data-crt-roll=""
className="pointer-events-none absolute inset-x-0 h-16"
style={{
background:
"linear-gradient(to bottom, transparent, rgba(255,255,255,0.05), transparent)",
animation: "crt-roll " + Math.max(1, rollSeconds) + "s linear infinite",
}}
/>
)}
{/* Tubes are darker at the corners. */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
background:
"radial-gradient(ellipse at center, transparent 45%, rgba(0,0,0,0.55) 100%)",
}}
/>
</div>
);
}
About this background
Almost every CRT effect on the web is one layer of horizontal stripes, which is why almost every CRT effect reads as stripes rather than as a screen. A real aperture-grille tube has two structures, not one: horizontal scanlines from the beam sweep and vertical red-green-blue triads from the physical mask. Adding that second layer at low opacity with overlay blending is what makes the difference, and it costs one more gradient. Two more touches complete it — a slow rolling refresh band, standing in for the beat frequency between the tube's refresh and a camera, and a corner vignette, because tubes are genuinely darker at the edges. Everything is a repeating CSS gradient rather than a texture, so the whole overlay is a few hundred bytes, scales to any size without resampling, and stays crisp at any device pixel ratio. Keep the intensity low over anything anyone has to read: this darkens content by design, and at high settings it will fail contrast requirements outright. The rolling band also animates continuously, so gate it behind a reduced-motion check.
Curator’s note
Built for viberdy 2.0. Most CRT overlays stop at horizontal stripes; the vertical RGB grille is the layer that actually sells it.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
ASCII Halftone Canvas
assetsTurns any canvas drawing into live ASCII art, sampled one pixel per glyph.
Ordered Dither Plate
backgroundsA gradient quantised to a few tones by an 8x8 Bayer matrix — offset then quantised, dithered in linear light, one cell per device pixel.
Perspective Grid Floor
backgroundsA tilted, endlessly scrolling grid floor that recedes toward the horizon.