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.json

ogl 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

  1. OGL, not raw WebGL. Renderer/program/geometry lifecycle is delegated to ogl. One fullscreen Triangle covers the viewport; one fragment shader paints every effect.
  2. 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.
  3. Phosphor tint from a single hue. A [0, 360) degree spins the tint around the wheel — 120 for green, 30 for amber, 200 for cool blue. The HSL→RGB conversion runs once per prop change.
  4. 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.
  5. ResizeObserver for parent resize. No window.resize listener — the canvas tracks its container only.
  6. 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.
  7. Visibility-aware. A visibilitychange listener flips a paused flag — the RAF stays scheduled but skips rendering while the tab is hidden.
  8. 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

PropTypeDefaultDescription
intensitynumber1Overall brightness multiplier. 0 paints nothing, 2+ is fully bloomed.
scanlinesnumber0.3Horizontal scan-line bar strength [0, 1].
noisenumber0.3FBM noise amplitude [0, 1] — warps the pattern each frame.
chromaticnumber1.5Per-channel RGB shift in shader units (typical [0, 6]). 0 disables.
vignettenumber0.2Barrel-curvature strength [0, 0.6]. Doubles as the vignette mask.
huenumber120Phosphor hue in degrees. 120 = green, 30 = amber, 200 = blue.
classNamestringClass applied to the wrapping <div> that hosts the WebGL canvas.

Accessibility

  • The wrapper carries aria-hidden="true" and pointer-events: none — pure visual decoration. Foreground content keeps its full interaction surface.
  • Under prefers-reduced-motion: reduce the 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.