Backgrounds

Noise Texture

A static SVG noise overlay that adds grain and depth to flat surfaces.

Install

npx shadcn@latest add @paragon/noise-texture

noise-texture.tsx

import * 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}
    />
  );
}