Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Shaders

Pro

CRT Phosphor Shader for React

A calm synth sunset seen through a curved picture tube: RGB phosphors, scanlines, soft bloom, rounded black corners and a slow refresh band. For React with Base UI, when a retro game, a music player or a playful 404 needs an intact, glowing old screen.

Framework

CRT Phosphor Shader draws an old picture tube showing a calm synth sunset: a gradient from deep violet to coral, and a glowing orb with slits across its lower half that drifts slowly and leaves a faint phosphor trail. You see it through the tube itself: curved glass that bulges the picture and rounds its corners into black, rows of red, green and blue phosphors, dark gaps between the scanlines, a soft bloom where the picture is brightest, and a faint band that rolls down every six seconds. A WGSL fragment shader on WebGPU draws the picture one scanline at a time and lays the phosphors over it, pixel by pixel.

Use it behind a retro game page, a music player or a playful 404, wherever an old screen fits. The picture stays clean: no tearing, static or flicker. For a corrupted signal, use Glitch Shader; for an 80s floor in plain CSS, the Retro Grid background. It runs only while it's on screen, holds a still frame with reduced motion, and shows a gradient of the same colors in browsers without WebGPU.

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Usage

import { CrtPhosphorShader } from "@/components/ui/crt-phosphor-shader";
<CrtPhosphorShader className="flex min-h-[32rem] items-center justify-center">
  <div className="rounded-md bg-black/60 px-8 py-6">
    <h1 className="font-mono text-5xl font-bold uppercase tracking-widest text-white">
      Press start
    </h1>
  </div>
</CrtPhosphorShader>

CRT Phosphor Shader is a container: your content sits on top of the tube, in normal flow. It fills its own box, so give it a height or content; the tube's corners are black, so it frames itself. For a backdrop behind the whole page, render it empty with className="fixed inset-0 -z-10". The middle of the picture is a mid-tone rose, so neither white nor black text reads on it alone, in either color scheme: put text on a panel such as bg-black/60.

Demos

Shadow mask

mask="shadow" swaps the vertical stripes for staggered dots, as on a TV, curvature={0.2} bulges the glass further, and lines={180} makes the scanlines chunkier.

Preview width

In a card

A late-night palette, skyColor="#0b1e3f" and horizonColor="#2dd4bf", on a slot mask, with lines={120} for bold scanlines in a small box and curvature={0.08} for flatter glass. It suits a "now playing" card or a game's save screen.

Preview width

Performance

About 0.6 ms of GPU time a frame at 1280×720 on an M2 Max laptop; slower GPUs take several times longer. Each pixel samples the picture once on its scanline and six times for the bloom, with no marching. Keep quality at 1: the phosphors and scanlines are pixel-sized, so a lower resolution coarsens them, and the shader is light enough. The animation stops when the shader scrolls out of view, and the browser pauses it in a hidden tab. Every shader on a page shares one GPU device and one frame loop, and vgpu loads only when the first one mounts.

Browser support

The shader needs WebGPU: Chrome and Edge, Safari 26 and later, and Firefox on Windows and macOS. Elsewhere, and until the first frame, the box shows a CSS gradient of the same colors, the sunset with a soft orb, thin scanlines and dark corners, so the layout never jumps. data-state on the root reads unsupported there, if you want to style around it. See Shader Canvas.

Accessibility

The tube is decoration: it's hidden from assistive tech and lets every click through to your content. With reduced motion it draws one still frame, with the refresh band held in place, and stops. Nothing flashes or flickers: the orb drifts over tens of seconds, and the refresh band brightens the picture by about a tenth as it rolls down, once every six seconds. Across the renders we measured, white text reaches 4.5:1 on 69% of the shader and black text on 32%, and the middle is the weakest part. White text on a bg-black/60 panel stayed above 5.9:1 on every pixel.

FAQ

Is it a video or an image?

Neither: the sunset, the orb and its slits are computed for every pixel, every frame. That's why the picture can bend with the glass, change color with a prop, and stay sharp at any size.

What are aperture, shadow and slot?

The three phosphor patterns of real tubes. aperture is vertical red, green and blue stripes, as on a Trinitron; shadow staggers them into dots, as on most TVs; slot breaks the stripes into short slots, staggered from column to column. Each phosphor is about one CSS pixel wide (two for the shadow mask's dots), so the pattern keeps its size on any screen.

How do the scanlines work?

lines sets how many scanlines cover the height, from 120 to 480. Each line shows one row of the picture, and its glow is a Gaussian that grows wider on bright lines, so highlights fill the gaps as on a real tube. On a small box or a low-density screen the shader uses fewer lines, so each stays at least 4 device pixels tall and never shimmers against the pixel grid.

Can I change the colors?

Yes: skyColor sets the top of the sunset, horizonColor the bottom, and glowColor the orb. All three take any CSS color, including theme variables, and follow a theme switch. intensity sets the brightness and the bloom together, from 0 to 2.

Can I make the screen flat?

Set curvature={0}: the picture fills the box edge to edge, with only the rounded corners and the vignette left. Up to 0.3, the glass bulges further and the corners pull in.

Will it slow my page down?

It costs about 0.6 ms of GPU time a frame at 1280×720 on a laptop, and the CPU only sets a few numbers each frame. It stops when it's off screen and draws a single frame with reduced motion. Leave quality at 1: the phosphors and scanlines are the look, and they need full resolution.

API Reference

CrtPhosphorShader

data-slot="crt-phosphor-shader"

PropType
curvature?number

How much the tube bulges, from 0 (flat) to 0.3. @default 0.12.

Default
0.12
fallback?string

What shows until the first frame and in browsers without WebGPU: a CSS background value (gradients, sizes, a colour) that looks like the shader. It can use the colour props' --shader-<field> variables.

glowColor?string

The orb and the highlights. Any CSS colour. @default "#ffe29a".

Default
"#ffe29a"
horizonColor?string

The bottom of the sunset. Any CSS colour. @default "#ff7b54".

Default
"#ff7b54"
intensity?number

Brightness and bloom, from 0 to 2. @default 1.

Default
1
interactive?boolean

Whether u.pointer follows the pointer. @default false.

lines?number

Scanlines over the height of the tube, from 120 (chunky) to 480 (fine); fewer on a small box, so each stays at least 4 device pixels tall. @default 240.

Default
240
mask?keyof typeof MASKS

The phosphor pattern: vertical stripes, staggered dots, or stripes broken into slots. @default "aperture".

Default
"aperture"
paused?boolean

Holds the current frame. @default false.

quality?number

Render scale, from 0.25 to 1, of the screen's pixels (capped at 2×). @default 1.

skyColor?string

The top of the sunset. Any CSS colour. @default "#2b1055".

Default
"#2b1055"
speed?number

Animation speed: 1 is normal, 0 holds the frame. @default 1.

time?number

A fixed moment, in seconds: draws it and doesn't animate.

render?ReactElement | ((props: object, state: object) => ReactElement)

Replace the host element or compose it with another component.

Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string

Forwarded to the part.

All 101 shaders