Web InnoventixFreeCode

Noise Background

Original · free

noise/grain gradient background

byWeb InnoventixReact + Tailwind
bgxnoisebackgrounds
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/bgx-noise.json
bgx-noise.tsx
"use client"

import { useId, useMemo, useState } from "react"
import { motion, useReducedMotion } from "motion/react"

type Palette = { id: string; name: string; stops: [string, string, string, string] }
type Blend = "overlay" | "soft-light"

const PALETTES: Palette[] = [
  { id: "aurora", name: "Aurora", stops: ["#6366f1", "#8b5cf6", "#22d3ee", "#34d399"] },
  { id: "ember", name: "Ember", stops: ["#fb7185", "#f59e0b", "#f43f5e", "#fb923c"] },
  { id: "tide", name: "Tide", stops: ["#0ea5e9", "#6366f1", "#22d3ee", "#2dd4bf"] },
  { id: "dusk", name: "Dusk", stops: ["#a78bfa", "#f472b6", "#818cf8", "#fbbf24"] },
]

const BLOBS: { pos: string; size: string; dx: number; dy: number; dur: number }[] = [
  { pos: "left-[-8%] top-[-14%]", size: "h-[38rem] w-[38rem]", dx: 52, dy: 34, dur: 19 },
  { pos: "right-[-12%] top-[-8%]", size: "h-[34rem] w-[34rem]", dx: -46, dy: 48, dur: 23 },
  { pos: "left-[16%] bottom-[-20%]", size: "h-[40rem] w-[40rem]", dx: 60, dy: -38, dur: 27 },
  { pos: "right-[4%] bottom-[-16%]", size: "h-[30rem] w-[30rem]", dx: -58, dy: -26, dur: 21 },
]

