Faulty Terminal
A WebGL-rendered CRT-VHS backdrop. A shimmering grid of glyphs warps with FBM noise, a scan-line bar sweeps across, chromatic aberration splits the channels, and barrel curvature pulls the edges into a vignette. Drop it inside any relative container — it fills the parent and is pointer-events: none, so foreground content stays interactive.
READY.
Customize
Brightness
1
0.3
Distortion
0.3
1.5
0.2
Color
120°
Installation
npm install ogl
npx shadcn@latest add https://craftbits.dev/r/faulty-terminal.jsonogl is an optional peer dependency — install it alongside the component.
Usage
import { FaultyTerminal } from "@craft-bits/core";
<section className="relative h-screen w-full">
<FaultyTerminal />
{/* foreground content */}
</section>Understanding the component
- OGL, not raw WebGL. Renderer/program/geometry lifecycle is delegated to
ogl. One fullscreenTrianglecovers the viewport; one fragment shader paints every effect. - Six knobs, not twenty. The source ships ~20 uniforms (page-load fades, mouse ripples, dither, gradient tint, dual flicker amounts, …). The library keeps the six that meaningfully change the look —
intensity,scanlines,noise,chromatic,vignette,hue— and drops the rest. - Phosphor tint from a single hue. A
[0, 360)degree spins the tint around the wheel —120for green,30for amber,200for cool blue. The HSL→RGB conversion runs once per prop change. - DPR-capped at 2. A retina display would otherwise paint 4× the fragments. Capping at 2 keeps things sharp on every screen we care about while halving the GPU load on phones.
- ResizeObserver for parent resize. No
window.resizelistener — the canvas tracks its container only. - Reduced motion paints one frame. Under
prefers-reduced-motion: reduce, the component renders a single static frame at mount and never starts the RAF loop. - Visibility-aware. A
visibilitychangelistener flips a paused flag — the RAF stays scheduled but skips rendering while the tab is hidden. - Full GL cleanup. On unmount we cancel the RAF, disconnect the ResizeObserver, remove the canvas, and call
WEBGL_lose_context.loseContext()— no dangling GPU resources.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
intensity | number | 1 | Overall brightness multiplier. 0 paints nothing, 2+ is fully bloomed. |
scanlines | number | 0.3 | Horizontal scan-line bar strength [0, 1]. |
noise | number | 0.3 | FBM noise amplitude [0, 1] — warps the pattern each frame. |
chromatic | number | 1.5 | Per-channel RGB shift in shader units (typical [0, 6]). 0 disables. |
vignette | number | 0.2 | Barrel-curvature strength [0, 0.6]. Doubles as the vignette mask. |
hue | number | 120 | Phosphor hue in degrees. 120 = green, 30 = amber, 200 = blue. |
className | string | — | Class applied to the wrapping <div> that hosts the WebGL canvas. |
Accessibility
- The wrapper carries
aria-hidden="true"andpointer-events: none— pure visual decoration. Foreground content keeps its full interaction surface. - Under
prefers-reduced-motion: reducethe RAF loop never starts; a single static frame is painted so the canvas isn't blank. - Pauses on
document.visibilitychange(tab switch) — no GPU spent on offscreen frames; resumes cleanly when the tab regains focus.
Credits
- Extracted from:
terminal-dreams(src/components/interactions/FaultyTerminal.tsx). Re-architected from a 20-uniform parameter sheet to a 6-knob library surface; added DPR cap, GL resource cleanup, reduced-motion short-circuit, and visibility pause. - Inspiration: react-bits.