Motiva
Мои коллекции
← каталог

Grid Beams

Фон-сетка с бегущими лучами: кометы с градиентными хвостами летят по горизонталям и вертикалям решётки.

cssautobasic0reduced-motion ✓

Пропсы

Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.

ПропТипПо умолчаниюЧто это
backdrop?string | falseBACKDROPПодложка (сама сетка + база): CSS-значение `background` или `false`. `false` нужен, когда запись стоит фоном СЕКЦИИ и подложку задаёт она.
children?ReactNodeСодержимое поверх сетки. В витрине — подпись, в секции обычно пусто.

Установка

npx shadcn@latest add https://motiva.pages.dev/r/grid-beams.json
Что нужно в проекте до установки
  1. components.json в корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.
  2. Алиас @ объявлен дважды: "paths": {"@/*": ["./src/*"]} в корневом tsconfig.json (в Vite он часто содержит только references — тогда CLI создаёт папку@) и resolve.alias в конфиге сборщика.
  3. Токены подключены один раз на приложение:import "@/styles/motiva-tokens.css" — они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему.
  4. Своя типографика не должна спорить: если у вас на обёрткеtext-align: center, он протечёт внутрь блоков.
  5. Проект без TypeScript? Всё равно нужен минимальныйtsconfig.json с pathsи "tsx": true в components.json: CLI грузит tsconfig безусловно, а с "tsx": false идёт за jsconfig.json и падает так же. Приехавший рядом.tsx можно не трогать — вам нужен.vanilla.js.

Экспорт

↓ Скачать .zip

Код

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>{`
        /* размерный контейнер: путь луча меряется в РОДИТЕЛЕ (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; } }
      `}</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>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva); идея-референс — Aceternity grid beams (MIT)v1.0.1