← каталог
Meteors
Метеоры: светящиеся росчерки падают по диагонали, каждый со своей задержкой и скоростью. Чистый CSS; позиции сеяно-случайны — без рассинхрона гидрации.
cssautobasic0reduced-motion ✓
🎛 Настройки — крути и копируй
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| backdrop? | string | false | BACKDROP | Подложка (небо): CSS-значение `background` или `false`. `false` нужен, когда запись стоит фоном СЕКЦИИ и подложку задаёт она. |
| count? | number | 10 | Сколько метеоров в небе. |
| speed? | number | 1 | Множитель скорости (1 — базовая). |
| hue? | number | 265 | Тон хвоста, градусы oklch. |
| children? | ReactNode | — | Содержимое поверх. В витрине — подпись, в секции обычно пусто. |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/meteors.jsonЧто нужно в проекте до установки
components.jsonв корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.- Алиас
@объявлен дважды:"paths": {"@/*": ["./src/*"]}в корневомtsconfig.json(в Vite он часто содержит толькоreferences— тогда CLI создаёт папку@) иresolve.aliasв конфиге сборщика. - Токены подключены один раз на приложение:
import "@/styles/motiva-tokens.css"— они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему. - Своя типографика не должна спорить: если у вас на обёртке
text-align: center, он протечёт внутрь блоков. - Проект без TypeScript? Всё равно нужен минимальный
tsconfig.jsonсpathsи"tsx": trueвcomponents.json: CLI грузит tsconfig безусловно, а с"tsx": falseидёт заjsconfig.jsonи падает так же. Приехавший рядом.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
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