export default function BgxNoise() {
  const reduce = useReducedMotion()
  const uid = useId()

  const [paletteId, setPaletteId] = useState<string>(PALETTES[0].id)
  const [grain, setGrain] = useState<number>(0.55)
  const [fineness, setFineness] = useState<number>(0.62)
  const [blend, setBlend] = useState<Blend>("overlay")
  const [flicker, setFlicker] = useState<boolean>(true)

  const palette = PALETTES.find((p) => p.id === paletteId) ?? PALETTES[0]
  const grainPct = Math.round(grain * 100)

  const noiseUrl = useMemo(() => {
    const svg =
      "<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'>" +
      "<filter id='n'>" +
      "<feTurbulence type='fractalNoise' baseFrequency='" +
      fineness.toFixed(3) +
      "' numOctaves='2' stitchTiles='stitch'/>" +
      "<feColorMatrix type='saturate' values='0'/>" +
      "</filter>" +
      "<rect width='200' height='200' filter='url(#n)'/>" +
      "</svg>"
    return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`
  }, [fineness])

  const grainMoving = flicker && !reduce

  const segItem =
    "relative cursor-pointer select-none rounded-lg px-3 py-2 text-center text-sm font-medium " +
    "text-slate-600 transition-colors dark:text-slate-300"
  const segActive =
    "peer-checked:bg-white peer-checked:text-slate-900 peer-checked:shadow-sm " +
    "dark:peer-checked:bg-slate-700 dark:peer-checked:text-white " +
    "peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 " +
    "peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-900"

  return (
    <section className="relative flex min-h-[46rem] w-full items-center overflow-hidden bg-slate-50 dark:bg-slate-950">
      <style>{`
        @keyframes bgxNoise_grain {
          0%   { background-position: 0 0; }
          20%  { background-position: -14% 8%; }
          40%  { background-position: 12% -12%; }
          60%  { background-position: -8% -6%; }
          80%  { background-position: 10% 12%; }
          100% { background-position: 0 0; }
        }
        .bgxNoise-grain { animation: bgxNoise_grain 0.5s steps(1, end) infinite; }
        @media (prefers-reduced-motion: reduce) {
          .bgxNoise-grain { animation: none; }
        }
      `}</style>

      {/* Gradient blob field */}
      <div aria-hidden className="absolute inset-0 z-0">
        {BLOBS.map((b, i) => (
          <motion.div
            key={i}
            className={`absolute rounded-full opacity-60 blur-3xl will-change-transform dark:opacity-45 ${b.pos} ${b.size}`}
            style={{ backgroundColor: palette.stops[i % palette.stops.length] }}
            animate={
              reduce
                ? undefined
                : { x: [0, b.dx, 0], y: [0, b.dy, 0], scale: [1, 1.12, 1] }
            }
            transition={{ duration: b.dur, repeat: Infinity, ease: "easeInOut" }}
          />
        ))}
      </div>

      {/* Grain overlay */}
      <div
        aria-hidden
        className={`pointer-events-none absolute inset-0 z-[1] ${grainMoving ? "bgxNoise-grain" : ""}`}
        style={{
          backgroundImage: noiseUrl,
          backgroundSize: "200px 200px",
          opacity: grain,
          mixBlendMode: blend,
        }}
      />

      {/* Legibility scrim */}
      <div
        aria-hidden
        className="absolute inset-0 z-[2] bg-gradient-to-b from-slate-50/50 via-transparent to-slate-50/70 dark:from-slate-950/50 dark:to-slate-950/80"
      />

      {/* Content */}
      <div className="relative z-10 mx-auto w-full max-w-6xl px-6 py-24 sm:px-8">
        <div className="grid items-center gap-12 lg:grid-cols-[1.05fr_1fr]">
          {/* Hero copy */}
          <div>
            <span className="inline-flex items-center gap-2 rounded-full border border-slate-300/70 bg-white/60 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-slate-600 backdrop-blur dark:border-slate-700/70 dark:bg-slate-900/50 dark:text-slate-300">
              <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
              Backgrounds — Grain &amp; gradient
            </span>

            <h1 className="mt-6 text-balance text-4xl font-semibold leading-[1.05] tracking-tight text-slate-900 sm:text-5xl dark:text-white">
              Flat gradients look digital.
              <br />
              <span className="text-slate-500 dark:text-slate-400">Grain makes them feel printed.</span>
            </h1>

            <p className="mt-6 max-w-md text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-300">
              A fine dusting of film grain breaks the banding you get from any CSS gradient,
              adds an analog warmth, and holds up from a phone to a 5K panel. Every dial here
              re-renders the surface behind this panel in real time — no images, just an SVG
              turbulence filter.
            </p>

            <dl className="mt-8 flex flex-wrap gap-x-8 gap-y-4">
              <div>
                <dt className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                  Payload
                </dt>
                <dd className="mt-0.5 text-lg font-semibold tabular-nums text-slate-900 dark:text-white">
                  0 KB images
                </dd>
              </div>
              <div>
                <dt className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                  Rendered by
                </dt>
                <dd className="mt-0.5 text-lg font-semibold text-slate-900 dark:text-white">
                  feTurbulence
                </dd>
              </div>
              <div>
                <dt className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                  Grain now
                </dt>
                <dd className="mt-0.5 text-lg font-semibold tabular-nums text-slate-900 dark:text-white">
                  {grainPct}%
                </dd>
              </div>
            </dl>
          </div>

          {/* Control panel */}
          <div className="rounded-2xl border border-slate-200/80 bg-white/70 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-xl sm:p-8 dark:border-slate-700/60 dark:bg-slate-900/60 dark:shadow-black/30">
            <div className="flex items-baseline justify-between">
              <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Tune the texture</h2>
              <span className="text-xs font-medium tabular-nums text-slate-500 dark:text-slate-400">
                {palette.name} · {blend === "overlay" ? "Overlay" : "Soft light"}
              </span>
            </div>

            {/* Palette */}
            <fieldset className="mt-6">
              <legend className="mb-2 block text-sm font-medium text-slate-700 dark:text-slate-200">
                Palette
              </legend>
              <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
                {PALETTES.map((p) => (
                  <label key={p.id} className="block">
                    <input
                      type="radio"
                      name={`${uid}-palette`}
                      value={p.id}
                      checked={paletteId === p.id}
                      onChange={() => setPaletteId(p.id)}
                      className="peer sr-only"
                    />
                    <span
                      className={
                        "flex flex-col items-center gap-2 rounded-xl border border-slate-200 bg-slate-50/60 p-2.5 " +
                        "text-xs font-medium text-slate-600 transition-colors " +
                        "hover:border-slate-300 " +
                        "peer-checked:border-indigo-500 peer-checked:text-slate-900 " +
                        "peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 " +
                        "peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white " +
                        "dark:border-slate-700 dark:bg-slate-800/40 dark:text-slate-300 dark:hover:border-slate-600 " +
                        "dark:peer-checked:text-white dark:peer-focus-visible:ring-offset-slate-900"
                      }
                    >
                      <span
                        className="h-8 w-full rounded-md ring-1 ring-inset ring-black/5"
                        style={{
                          backgroundImage: `linear-gradient(135deg, ${p.stops.join(", ")})`,
                        }}
                      />
                      {p.name}
                    </span>
                  </label>
                ))}
              </div>
            </fieldset>

            {/* Grain amount */}
            <div className="mt-6">
              <div className="flex items-center justify-between">
                <label
                  htmlFor={`${uid}-grain`}
                  className="text-sm font-medium text-slate-700 dark:text-slate-200"
                >
                  Grain amount
                </label>
                <span className="text-sm font-semibold tabular-nums text-slate-500 dark:text-slate-400">
                  {grainPct}%
                </span>
              </div>
              <input
                id={`${uid}-grain`}
                type="range"
                min={0}
                max={1}
                step={0.01}
                value={grain}
                onChange={(e) => setGrain(Number(e.target.value))}
                aria-valuetext={`${grainPct} percent`}
                className="mt-2 h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
              />
            </div>

            {/* Grain fineness */}
            <div className="mt-5">
              <div className="flex items-center justify-between">
                <label
                  htmlFor={`${uid}-fine`}
                  className="text-sm font-medium text-slate-700 dark:text-slate-200"
                >
                  Grain fineness
                </label>
                <span className="text-sm font-semibold tabular-nums text-slate-500 dark:text-slate-400">
                  {fineness.toFixed(2)}
                </span>
              </div>
              <input
                id={`${uid}-fine`}
                type="range"
                min={0.3}
                max={1}
                step={0.01}
                value={fineness}
                onChange={(e) => setFineness(Number(e.target.value))}
                aria-valuetext={`base frequency ${fineness.toFixed(2)}`}
                className="mt-2 h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
              />
              <p className="mt-1.5 text-xs text-slate-500 dark:text-slate-400">
                Lower is coarse, chunky grain. Higher is fine, sandy grain.
              </p>
            </div>

            {/* Blend mode */}
            <fieldset className="mt-6">
              <legend className="mb-2 block text-sm font-medium text-slate-700 dark:text-slate-200">
                Blend mode
              </legend>
              <div className="grid grid-cols-2 gap-1 rounded-xl bg-slate-100 p-1 dark:bg-slate-800/70">
                {(["overlay", "soft-light"] as Blend[]).map((mode) => (
                  <label key={mode} className="block">
                    <input
                      type="radio"
                      name={`${uid}-blend`}
                      value={mode}
                      checked={blend === mode}
                      onChange={() => setBlend(mode)}
                      className="peer sr-only"
                    />
                    <span className={`${segItem} ${segActive}`}>
                      {mode === "overlay" ? "Overlay" : "Soft light"}
                    </span>
                  </label>
                ))}
              </div>
            </fieldset>

            {/* Flicker switch */}
            <div className="mt-6 flex items-center justify-between rounded-xl border border-slate-200 bg-slate-50/60 px-4 py-3 dark:border-slate-700 dark:bg-slate-800/40">
              <span className="pr-4">
                <span className="block text-sm font-medium text-slate-700 dark:text-slate-200">
                  Animate grain
                </span>
                <span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">
                  {reduce
                    ? "Paused — reduced motion is on"
                    : "Film-style flicker at ~10fps"}
                </span>
              </span>
              <button
                type="button"
                role="switch"
                aria-checked={flicker}
                aria-label="Animate grain"
                onClick={() => setFlicker((v) => !v)}
                className={
                  "relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors " +
                  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 " +
                  "focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 " +
                  (flicker ? "bg-indigo-500" : "bg-slate-300 dark:bg-slate-600")
                }
              >
                <span
                  className={
                    "inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform " +
                    (flicker ? "translate-x-6" : "translate-x-1")
                  }
                />
              </button>
            </div>

            {/* Live readout */}
            <p
              aria-live="polite"
              className="mt-5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs font-medium tabular-nums text-slate-500 dark:text-slate-400"
            >
              <span className="rounded-md bg-slate-100 px-2 py-1 dark:bg-slate-800">
                grain {grainPct}%
              </span>
              <span className="rounded-md bg-slate-100 px-2 py-1 dark:bg-slate-800">
                freq {fineness.toFixed(2)}
              </span>
              <span className="rounded-md bg-slate-100 px-2 py-1 dark:bg-slate-800">
                {blend}
              </span>
              <span className="rounded-md bg-slate-100 px-2 py-1 dark:bg-slate-800">
                {grainMoving ? "animating" : "static"}
              </span>
            </p>
          </div>
        </div>
      </div>
    </section>
  )
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →