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

Meteors

Метеоры: светящиеся росчерки падают по диагонали, каждый со своей задержкой и скоростью. Чистый CSS; позиции сеяно-случайны — без рассинхрона гидрации.

cssautobasic0reduced-motion ✓
🎛 Настройки — крути и копируй

Пропсы

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

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

Установка

npx shadcn@latest add https://motiva.pages.dev/r/meteors.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 BACKDROP = "linear-gradient(180deg, oklch(0.17 0.02 270), oklch(0.12 0.015 270))";

/**
 * Сеяный генератор (mulberry32): позиции метеоров «случайны», но детерминированы.
 * Настоящий Math.random() дал бы разные значения на сервере и на клиенте —
 * и React ругался бы на рассинхрон гидрации при каждом открытии.
 */
function mulberry32(seed: number) {
  let a = seed >>> 0;
  return () => {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

export interface MeteorsProps {
  /**
   * Подложка (небо): CSS-значение `background` или `false`.
   * `false` нужен, когда запись стоит фоном СЕКЦИИ и подложку задаёт она.
   */
  backdrop?: string | false;
  /** Сколько метеоров в небе. */
  count?: number;
  /** Множитель скорости (1 — базовая). */
  speed?: number;
  /** Тон хвоста, градусы oklch. */
  hue?: number;
  /** Содержимое поверх. В витрине — подпись, в секции обычно пусто. */
  children?: ReactNode;
}

/**
 * Метеоры: росчерки падают по диагонали, каждый со своей задержкой и скоростью.
 * Чистый CSS: голова — точка со свечением, хвост — псевдоэлемент-градиент,
 * полёт — один transform. Отрицательные задержки заселяют небо сразу, без «пустого старта».
 * Reduced-motion — метеоры скрыты, остаётся спокойное небо.
 */
export function Meteors({ backdrop = BACKDROP, count = 10, speed = 1, hue = 265, children }: MeteorsProps = {}) {
  const rand = mulberry32(1042);
  const items = Array.from({ length: Math.max(1, Math.round(count)) }, () => {
    const dur = (4 + rand() * 5) / Math.max(0.2, speed); // 4–9 c на пролёт до множителя
    return {
      left: -15 + rand() * 100, // cqw: часть стартует за левой кромкой
      top: -25 + rand() * 55, // cqh: часть выше кромки, часть сразу в кадре
      dur,
      delay: -rand() * dur, // отрицательная задержка: метеор уже в полёте на первом кадре
      scale: 0.7 + rand() * 0.6,
    };
  });
  const tail = `oklch(0.82 0.05 ${hue})`;
  return (
    <div
      className="mt-sky"
      style={{
        position: "absolute",
        inset: 0,
        overflow: "hidden",
        ...(backdrop === false ? null : { background: backdrop }),
      }}
    >
      <style>{`
        /* размерный контейнер: пролёт меряется в РОДИТЕЛЕ (cq-единицы), не во вьюпорте */
        .mt-sky { container-type: size; }
        .mt {
          position: absolute;
          width: 2px; height: 2px; border-radius: 999px;
          background: oklch(0.95 0.02 265 / 0.95);
          box-shadow: 0 0 6px 1px oklch(0.9 0.04 265 / 0.55);
          opacity: 0; /* до старта итерации и при data-shot метеора не видно */
          animation: mt-fly var(--mt-dur) linear infinite;
          animation-delay: var(--mt-delay);
        }
        /* хвост тянется ПРОТИВ движения (в минус-X локальной оси) */
        .mt::before {
          content: "";
          position: absolute;
          top: 50%; right: 1px;
          width: 90px; height: 1px;
          transform: translateY(-50%);
          background: linear-gradient(270deg, var(--mt-tail), transparent);
        }
        /* 35 градусов вниз-вправо; дистанция в cqmax покрывает контейнер любой пропорции */
        @keyframes mt-fly {
          0%   { transform: rotate(35deg) scale(var(--mt-scale)) translateX(0); opacity: 0; }
          6%   { opacity: 1; }
          80%  { opacity: 1; }
          100% { transform: rotate(35deg) scale(var(--mt-scale)) translateX(150cqmax); opacity: 0; }
        }
        @media (prefers-reduced-motion: reduce) { .mt { display: none; } }
      `}</style>
      {items.map((m, i) => (
        <span
          key={i}
          className="mt"
          style={
            {
              left: `${m.left.toFixed(2)}cqw`,
              top: `${m.top.toFixed(2)}cqh`,
              "--mt-dur": `${m.dur.toFixed(2)}s`,
              "--mt-delay": `${m.delay.toFixed(2)}s`,
              "--mt-scale": m.scale.toFixed(2),
              "--mt-tail": tail,
            } as CSSProperties
          }
        />
      ))}
      {children && (
        <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}>{children}</div>
      )}
    </div>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva); идея-референс — Magic UI Meteors (MIT)v1.0.0