A static SVG noise overlay that adds grain and depth to flat surfaces.
npx shadcn@latest add @paragon/noise-textureimport * as React from "react";
import { cn } from "@/lib/utils";
export interface NoiseTextureProps extends React.ComponentProps<"div"> {
/** Grain opacity in the light theme, 0–1. */
opacity?: number;
/** Grain opacity in the dark theme, 0–1. Dark surfaces read flatter, so slightly more. */
darkOpacity?: number;
/** feTurbulence base frequency. Higher = finer grain. */
baseFrequency?: number;
/** feTurbulence octave count. More octaves = denser, richer grain. */
numOctaves?: number;
/** Repeating tile size in px. Smaller tiles = tighter grain, larger = airier. */
size?: number;
/** Turbulence seed. Change it to get a different (still deterministic) pattern. */
seed?: number;
}
/**
* A static film-grain overlay that adds tooth and depth to flat surfaces.
*
* Technique: an inline SVG feTurbulence (fractal noise, desaturated) is
* embedded as a data-URI background and tiled with `stitchTiles`, so the
* grain is a single cached raster — zero animation, zero runtime cost.
* Opacity is tuned per theme via CSS variables. Absolutely positioned —
* the parent needs `position: relative` (and `overflow-hidden` if rounded).
*/
export function NoiseTexture({
opacity = 0.05,
darkOpacity = 0.08,
baseFrequency = 0.65,
numOctaves = 3,
size = 128,
seed = 0,
className,
style,
...props
}: NoiseTextureProps) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><filter id="n" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="${baseFrequency}" numOctaves="${numOctaves}" seed="${seed}" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter><rect width="100%" height="100%" filter="url(#n)"/></svg>`;
return (
<div
aria-hidden
data-slot="noise-texture"
className={cn(
"pointer-events-none absolute inset-0 opacity-[var(--nt-opacity)] dark:opacity-[var(--nt-opacity-dark)]",
className,
)}
style={
{
"--nt-opacity": opacity,
"--nt-opacity-dark": darkOpacity,
backgroundImage: `url("data:image/svg+xml,${encodeURIComponent(svg)}")`,
backgroundRepeat: "repeat",
backgroundSize: `${size}px ${size}px`,
...style,
} as React.CSSProperties
}
{...props}
/>
);
}