---
name: noise-texture-overlay
description: Use when you want "Editorial, premium, subtle" - Adds subtle grain to reduce flatness and create a tactile finish.
---

# Noise Texture Overlay

> **Category:** Background  -  **Personality:** Editorial, premium, subtle
>
> **Best use:** Premium websites, gradients, dark UI
>
> **Ratings:** Professional 5/5 - Casual 3/5 - Exotic 3/5 - Visual impact 3/5 - Difficulty 1/5 - Performance cost 1/5

## What it is

A noise texture overlay lays a fine, semi-transparent sheet of generated grain over your whole UI. The grain comes from an inline SVG `<feTurbulence>` filter, sits in one layer with `pointer-events:none`, and is folded into the page with `mix-blend-mode`. It breaks up the visible banding in CSS gradients, gives flat dark surfaces a printed, tactile quality, and lends photography a filmic finish. It is a one-element, near-zero-cost effect that instantly stops a premium/editorial design from feeling digitally sterile.

## Dependencies / CDN

None - pure CSS + an inline SVG filter; the slider/toggle/before-after are vanilla JS. The demo also loads two Google Fonts (Newsreader + Spline Sans) purely for type - optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&family=Spline+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
```

## HTML

The effect is a single overlay placed above your content. (The demo wraps it in a draggable before/after split; the core is just the `.grain` div and its SVG.)

```html
<div class="stage">
  <!-- your premium UI here -->
  <main> ... </main>

  <!-- the noise: one overlay that never intercepts clicks -->
  <div class="grain" aria-hidden="true">
    <svg class="grain-tile" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
      <filter id="noise" x="0" y="0" width="100%" height="100%">
        <feTurbulence type="fractalNoise" baseFrequency="0.72" numOctaves="2" stitchTiles="stitch"/>
        <feColorMatrix type="matrix" values="1 0 0 0 0  1 0 0 0 0  1 0 0 0 0  0 0 0 0 1"/>
      </filter>
      <rect width="100%" height="100%" filter="url(#noise)"/>
    </svg>
  </div>
</div>
```

## CSS

```css
/* the stage owns a stacking context so the blend stays contained to it */
.stage{position:relative; isolation:isolate; overflow:hidden}

/* THE OVERLAY — this is the whole effect */
.grain{
  position:absolute; inset:0;       /* use position:fixed; inset:0 for whole-page grain */
  z-index:3;
  pointer-events:none;              /* never block the UI underneath */
  mix-blend-mode:overlay;           /* grain rides the light/dark of the UI, not a grey film */
  opacity:.3;                       /* dial intensity here: ~.1 subtle … ~.5 gritty */
}

/* a tile 2x the size, parked at -50%, so it can jitter without ever exposing an edge */
.grain-tile{position:absolute; top:-50%; left:-50%; width:200%; height:200%; display:block;
  animation:grain-flick .5s steps(1,end) infinite}   /* the film flicker */

@keyframes grain-flick{
  0%{transform:translate(0,0)}      11%{transform:translate(-6%,-3%)}  22%{transform:translate(5%,-6%)}
  33%{transform:translate(-4%,4%)}  44%{transform:translate(6%,3%)}    55%{transform:translate(-7%,-2%)}
  66%{transform:translate(2%,6%)}   77%{transform:translate(-3%,-6%)}  88%{transform:translate(4%,2%)}
  100%{transform:translate(0,0)}
}

/* reduced motion: keep the grain, drop the flicker */
@media (prefers-reduced-motion: reduce){ .grain-tile{animation:none} }
```

## JavaScript

Not required for the grain. The demo uses a little vanilla JS only to drive the intensity slider, the before/after split (a `clip-path` on `.grain`), and the flicker toggle:

```js
const stage     = document.querySelector('.stage');
const grain     = stage.querySelector('.grain');
const intensity = document.querySelector('#intensity');   // <input type="range" 0–100>

// intensity 0–100%  ->  overlay opacity 0–0.6
intensity.addEventListener('input', () => {
  grain.style.opacity = (intensity.value / 100 * 0.6).toFixed(3);
});

