Workspace / component / backgrounds

Noise Light Background

Bright surface with a film-grain noise pass and a faint vignette. Adds tooth to flat layouts without changing their color story.

Workspacecomponentbackgrounds

paxfx add blocks/component-backgrounds-noise-lightlocal only

Tier: low. Reduced motion: Static grain frame.

"use client";

import type { ReactNode } from "react";
import { cn, useReducedMotion } from "@pax-sh/blocks";

export interface NoiseLightBackgroundProps {
  /** 0 to 1; how visible the grain pass is. */
  grain?: number;
  /** 0 to 1; strength of the edge vignette. */
  vignette?: number;
  className?: string;
  children?: ReactNode;
}

/**
 * Bright surface with a film-grain noise pass and a faint vignette. The grain
 * is an inline SVG turbulence tile shifted through a short stepped loop so it
 * shimmers like film; only transform animates. Under reduced motion the grain
 * holds a single static frame.
 */
export function NoiseLightBackground({
  grain = 0.5,
  vignette = 0.5,
  className,
  children,
}: NoiseLightBackgroundProps) {
  const reduced = useReducedMotion();
  const noiseTile =
    "data:image/svg+xml;utf8," +
    encodeURIComponent(
      '<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.82" numOctaves="2" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter><rect width="180" height="180" filter="url(#n)" opacity="1"/></svg>',
    );

  return (
    <div
      className={cn(
        "relative isolate overflow-hidden rounded-2xl border border-[var(--pax-border,rgba(18,17,15,0.12))]",
        "bg-[var(--pax-paper,#f6f2ea)] text-[var(--pax-ink,#12110f)]",
        className,
      )}
      data-pax-asset="component-backgrounds-noise-light"
    >
      <style>{`
        @keyframes pax-noise-light-shimmer {
          0% { transform: translate3d(0, 0, 0); }
          25% { transform: translate3d(-34px, 18px, 0); }
          50% { transform: translate3d(22px, -30px, 0); }
          75% { transform: translate3d(-16px, -12px, 0); }
          100% { transform: translate3d(0, 0, 0); }
        }
      `}</style>
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0"
        style={{
          background: "linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0) 45%)",
        }}
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -inset-10 mix-blend-multiply will-change-transform"
        style={{
          backgroundImage: `url("${noiseTile}")`,
          backgroundSize: "180px 180px",
          opacity: 0.24 * grain,
          animation: reduced ? "none" : "pax-noise-light-shimmer 1.6s steps(4, jump-none) infinite",
        }}
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0"
        style={{
          background: `radial-gradient(115% 90% at 50% 45%, transparent 62%, color-mix(in srgb, var(--pax-ink,#12110f) ${Math.round(
            18 * vignette,
          )}%, transparent) 100%)`,
        }}
      />
      <div className="relative z-[1]">{children}</div>
    </div>
  );
}