Backgrounds
Noise Texture
An SVG turbulence grain layer that gives flat fills and gradients the tooth of print — optionally jittering like film.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/noise-texture.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "NoiseTexture" component — an SVG grain
overlay that adds texture on top of whatever background is already there. Its
only dependency is a cn() class merger (clsx + tailwind-merge). It IS a client
component ("use client") for exactly one reason: React.useId().
Contract
- export function NoiseTexture(props): props extend
Omit<React.ComponentProps<"div">, "children"> (rest props + ref spread onto
the root div) plus:
- opacity?: number (default 0.15) — opacity of the grain layer.
- baseFrequency?: number (default 0.8) — feTurbulence frequency; higher is
finer sand, lower is coarser cloudy grain.
- grain?: "static" | "animated" (default "static").
- blend?: "overlay" | "soft-light" | "normal" (default "normal") — how the
grain composites onto what is beneath it.
- It renders no children: it is the topmost decoration layer. The consumer puts
it inside a `relative` ancestor, after any other background layer, and writes
content in a `relative` sibling that stacks above it.
Behavior
- Root div: pointer-events-none absolute inset-0 overflow-hidden, aria-hidden,
with inline style { opacity, mixBlendMode: blend, ...consumerStyle }.
- Inside sits an <svg> holding <defs><filter id={filterId}> with
<feTurbulence type="fractalNoise" baseFrequency={baseFrequency}
numOctaves={3} stitchTiles="stitch" /> followed by
<feColorMatrix type="saturate" values="0" />, and a
<rect width="100%" height="100%" filter={`url(#${filterId})`} />. The rect is
only a canvas for the filter — turbulence generates its own pixels, so no
fill color is involved.
- UNIQUE FILTER ID IS MANDATORY. Two instances on one page with a hardcoded id
make the second silently reuse the first one's filter, so every instance
derives its id from React.useId(). useId() emits characters that are illegal
in an XML id (and therefore inside url(#…)), so strip them:
`zy-noise-${React.useId().replace(/[^a-zA-Z0-9]/g, "")}`. This is the only
reason the component is client-side.
- The svg layer is oversized and offset (-left-[6%] -top-[6%], 112% square) so
the animated variant can move without ever uncovering an edge.
- grain="animated": a keyframe walks the layer between five small translate
poses with steps(1) timing, so the grain JUMPS like film stock instead of
sliding. Only transform animates — the expensive filter is never re-evaluated.
- The @keyframes ship inside the component via a React 19 hoisted
<style href="zyeon-noise-texture" precedence="medium"> tag — no Tailwind
config edits, and multiple instances dedupe to one style tag by href.
- prefers-reduced-motion: motion-reduce:[animation:none] — the grain stops
jumping but stays rendered, degrading exactly to the "static" variant.
Rendering & styling
- No color tokens are needed and none are hardcoded: feTurbulence generates the
pixels and feColorMatrix saturate=0 makes them neutral grey, so the layer
never tints the theme colors underneath. No hex / rgb() / oklch() anywhere.
- Blend choice is a real trade-off, document it: "overlay" and "soft-light"
preserve the hue underneath but collapse to nothing over near-white or
near-black surfaces, which is why "normal" — a neutral grey veil that is
visible on any lightness — is the default.
- Keep opacity low. Above ~0.5 the grain stops being a finish and starts
fighting the content for attention.
- Merge consumer className via cn() so the call site can scope the grain to
part of the surface or restack it.
Customization levers
- Strength: opacity is the master dial — 0.08–0.2 is a finish, 0.3–0.45 is a
visible film look.
- Particle size: baseFrequency — 0.4–0.6 for coarse, cloudy grain, 1.2–2 for
fine sand. Pair coarse grain with lower opacity.
- Depth: numOctaves (3 here) — 1 gives clean single-scale noise, 4–5 gives
clumpier, more organic grain at a higher render cost.
- Composite: blend — "overlay"/"soft-light" on photos and mid-tone gradients,
"normal" on flat theme surfaces.
- Tint: drop the feColorMatrix and the grain keeps feTurbulence's RGB, or swap
it for a saturate value between 0 and 1 for a subtly colored film emulsion.
- Motion: the jitter step (0.6s) and the translate distances in the keyframe;
smaller steps read as electrical noise, larger as projector flicker.
- Scope: it is just an absolute layer — put it on a card, a modal header or a
single hero rather than the whole page, and stack it above another background
component (gradient-mesh, aurora, an image) as the last child.Concepts
- Finish, not subject — the component paints no color of its own; it is the last layer over an existing fill or gradient, which is why its default opacity is low and it exposes a blend mode instead of a palette.
- Unique filter id — SVG filter ids are document-global, so two instances sharing one id silently collapse into one filter; deriving the id from
useId()(with illegal XML characters stripped) is what makes the component safe to use more than once per page. - Transform-only animation — the film jitter moves the already-rendered layer instead of re-running
feTurbulence, so animated grain costs a composite rather than a filter pass every frame. - steps(1) timing — discrete jumps between poses read as photographic grain; the same keyframe with smooth interpolation reads as a sliding texture and breaks the illusion.
- Blend-mode trade-off —
overlayandsoft-lightkeep the underlying hue but vanish on near-white or near-black surfaces, sonormalis the honest default for arbitrary theme backgrounds. - Reduced-motion honesty — under
prefers-reduced-motionthe layer degrades to the static variant instead of disappearing: the texture, which is the whole point, stays.