---
name: grainy-gradient
description: Use when you want "Premium, artistic, modern" - Combines gradients with noise texture for a rich editorial look.
---

# Grainy Gradient

> **Category:** Background  -  **Personality:** Premium, artistic, modern
>
> **Best use:** Hero backgrounds, editorial layouts
>
> **Ratings:** Professional 5/5 - Casual 4/5 - Exotic 4/5 - Visual impact 4/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

A grainy gradient **fuses a bold, colourful gradient with a fine film-grain layer composited on top**. The gradient supplies the colour and depth; the grain — an inline SVG `feTurbulence` noise tiled over it and blended in with `mix-blend-mode` — breaks up the smooth banding a large gradient otherwise shows and adds a tactile, almost-printed/cinematic finish. That combination is the "expensive" editorial look you see on Spotify, Stripe and magazine heroes. Reach for it on hero backgrounds and editorial layouts where a flat gradient feels cheap. It is deliberately *distinct from a plain noise overlay*: here the grain is one aesthetic with a vivid gradient, not a faint texture on an otherwise neutral page.

## Dependencies / CDN

**None for the effect — pure CSS** (one inline SVG data-URI; no image files, no library). The demo loads two display/body fonts (optional) and adds ~10 lines of vanilla JS purely for the grain on/off toggle.

```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=Instrument+Serif:ital@0;1&family=Schibsted+Grotesk:wght@400;500;700&display=swap" rel="stylesheet">
```

## HTML

The stage stacks three absolutely-positioned fills (gradient field → legibility veil → grain) beneath the content:

```html
<div class="stage" id="ggStage">
  <div class="field" aria-hidden="true">      <!-- bold gradient + blurred colour blobs -->
    <span class="blob b1"></span><span class="blob b2"></span>
    <span class="blob b3"></span><span class="blob b4"></span>
  </div>
  <div class="veil"  aria-hidden="true"></div> <!-- subtle scrim so text stays legible -->
  <div class="grain" aria-hidden="true"></div> <!-- THE grain, blended over the gradient -->
  <div class="content"> … editorial hero copy … </div>
</div>
```

## CSS

```css
.stage{position:relative;border-radius:26px;overflow:hidden;min-height:620px;
  display:flex;background:#0b0610;isolation:isolate}      /* isolate = contain the blend */

/* 1) the bold gradient field: a base ramp + a few big blurred colour blobs (cheap mesh) */
.field{position:absolute;inset:0;z-index:0;
  background:linear-gradient(135deg,#1f0f3f,#4a1559 30%,#9b2257 58%,#e85d2f)}
.blob{position:absolute;border-radius:50%;filter:blur(72px)}
.b1{width:62%;height:72%;background:#6d28d9;top:-18%;left:-12%;opacity:.85}
.b2{width:58%;height:66%;background:#e11d6b;top:14%;left:33%;opacity:.78}
.b3{width:60%;height:72%;background:#fb7a3c;bottom:-24%;right:-12%;opacity:.82}
.b4{width:40%;height:48%;background:#0fb9a6;top:-10%;right:6%;opacity:.5}  /* off-key accent */

/* 2) THE GRAIN — inline feTurbulence tile, desaturated, blended over the gradient.
      Keep the data-URI on ONE line; encode  # → %23  and  % → %25  (e.g. 100% → 100%25). */
.grain{position:absolute;inset:0;z-index:2;pointer-events:none;opacity:.55;
  mix-blend-mode:overlay;background-size:180px 180px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
.stage.is-flat .grain{opacity:0;transition:opacity .5s ease}  /* toggle off → smooth gradient */

/* 3) text rides ABOVE the grain so it stays crisp */
.content{position:relative;z-index:3;color:#fff}

@media (prefers-reduced-motion:reduce){ .blob{animation:none} }  /* show a static gradient */
```

## JavaScript

Not required for the effect. The demo adds an optional toggle so you can *see* the grain's contribution (smooth/banding-prone vs. textured):

```js
var stage = document.getElementById('ggStage');
var btn   = document.getElementById('ggToggle');
btn.addEventListener('click', function () {
  var flat = stage.classList.toggle('is-flat');     // flat === grain hidden
  btn.setAttribute('aria-pressed', String(!flat));
  btn.querySelector('b').textContent = flat ? 'off' : 'on';
});
```

## How it works

- **`feTurbulence type="fractalNoise"`** generates a procedural noise bitmap in the browser — no downloaded image. `baseFrequency` controls grain fineness (higher = finer), `numOctaves` adds detail, and `stitchTiles="stitch"` makes the tile seamless so one tiny 180×180 SVG can blanket any area via `background-repeat`.
- **`feColorMatrix type="saturate" values="0"`** strips the colour out of the noise so it reads as neutral film grain instead of rainbow speckle; intensity is then set purely by the layer's `opacity`.
- **`mix-blend-mode: overlay`** composites the grain *against* the gradient — lightening the bright bands and darkening the dark ones around the mid-tone. That is what dissolves gradient banding and produces the tactile "expensive" surface. Lay the same noise on top with no blend mode and you just get grey dots.
- **Layer order + `isolation:isolate`** — grain sits above the gradient but below the content, so the colour is textured while text stays razor-sharp; `isolation` keeps the blend contained to the stage rather than bleeding onto the page.
- The colour is a base `linear-gradient` plus a few large `blur()`-ed blobs (a cheap mesh-gradient); a slow `translate` float on the blobs keeps it alive.

## Customization

- **Grain amount:** the grain layer `opacity` (.35 subtle → .7 heavy). The on/off toggle is simply this going to `0`.
- **Grain size:** `baseFrequency` (0.5 coarse/chunky → 0.9 fine/photographic) and `background-size` (smaller tile = denser grain).
- **Blend character:** `overlay` (punchy — the demo) vs `soft-light` (gentler, more filmic) vs `multiply` (darkens; good over light gradients).
- **Palette:** swap the `linear-gradient` stops and blob colours. Keep it bold but limited (3–4 hues) and add one off-key accent (the teal blob here) for richness.
- **Coloured grain:** delete the `feColorMatrix` line to keep the noise's RGB speckle for a chromatic / risograph vibe.

## Accessibility & performance

- Cheap by design: one tiny SVG tile, a GPU-composited blend, no animation loop and no image requests — hence the low performance rating. Keep `pointer-events:none` on the grain/veil so they never intercept clicks.
- Keep body copy **above** the grain (higher `z-index`) and add a subtle dark `veil`/scrim so text contrast holds over the bright parts of the gradient.
- Respect motion preferences: the only motion (blob drift, the demo's equaliser) is disabled under `@media (prefers-reduced-motion: reduce)`, leaving a static gradient. The grain itself is static — like print.

## Gotchas

- **Encode the data-URI.** Inside `url("…")` you must percent-encode `#` → `%23` and `%` → `%25` (so `100%` becomes `100%25` and `url(#n)` becomes `url(%23n)`), and use single quotes for the SVG attributes so they don't clash with the outer double quotes. One stray character and the whole background silently fails to paint.
- **It needs a vivid backdrop.** Grain over a flat colour just looks like dirt — the magic is grain *fused with* a bold gradient. This is exactly what separates it from a plain noise-texture overlay.
- **Don't animate `feTurbulence`.** Re-rasterising the filter every frame is janky; for "moving" grain animate `background-position` of the static tile in `steps()`, or just leave it static.
- **Contain the blend.** A parent `filter`/`transform` can create an unexpected containing block and break the blend or the `overflow:hidden` clipping — keep the grain and gradient as siblings inside the `isolate`d stage.
