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…

"use client"

import * as React from "react"
import { cn } from "@/lib/utils"

/**
 * Keyframes ship inside the component via React 19 hoisted <style> —
 * no tailwind config edits, and duplicates dedupe by href.
 * steps(1) makes the grain jump between poses like film stock instead of
 * sliding; the layer is oversized so a jump never uncovers an edge.
 */
const KEYFRAMES = `@keyframes zy-noise-jitter{0%{transform:translate(0,0)}20%{transform:translate(-2%,1%)}40%{transform:translate(1%,-2%)}60%{transform:translate(-1%,-1%)}80%{transform:translate(2%,1%)}100%{transform:translate(0,0)}}`

export type NoiseGrain = "static" | "animated"

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/noise-texture.json

Prompt

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-offoverlay and soft-light keep the underlying hue but vanish on near-white or near-black surfaces, so normal is the honest default for arbitrary theme backgrounds.
  • Reduced-motion honesty — under prefers-reduced-motion the layer degrades to the static variant instead of disappearing: the texture, which is the whole point, stays.

On This Page