COMPONENT / crt-overlay
CRT Overlay_
Scanlines, a vignette and a sub-3Hz flicker laid over its children like an old monitor. Pure CSS; the flicker is off under reduced motion. Requires @cybr/tokens and @cybr/effects.
Install
1. Map the
@cybrnamespace, once per project. Add this inside yourcomponents.json, or into its existing"registries"object. Nocomponents.jsonyet? Runnpx shadcn@latest initfirst to create one."registries": { "@cybr": "https://cybr-ui.com/r/{name}.json" }2. Add components:
$ npx shadcn@latest add @cybr/crt-overlay
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
NO CARRIER
CH 03 · RETRY IN 05
04 / CRT OVERLAY
SCANLINES · VIGNETTE · FLICKER < 3HZ
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
const scanlines =
"repeating-linear-gradient(0deg, color-mix(in srgb, var(--ground-void) 45%, transparent) 0 2px, transparent 2px 4px)";
const vignette =
"radial-gradient(ellipse at center, transparent 45%, color-mix(in srgb, var(--ground-void) 75%, transparent) 100%)";
// Old-monitor screen: scanlines and a vignette laid over the children, and a
// faint flicker (two dips every 4s, under 3Hz). The overlays are aria-hidden
// and never take pointer events. The flicker is off under reduced motion.
export function CrtOverlay({
vignette: showVignette = true,
flicker = true,
className,
children,
...props
}: ComponentProps<"div"> & { vignette?: boolean; flicker?: boolean }) {
return (
<div className={cn("relative isolate overflow-hidden bg-ground-deep", flicker && "crt-flicker", className)} {...props}>
{children}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 z-10" style={{ backgroundImage: scanlines }} />
{showVignette ? (
<div aria-hidden="true" className="pointer-events-none absolute inset-0 z-10" style={{ backgroundImage: vignette }} />
) : null}
</div>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| vignette | boolean | true | Darken the edges toward --ground-void. |
| flicker | boolean | true | Two faint opacity dips every 4s on the whole screen, children included. |
| ...props | ComponentProps<"div"> | — | Every other native <div> attribute is forwarded; className is merged last with cn(). Give the root a height (or content) and any layout classes; they apply to the children directly. |
Usage
Needs the stylesheets from @cybr/tokens and @cybr/effects. After the install, import them in app/globals.css:
@import "tailwindcss";
@import "../styles/cybr-tokens.css";
@import "../styles/cybr-effects.css";
import { CrtOverlay } from "@/components/ui/crt-overlay";
export function SignalLost() {
return (
<CrtOverlay className="flex min-h-80 items-center justify-center">
<p className="font-display text-display-md text-signal">NO CARRIER</p>
</CrtOverlay>
);
}
States
- Running: scanlines and vignette over the children; the screen dips in brightness twice every 4s.
- No vignette / no flicker: turn either off with its prop.
- Reduced motion: no flicker; scanlines and vignette stay.
- Unlike the other backgrounds, the scanlines and vignette sit above the children. Children with their own
z-indexabove 10 will cover them.
Accessibility
- The overlays are
aria-hiddenand never take pointer events, so links and buttons inside still work. - The flicker is 0.5 dips per second, far under the WCAG 2.3.1 limit of 3 flashes per second, and only dims to 94%.
- The flicker cannot be paused. It stops only under
prefers-reduced-motionor withflicker={false}. Beside other content for more than 5 seconds, that falls short of WCAG 2.2.2 (Pause, Stop, Hide). - Scanlines darken the children by up to 45% on alternate lines. Use large
--signalor--inktext, and avoid small--labeltext for anything people must read.