COMPONENT / data-rain
Data Rain_
Falling --label glyph columns with one --signal column, fading into --ground-void. Pure CSS; static 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/data-rain
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
05 / DATA RAIN
GREY BY DEFAULT · ONE SIGNAL COLUMN
registry/ui/data-rain.tsx
import type { ComponentProps, CSSProperties } from "react";
import { cn } from "@/lib/utils";
const GLYPHS = "01#%&@$*<>/\\|=+ΞΣ¥ア";
// Deterministic, so server and client markup always match.
function rainColumn(i: number) {
const length = 14 + ((i * 7) % 12);
let text = "";
for (let j = 0; j < length; j++) {
text += GLYPHS[(i * 13 + j * 5) % GLYPHS.length];
}
return {
text,
opacity: 0.25 + ((i * 3) % 5) * 0.12,
duration: `${(3 + ((i * 5) % 7) * 0.6).toFixed(1)}s`,
delay: `${(-((i * 11) % 9) * 0.5).toFixed(1)}s`,
};
}
// Falling glyph columns in --label, with one --signal column, fading out into
// --ground-void at the bottom. Purely decorative: the whole layer is
// aria-hidden and sits behind the children. Under reduced motion the columns
// rest at the top.
export function DataRain({
columns = 18,
signalColumn = 7,
className,
children,
...props
}: ComponentProps<"div"> & { columns?: number; signalColumn?: number }) {
return (
<div className={cn("relative isolate overflow-hidden bg-ground-void", className)} {...props}>
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 font-mono text-sm leading-[1.15]">
{Array.from({ length: columns }, (_, i) => {
const column = rainColumn(i);
const signal = i === signalColumn;
return (
<span
key={i}
className={cn("data-rain absolute top-0 w-3.5 break-all", signal ? "text-signal" : "text-label")}
style={
{
left: `calc(${i} * 100% / ${columns} + 4px)`,
opacity: signal ? 1 : column.opacity,
"--rain-duration": column.duration,
"--rain-delay": column.delay,
} as CSSProperties
}
>
{column.text}
</span>
);
})}
<div className="absolute inset-0 bg-linear-to-b from-transparent from-40% to-ground-void" />
</div>
{children}
</div>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | number | 18 | How many glyph columns, spread evenly across the width. |
| signalColumn | number | 7 | Zero-based index of the one --signal column. Any index outside 0 to columns − 1 means all columns are grey. |
| ...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 { DataRain } from "@/components/ui/data-rain";
export function Uplink() {
return (
<DataRain className="flex min-h-96 items-end p-10">
<p className="bg-ground-deep px-3 py-2 font-mono text-sm text-ink">UPLINK ESTABLISHED</p>
</DataRain>
);
}
States
- Running: columns fall in hard steps at different speeds and offsets, fading out at the bottom.
- Reduced motion: the columns rest at the top, still and readable as texture.
- Columns are generated from their index, not at random, so the server and browser render the same markup.
- Some glyphs (such as ア) are not in JetBrains Mono and fall back to a system font.
Accessibility
- The whole rain layer is
aria-hidden, so screen readers never hear the glyphs, and it never takes pointer events. - The columns move in small steps; they do not flash.
- The motion cannot be paused. It stops only under
prefers-reduced-motion. Beside other content for more than 5 seconds, that falls short of WCAG 2.2.2 (Pause, Stop, Hide). - Set readable text on a solid
--ground-deeppanel, as in the example, so falling glyphs never sit behind it.