Workspace / component / backgrounds
Paper Grain Background
paxfx add blocks/component-backgrounds-paper-grainlocal onlyTier: 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>
);
}