Workspace / component / backgrounds

Paper Grain Background

Warm paper surface with a fine procedural grain and a soft top-light gradient. The default Pax editorial canvas.

Workspacecomponentbackgrounds

paxfx add blocks/component-backgrounds-paper-grainlocal only

Tier: low. Reduced motion: Fully static surface.

"use client";

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

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

/**
 * Warm paper surface with a fine procedural grain and a soft top light.
 * The grain is an inline SVG turbulence tile, so there are no image assets
 * and the texture stays crisp at every device pixel ratio. Fully static.
 */
export function PaperGrainBackground({
  grain = 0.5,
  className,
  children,
}: PaperGrainBackgroundProps) {
  const noiseTile =
    "data:image/svg+xml;utf8," +
    encodeURIComponent(
      '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter><rect width="160" height="160" 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-paper-grain"
    >
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0"
        style={{
          background:
            "linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 38%), radial-gradient(120% 70% at 50% -10%, rgba(255,252,242,0.9) 0%, rgba(255,252,242,0) 60%)",
        }}
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 mix-blend-multiply"
        style={{
          backgroundImage: `url("${noiseTile}")`,
          backgroundSize: "160px 160px",
          opacity: 0.14 * grain * 2,
        }}
      />
      <div className="relative z-[1]">{children}</div>
    </div>
  );
}