shadcn.io is not affiliated with official shadcn/ui
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.
Installation
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.
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.
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"
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.
Related


Glitch Shader
Digital & retroA corrupted signal, free
- Pro


Synthwave Canyon Shader
Water & landscapesThe sunset as a 3D flight
- Pro


Pixel Sort Shader
Digital & retroA dusk melted into streaks
- Pro


Oldschool Tube Shader
Tunnels & 3DDemoscene metal for a retro page


Dither Gradient Shader
Digital & retro1-bit pixels on paper, free
- Pro


ASCII Sculpture Shader
Digital & retroAmber phosphor, as glyphs