// before/after: clip the grain to the right of a draggable split (CSS var --split, 0–1)
function setSplit(p){ stage.style.setProperty('--split', Math.max(0, Math.min(1, p))); }
// .grain{ clip-path: inset(0 0 0 calc(var(--split,.5) * 100%)) }

// film flicker, auto-off when the OS asks for reduced motion
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
stage.classList.toggle('is-paused', reduce);   // .is-paused .grain-tile{ animation:none }
```

## How it works

- **`feTurbulence type="fractalNoise"`** generates Perlin-style noise during rasterisation. `baseFrequency` sets the grain size (higher = finer), `numOctaves` layers in detail, and `stitchTiles="stitch"` makes the tile seamless so it can repeat or translate without showing a seam.
- **`feColorMatrix`** flattens that coloured RGBA noise to opaque grey: the three `1 0 0 0 0` rows copy the red channel into green and blue, and the `0 0 0 0 1` alpha row forces full opacity. You end up with clean grey grain instead of rainbow static.
- **`mix-blend-mode: overlay`** is what makes it read as *texture on the UI* rather than a grey haze: mid-grey is near-identity, brighter noise lightens, darker noise darkens, so the grain modulates the colours already on screen.
- **`pointer-events:none`** lets every click, hover and selection pass straight through to the UI beneath.
- **The flicker is essentially free.** The costly turbulence is rendered once; the animation only `translate`s that cached tile a few percent in `steps()`, so it *jumps* between offsets like real film grain instead of recomputing noise every frame.

## Customization

- **Intensity:** the `opacity` of `.grain` (≈0.08 whisper → 0.5 gritty). The demo maps a slider straight onto it.
- **Grain size:** `baseFrequency` — `0.5` chunky, `0.72` filmic, `0.9+` ultra-fine dust. Set once; no need to animate it.
- **Tone / blend:** swap `mix-blend-mode` — `overlay` (balanced), `soft-light` (gentler, more premium), `screen` (only brightens — best on pure-black UIs), `multiply` (only darkens — best on light/paper UIs).
- **Coloured grain:** drop the `feColorMatrix` to keep the noise coloured, or tint `.grain` with a `background` + `multiply`.
- **Page vs section:** `position:fixed; inset:0` pins the grain to the viewport so it holds still while content scrolls; `position:absolute` (as in the demo) confines it to one panel.
- **Static version:** delete the `animation` line and you keep the full texture with zero motion.

## Accessibility & performance

- **Cost is tiny** — a single filtered element. Render the turbulence once and never animate `baseFrequency` (that re-runs the entire filter every frame). The flicker animates only `transform`, which the compositor handles off the main thread.
- **Honour `prefers-reduced-motion`:** disable the flicker (static grain is still fully on-brand). The demo also disables the flicker toggle in that mode.
- **Legibility:** heavy grain over small text can reduce contrast — keep `opacity` modest over body copy, or exclude text-heavy regions. The overlay is `aria-hidden` and `pointer-events:none`, so it is invisible to assistive tech and to input.
- **Retina-proof:** the grain is generated at device pixels rather than shipped as a bitmap, so it stays crisp at any DPI with no extra asset to download.

## Gotchas

- **Give the stage a stacking context** (`isolation:isolate`, or any transform/opacity that creates one) or `mix-blend-mode` will blend against the entire page behind the panel.
- **`overflow:hidden` on the stage** stops the oversized, jittering tile from spilling past the panel's edges.
- **Never omit `pointer-events:none`** — without it the overlay eats every click and hover on your UI.
- **A flat solid fill shows almost nothing** under `overlay`/`soft-light`; the effect needs gradients, photos or layered colour to bite — which is exactly why it pairs with "premium gradients / dark UI".
- **`baseFrequency` is relative to the SVG's user units.** If you scale the SVG via a `viewBox`, the grain scales with it; sizing the tile in CSS pixels (no `viewBox`, as here) keeps grain at true pixel size.
- A `background-image:url("data:image/svg+xml,…")` encoding of the same noise also works, but an inline `<svg>` element is easier to tweak live (change `baseFrequency` from JS) and to `clip-path` for a before/after split.
