Grid Beams
Фон-сетка с бегущими лучами: кометы с градиентными хвостами летят по горизонталям и вертикалям решётки.
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| backdrop? | string | false | BACKDROP | Подложка (сама сетка + база): CSS-значение `background` или `false`. `false` нужен, когда запись стоит фоном СЕКЦИИ и подложку задаёт она. |
| children? | ReactNode | — | Содержимое поверх сетки. В витрине — подпись, в секции обычно пусто. |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/grid-beams.jsonЧто нужно в проекте до установки
Файл
components.jsonв корнеИз него CLI узнаёт, куда класть файлы. Алиасы — в такой форме:
"aliases": { "components": "@/components", "lib": "@/lib" }Нет файла — установка не начнётся вовсе.
Алиас
@объявлен дваждыВ корневом
tsconfig.jsonи в конфиге сборщика. В Vite корневой tsconfig часто содержит толькоreferences— этого мало."paths": { "@/*": ["./src/*"] } // tsconfig.json resolve.alias = { "@": "/src" } // vite.config.tsБез
pathsCLI молча создаёт папку с именем@.Токены подключены один раз на приложение
Они приезжают вместе с записью — достаточно импортировать их в точке входа:
import "@/styles/motiva-tokens.css";Без них блок потеряет цвета, ритм и тему.
Своя типографика не спорит с блоком
Наследуемые свойства протекают внутрь:
text-align: centerна обёртке отцентрует содержимое секции. То же касаетсяfont-familyиline-height.Проект без TypeScript? всё равно нужен tsconfig
CLI грузит
tsconfig.jsonбезусловно, а при"tsx": falseидёт заjsconfig.jsonи падает так же. Минимум — файл сpathsи"tsx": trueвcomponents.json.Приехавший рядом
.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
const css = `/* размерный контейнер: путь луча меряется в РОДИТЕЛЕ (cqw/cqh), а не во вьюпорте.
С 100vw в секции уже своей ширины луч улетал далеко за кромку и возвращался
с паузой — сетка казалась то живой, то мёртвой */
.gb { container-type: size; }
.gb-beam { position: absolute; border-radius: 999px; animation: var(--gb-anim) linear infinite; }
.gb-beam[data-dir="h"] { width: 130px; height: 2px; left: -140px; --gb-anim: gb-h var(--gb-dur); }
.gb-beam[data-dir="v"] { width: 2px; height: 130px; top: -140px; --gb-anim: gb-v var(--gb-dur); }
@keyframes gb-h { to { transform: translateX(calc(100cqw + 280px)); } }
@keyframes gb-v { to { transform: translateY(calc(100cqh + 280px)); } }
@media (prefers-reduced-motion: reduce) { .gb-beam { display: none; } }
`;
import type { CSSProperties, ReactNode } from "react";
const CELL = 48; // шаг сетки, px — лучи привязаны к линиям кратно ему
/** Подложка по умолчанию — как в витрине. */
const BACKDROP = `repeating-linear-gradient(180deg, transparent 0 ${CELL - 1}px, oklch(0.3 0.02 270 / 0.5) ${CELL - 1}px ${CELL}px), repeating-linear-gradient(90deg, transparent 0 ${CELL - 1}px, oklch(0.3 0.02 270 / 0.5) ${CELL - 1}px ${CELL}px), oklch(0.15 0.015 270)`;
export interface GridBeamsProps {
/**
* Подложка (сама сетка + база): CSS-значение `background` или `false`.
* `false` нужен, когда запись стоит фоном СЕКЦИИ и подложку задаёт она.
*/
backdrop?: string | false;
/** Содержимое поверх сетки. В витрине — подпись, в секции обычно пусто. */
children?: ReactNode;
}
const BEAMS: { dir: "h" | "v"; cellIndex: number; delay: number; dur: number; hue: number }[] = [
{ dir: "h", cellIndex: 2, delay: 0, dur: 3.2, hue: 300 },
{ dir: "h", cellIndex: 5, delay: 1.4, dur: 4.1, hue: 210 },
{ dir: "v", cellIndex: 5, delay: 0.7, dur: 3.6, hue: 150 },
{ dir: "v", cellIndex: 12, delay: 2.2, dur: 3, hue: 20 },
];
/**
* Фон-сетка с бегущими лучами: линии — repeating-gradient, «кометы» с градиентным хвостом
* летят по горизонталям и вертикалям. Чистый CSS. Reduced-motion/data-shot — только сетка.
*/
export function GridBeams({ backdrop = BACKDROP, children }: GridBeamsProps = {}) {
return (
<div
className="gb"
style={{
position: "absolute",
inset: 0,
overflow: "hidden",
// ВАЖНО: горизонтальные линии — 180deg (ось сверху вниз). С 0deg ось идёт снизу вверх,
// и повтор отсчитывается от НИЖНЕЙ кромки — тогда линии не совпадают с `top` лучей.
...(backdrop === false ? null : { background: backdrop }),
}}
>
<style href="motiva-grid-beams" precedence="medium">{css}</style>
{BEAMS.map((b, i) => (
<span
key={i}
className="gb-beam"
data-dir={b.dir}
style={
{
// центр луча (2px) точно на линии сетки (1px на границе клетки)
[b.dir === "h" ? "top" : "left"]: b.cellIndex * CELL - 1.5,
"--gb-dur": `${b.dur}s`,
animationDelay: `${b.delay}s`,
background:
b.dir === "h"
? `linear-gradient(90deg, transparent, oklch(0.75 0.16 ${b.hue}))`
: `linear-gradient(180deg, transparent, oklch(0.75 0.16 ${b.hue}))`,
boxShadow: `0 0 12px oklch(0.75 0.16 ${b.hue} / 0.7)`,
} as CSSProperties
}
/>
))}
{/* подпись витрины приезжает из preview.tsx: зашитый текст поверх фона печатался бы
и в чужой секции — ровно та причина, по которой запись нельзя было переиспользовать */}
{children && (
<div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}>{children}</div>
)}
</div>
);
}