vault66-crt-effect
Version:
A customizable CRT (retro monitor) visual effect for React, Vue, and Web Components
276 lines (216 loc) • 21.1 kB
Markdown
# vault66-crt-effect
[**Live playground and documentation →**](https://vault66.dev/)



A CRT screen effect you wrap around any content: scanlines, sweep line, glow, flicker, glitch, screen curvature, glare, and static. Ships for **React**, **Vue**, and as a framework-agnostic **Web Component** (so it also works in Angular, plain HTML, Astro, Svelte, or anything else that runs in a browser).
---

## Installation
```bash
npm install vault66-crt-effect
pnpm add vault66-crt-effect
yarn add vault66-crt-effect
bun add vault66-crt-effect
```
## Use it with your framework
Same effect, same props everywhere — only the import and the syntax change. Presets get you a full look in one line; every individual prop is listed in [Props](#props).
### React
```jsx
import CRTEffect from "vault66-crt-effect";
import "vault66-crt-effect/style.css";
export default function App() {
return (
<CRTEffect preset="fallout">
<div style={{ padding: 20, fontSize: 24 }}>Welcome to the Wasteland!</div>
</CRTEffect>
);
}
```
### Vue
```vue
<script setup>
import CRTEffect from "vault66-crt-effect/vue";
import "vault66-crt-effect/style.css";
</script>
<template>
<CRTEffect preset="fallout">
<div style="padding: 20px; font-size: 24px">Welcome to the Wasteland!</div>
</CRTEffect>
</template>
```
### Web Component
Import once to register the `<crt-effect>` element, then use it anywhere — no CSS import needed, the component carries its own styles in a shadow root. Props become attributes: booleans are bare (`fill`), everything else is kebab-case (`sweep-duration="5"`).
```html
<script type="module">
import "vault66-crt-effect/element";
</script>
<crt-effect preset="fallout">
<div style="padding: 20px; font-size: 24px">Welcome to the Wasteland!</div>
</crt-effect>
```
**Angular** uses the same element — just add `CUSTOM_ELEMENTS_SCHEMA` to your component/module so the compiler accepts the custom tag (otherwise you'll hit `NG8001`), and `import "vault66-crt-effect/element"` once:
```ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "vault66-crt-effect/element";
@Component({
selector: "app-root",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `<crt-effect preset="fallout">…</crt-effect>`,
})
export class AppComponent {}
```
## Props
Every prop works the same across React, Vue, and the Web Component. Start from a `preset` and override whatever you want, or set props from scratch.
| Prop | Type | Default | Purpose |
| --------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `preset` | `"fallout" \| "dos" \| "cyberpunk" \| "arcade" \| "commodore64" \| "apple2" \| "vt100" \| "minimal"` | `undefined` | Apply a complete preset configuration. Individual props can override preset values. See [Presets](#presets) below |
| `enabled` | `boolean` | `true` | Enables or disables the entire CRT effect |
| `sweepDuration` | `number` | `10` | Duration in seconds for the vertical sweep line animation |
| `sweepThickness` | `number` | `10` | Height (thickness in pixels) of the sweep line |
| `sweepStyle` | `"classic" \| "soft"` | `"classic"` | Style of the vertical sweep line: `"classic"` is a sharp black line, `"soft"` is a blurred shadow effect |
| `scanlineOpacity` | `number` (0 to 1) | `0.2` | Opacity of the scanlines |
| `scanlineThickness` | `number` | `2` | Thickness of each scanline in pixels |
| `scanlineGap` | `number` | `3` | Gap between scanlines in pixels |
| `scanlineColor` | `string` | `"rgba(91, 179, 135, 0.2)"` | Custom hex, RGB, or RGBA scanline color (used only if `theme` is `"custom"`; alpha is controlled by `scanlineOpacity`) |
| `enableScanlines` | `boolean` | `true` | Shows or hides the scanlines overlay |
| `scanlineOrientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Orientation of the scanlines: `"horizontal"` renders lines from top to bottom, `"vertical"` renders lines from left to right |
| `enableSweep` | `boolean` | `true` | Shows or hides the vertical sweep line animation |
| `theme` | `"green" \| "amber" \| "blue" \| "custom"` | `"green"` | Predefined scanline color themes (overrides `scanlineColor` unless `custom` is used) |
| `enableGlow` | `boolean` | `false` | Enables outer glow effect around the container |
| `glowColor` | `string` | `"rgba(0, 255, 128, 0.3)"` | Color of the outer glow |
| `enableEdgeGlow` | `boolean` | `false` | Enables inset glow effect around edges |
| `edgeGlowColor` | `string` | `"rgba(0, 255, 128, 0.2)"` | Color of the inset edge glow |
| `edgeGlowSize` | `number` | `30` | Size in pixels of the inset edge glow (box-shadow inset size) |
| `enableFlicker` | `boolean` | `false` | Enables subtle flicker animation for CRT realism |
| `flickerIntensity` | `"low" \| "medium" \| "high" \| number` | `0.08` | Flicker depth: preset string or custom number (0-1) where 0=no flicker, 1=max brightness variance |
| `flickerSpeed` | `"low" \| "medium" \| "high" \| number` | `0.8` | Flicker animation speed: preset string or custom number in seconds. Lower values = faster flicker |
| `enableGlitch` | `boolean` | `false` | Enables shaking/interference glitch animation |
| `glitchIntensity` | `"low" \| "medium" \| "high" \| number` | `0.6` | Glitch distance: preset string or custom number (0-1) where 0=no movement, 1=max shake amount |
| `glitchSpeed` | `"low" \| "medium" \| "high" \| number` | `0.6` | Glitch animation speed: preset string or custom number in seconds. Lower values = faster glitch |
| `enableVignette` | `boolean` | `false` | Enables a subtle edge-darkening vignette overlay |
| `vignetteIntensity` | `number` (0 to 1) | `0.4` | Controls vignette darkness at the edges |
| `enableCurvature` | `boolean` | `false` | Enables a curved-glass overlay: a soft corner highlight plus darkened, rounded screen edges |
| `curvatureIntensity` | `number` (0 to 1) | `0.5` | Strength of the curved-glass edge darkening |
| `enableGlare` | `boolean` | `false` | Enables a broad, soft diagonal glass-reflection sheen across the screen |
| `glareIntensity` | `number` (0 to 1) | `0.18` | Brightness of the glare/reflection sheen |
| `enableNoise` | `boolean` | `false` | Enables an animated RF static/snow overlay generated from SVG fractal noise |
| `noiseOpacity` | `number` (0 to 1) | `0.15` | Opacity of the static/snow overlay |
| `glitchChromatic` | `boolean` | `false` | Adds animated red/cyan chromatic aberration to the glitch (requires `enableGlitch`) |
| `tintText` | `boolean` | `false` | Recolors the wrapped content to a phosphor tint (derived from `theme`) with a soft glow. Children with their own explicit `color` keep it |
| `textColor` | `string` | `undefined` | Overrides the phosphor tint color used when `tintText` is on. Any CSS color |
| `sweepColor` | `string` | `undefined` | Gives the sweep line a bright, colored glow instead of the default dark refresh bar. Any CSS color |
| `fill` | `boolean` | `false` | Stretches the effect to fill its parent's full width and height instead of sizing to its content. See [Full-screen layouts](#full-screen-layouts) |
| `children` | content | — | Content rendered inside the effect: React children, the Vue default slot, or slotted DOM in the Web Component |
In the Web Component, prop names are kebab-case attributes: `enableGlow` → `enable-glow`, `sweepDuration` → `sweep-duration="5"`, and booleans are set by presence (`fill`, `enable-flicker`).
## Presets
Eight ready-made monitor looks. Use one as-is, or as a starting point and override individual props.
| Preset | Look |
| ------------- | --------------------------------------------------------------------------------------------- |
| `fallout` | Green Vault-Tec phosphor terminal — medium scanlines, sweep, subtle edge glow, gentle flicker |
| `dos` | Amber IBM/Hercules monitor — clean scanlines, no sweep or glow |
| `cyberpunk` | Purple/magenta neon — heavy outer + edge glow, fast sweep, flicker, glitch |
| `arcade` | Bright saturated green — thick scanlines, fast sweep, strong glow |
| `commodore64` | Periwinkle 8-bit home computer — chunky scanlines, subtle flicker |
| `apple2` | Green Apple II — tight scanlines, clean (no sweep or glow) |
| `vt100` | Pale green DEC terminal — minimal scanlines, no sweep or glow |
| `minimal` | Barely-there — subtle scanlines + light vignette only |
```jsx
// Use a preset as-is
<CRTEffect preset="arcade">
<YourContent />
</CRTEffect>
// Override specific preset values
<CRTEffect preset="fallout" sweepDuration={5} enableFlicker={false}>
<YourContent />
</CRTEffect>
```
## Customization
Start from a preset and change a few values, or build the whole look from individual props. A few common patterns (React syntax; the props are identical in Vue and the Web Component):
```jsx
// Custom scanline color and orientation
<CRTEffect theme="custom" scanlineColor="#ff6400" scanlineOpacity={0.3} scanlineOrientation="vertical">
<YourContent />
</CRTEffect>
// Fine-tune flicker and glitch with numbers
<CRTEffect
enableFlicker
flickerIntensity={0.05} // subtle brightness variance (0-1)
flickerSpeed={2} // slow flicker (seconds)
enableGlitch
glitchIntensity={0.3} // gentle shake (0-1)
glitchSpeed={1.5} // medium speed (seconds)
>
<YourContent />
</CRTEffect>
// Or use the "low" / "medium" / "high" shortcuts
<CRTEffect enableFlicker flickerIntensity="low" enableGlitch glitchIntensity="high">
<YourContent />
</CRTEffect>
```
## Full-screen layouts
By default the wrapper **sizes to its content** — good for cards, panels, and inline blocks. When you want the effect to cover a whole region or the full viewport, add `fill` so the wrapper stretches to its parent (and the content stretches with it):
```jsx
// The parent needs a real height — here, the full viewport
<div style={{ width: "100vw", height: "100vh" }}>
<CRTEffect preset="fallout" fill>
<YourApp />
</CRTEffect>
</div>
```
> `fill` fills the **parent**, so the parent must have a resolved height (`100vh`, a fixed `px`, or `100%` inherited down the chain).
## Building your own integration
If your framework isn't React or Vue and you'd rather not use the Web Component, the core is exported separately. `computeCrt(options)` takes the same props and hands back everything needed to render the effect onto your own markup — no framework attached. It's exactly what the React and Vue wrappers are built on.
It returns four things:
```js
const { enabled, wrapper, inner, overlays } = computeCrt({ preset: "fallout" });
```
- **`enabled`** — `false` when the effect is turned off; render your content untouched.
- **`wrapper`** — `{ className, style }` for the outer element.
- **`inner`** — `{ className, style }` for the element that holds your content.
- **`overlays`** — an array of class names, one per overlay layer.
The markup contract is: a **wrapper** element containing an **inner** element (your content goes in there), followed by one empty **overlay** div per entry in `overlays`:
```html
<div class="{wrapper.className}" style="{wrapper.style}">
<div class="{inner.className}" style="{inner.style}">…your content…</div>
<!-- one sibling div per overlay class -->
<div class="crt-vignette"></div>
</div>
```
One catch when applying the styles by hand: the `style` bag contains both normal CSS properties (`filter`, `flexDirection`) and CSS custom properties (`--scanline-color-rgb`). Custom properties have to be set with `setProperty`, not by assigning to `el.style`. Here's a complete vanilla-DOM renderer that handles it:
```js
import { computeCrt } from "vault66-crt-effect/core";
import "vault66-crt-effect/style.css";
function applyStyle(el, style) {
for (const [key, value] of Object.entries(style)) {
if (key.startsWith("--")) el.style.setProperty(key, String(value));
else el.style[key] = value;
}
}
// Wrap any element in the CRT effect and return the new root element.
function crt(content, options) {
const { enabled, wrapper, inner, overlays } = computeCrt(options);
if (!enabled) return content; // effect off — hand content back as-is
const root = document.createElement("div");
root.className = wrapper.className;
applyStyle(root, wrapper.style);
const innerEl = document.createElement("div");
innerEl.className = inner.className;
if (inner.style) applyStyle(innerEl, inner.style);
innerEl.append(content);
root.append(innerEl);
for (const cls of overlays) {
const layer = document.createElement("div");
layer.className = cls;
root.append(layer);
}
return root;
}
const screen = crt(myContent, { preset: "fallout" });
document.body.append(screen);
```
## Reduced motion
If the system has "Reduce Motion" enabled, animated layers (sweep, flicker, glitch, static) turn themselves off automatically, while layers like scanlines and glow stay visible. Nothing to configure.
## License
MIT — see [LICENSE](LICENSE) for details.