COMPONENT / perspective-grid
Perspective Grid_
Signal floor grid tilted toward a glowing horizon, scrolling toward the viewer. 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/perspective-grid
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
02 / PERSPECTIVE GRID
DESATURATED · NO SUNSET GRADIENTS
registry/ui/perspective-grid.tsx
import type { ComponentProps, CSSProperties } from "react";
import { cn } from "@/lib/utils";
// Signal floor grid tilted toward a glowing horizon at 46% height, scrolling
// toward the viewer. Desaturated ground, no sunset gradient. The layers are
// aria-hidden and sit behind the children. Static under reduced motion.
export function PerspectiveGrid({
cellSize = 48,
className,
children,
...props
}: ComponentProps<"div"> & { cellSize?: number }) {
const line = (to: string) =>
`linear-gradient(${to}, var(--signal) var(--border-base), transparent var(--border-base))`;
const floor = {
"--perspective-cell": `${cellSize}px`,
backgroundImage: `${line("to bottom")}, ${line("to right")}`,
backgroundSize: `${cellSize}px ${cellSize}px`,
transform: "rotateX(64deg)",
maskImage: "linear-gradient(to bottom, transparent, black 35%)",
} as CSSProperties;
return (
<div className={cn("relative isolate overflow-hidden bg-ground-void", className)} {...props}>
{/* Perspective lives on this layer, not in the floor's transform: Chrome otherwise paints the tilted floor over the children. */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 perspective-[240px] perspective-origin-[50%_46%]"
>
<div className="absolute inset-x-0 top-[46%] h-(--border-base) bg-signal shadow-[0_0_18px_var(--signal)]" />
<div className="perspective-scroll absolute inset-x-[-60%] top-[46%] h-[120%] origin-top opacity-60" style={floor} />
</div>
{children}
</div>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| cellSize | number | 48 | Grid cell size in px before the tilt. The scroll moves one cell per 1.4s. |
| ...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 { PerspectiveGrid } from "@/components/ui/perspective-grid";
export function Horizon() {
return (
<PerspectiveGrid className="flex min-h-96 items-start justify-center pt-16">
<h1 className="font-display text-display-lg text-ink uppercase">Sector 7</h1>
</PerspectiveGrid>
);
}
States
- Running: the floor grid scrolls toward the viewer below a glowing
--signalhorizon at 46% height. - Reduced motion: the grid is still; the horizon and grid stay visible.
- The monolith block in the preview is demo content from the artboard, not part of the component.
Accessibility
- The horizon and floor are one
aria-hiddenlayer behind the children and never take pointer events. - The motion is a smooth slide with no flashing.
- 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). - Keep text above the horizon, on
--ground-void. Text over the grid lines loses contrast.