← каталог
Noise / Grain
Плёночное зерно поверх любого фона: SVG feTurbulence как data-URI + steps()-джиттер — зерно «живёт» как на плёнке.
cssautobasic0reduced-motion ✓
🎛 Настройки — крути и копируй
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| children? | ReactNode | — | |
| opacity? | number | 0.25 | Непрозрачность зерна 0–1 (формат «0.25»). |
| size? | number | 240 | Размер тайла зерна, px (формат «240»): меньше — мельче зерно. |
| blend? | "screen" | "overlay" | "soft-light" | "normal" | "screen" | Режим смешивания плёнки. |
| animated? | boolean | true | Живое зерно (лёгкий джиттер кадрами) или статичная плёнка. |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/noise-grain.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";
/** SVG-шум (feTurbulence) как data-URI — без внешних ассетов и canvas. */
const NOISE_URI =
"data:image/svg+xml," +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter><rect width="240" height="240" filter="url(%23n)" opacity="0.85"/></svg>`,
);
export interface NoiseGrainProps {
children?: ReactNode;
/** Непрозрачность зерна 0–1 (формат «0.25»). */
opacity?: number;
/** Размер тайла зерна, px (формат «240»): меньше — мельче зерно. */
size?: number;
/** Режим смешивания плёнки. */
blend?: "screen" | "overlay" | "soft-light" | "normal";
/** Живое зерно (лёгкий джиттер кадрами) или статичная плёнка. */
animated?: boolean;
}
/**
* Плёночное зерно поверх любого фона: SVG feTurbulence data-URI + steps()-джиттер позиции —
* зерно «живёт» как на плёнке. Чистый CSS. Reduced-motion/data-shot — зерно статично.
*/
export function NoiseGrain({ children, opacity = 0.25, size = 240, blend = "screen", animated = true }: NoiseGrainProps) {
return (
<div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
<style>{`
/* джиттер через transform (композитор), НЕ background-position — тот перекрашивает
весь полноэкранный слой на каждый шаг */
@keyframes ng-jitter {
0% { transform: translate(0, 0); } 25% { transform: translate(-70px, 40px); }
50% { transform: translate(40px, -90px); } 75% { transform: translate(-30px, -50px); }
100% { transform: translate(60px, 70px); }
}
.ng-film { animation: ng-jitter 0.7s steps(5) infinite; will-change: transform; }
@media (prefers-reduced-motion: reduce) { .ng-film { animation: none; } }
`}</style>
{children ?? (
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(80% 90% at 30% 20%, oklch(0.45 0.12 300 / 0.7), transparent 60%), radial-gradient(70% 80% at 75% 75%, oklch(0.5 0.12 220 / 0.55), transparent 60%), oklch(0.16 0.02 270)",
display: "grid",
placeItems: "center",
color: "oklch(0.92 0.01 265)",
}}
>
<div style={{ textAlign: "center" }}>
<div style={{ fontSize: "1.8rem", fontWeight: 700, letterSpacing: "-0.02em" }}>Noise / Grain</div>
<div style={{ marginTop: 6, fontFamily: "ui-monospace, monospace", fontSize: "0.75rem", opacity: 0.6 }}>
плёночное зерно поверх градиента
</div>
</div>
</div>
)}
<div
className={animated ? "ng-film" : undefined}
aria-hidden
style={{
position: "absolute",
inset: "-120px",
backgroundImage: `url("${NOISE_URI}")`,
backgroundSize: `${size}px ${size}px`,
opacity,
pointerEvents: "none",
mixBlendMode: blend as CSSProperties["mixBlendMode"],
}}
/>
</div>
);
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0