Skip to content

CRT Scanline Overlay

Scanlines, aperture grille, rolling refresh and vignette over any content.

Freebrutalistneonplayful
Category
Backgrounds
Added
2026-09-20
Deps
none
Updated
2026-09-20
Preview

SYS / READY

CRT MODE

640 × 480 · 60Hz

Props

3
30
Rolling refresh bar
12
Aperture grille
No runtime dependencies
"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>
  );
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
New

Turns any canvas drawing into live ASCII art, sampled one pixel per glyph.

asciicanvashalftone
Featured
NewPro

A gradient quantised to a few tones by an 8x8 Bayer matrix — offset then quantised, dithered in linear light, one cell per device pixel.

canvasditherprint
Pro

A tilted, endlessly scrolling grid floor that recedes toward the horizon.

gridperspectivesynthwave