Pure CSSMIT licensed

Grain & Noise Texture Overlay

The texture that makes a flat gradient look printed instead of rendered. An inline SVG feTurbulence filter generates real fractal noise as a data URI — no PNG request, no 200KB tiling image — and a steps() animation jitters it eight times a second for authentic film grain rather than a frozen static layer. Three intensities and three blend modes are switchable so you can dial it against your own art direction.

Live Demo
Try it

Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS

ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link

10% off at checkout with code
Check ZTM Course

The code

<section class="bga-04" aria-label="Film grain and noise texture overlay background demo">
  <div class="bga-04__base" aria-hidden="true">
    <img class="bga-04__photo" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=1800&auto=format&fit=crop" alt="" width="1800" height="1200" loading="lazy" decoding="async">
  </div>
  <div class="bga-04__grain" aria-hidden="true"></div>
  <div class="bga-04__vig" aria-hidden="true"></div>

  <div class="bga-04__stage">
    <header class="bga-04__head">
      <p class="bga-04__eyebrow">Texture · 04</p>
      <h1 class="bga-04__title">Grain is the difference between rendered and printed.</h1>
      <p class="bga-04__sub">Fractal noise generated by an inline SVG filter, tiled seamlessly, jittered on a six-step clock. No image request, no canvas loop.</p>
    </header>

    <div class="bga-04__controls" role="group" aria-label="Grain options">
      <div class="bga-04__set">
        <span class="bga-04__lab">Intensity</span>
        <input class="bga-04__sr" type="radio" name="bga-04-i" id="bga-04-i1">
        <label class="bga-04__chip" for="bga-04-i1">Whisper</label>
        <input class="bga-04__sr" type="radio" name="bga-04-i" id="bga-04-i2" checked>
        <label class="bga-04__chip" for="bga-04-i2">35mm</label>
        <input class="bga-04__sr" type="radio" name="bga-04-i" id="bga-04-i3">
        <label class="bga-04__chip" for="bga-04-i3">Pushed</label>
      </div>
      <div class="bga-04__set">
        <span class="bga-04__lab">Blend</span>
        <input class="bga-04__sr" type="radio" name="bga-04-b" id="bga-04-b1" checked>
        <label class="bga-04__chip" for="bga-04-b1">overlay</label>
        <input class="bga-04__sr" type="radio" name="bga-04-b" id="bga-04-b2">
        <label class="bga-04__chip" for="bga-04-b2">soft-light</label>
        <input class="bga-04__sr" type="radio" name="bga-04-b" id="bga-04-b3">
        <label class="bga-04__chip" for="bga-04-b3">multiply</label>
      </div>
    </div>
    <p class="bga-04__spec">feTurbulence fractalNoise · baseFrequency .82 · numOctaves 4 · stitchTiles stitch · steps(6) jitter</p>
  </div>
</section>
.bga-04,
.bga-04 *,
.bga-04 *::before,
.bga-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bga-04 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  --bga-04-op: .42;
  --bga-04-blend: overlay;
  --bga-04-freq: .82;
  --bga-04-ink: oklch(0.99 0.005 90);
  background: oklch(0.28 0.06 32);
  color: var(--bga-04-ink);
  font-family: ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
  -webkit-font-smoothing: antialiased;
}

.bga-04__base {
  position: absolute;
  inset: 0;
  background: radial-gradient(90% 70% at 12% 8%,oklch(0.68 0.18 44/.9),transparent 62%), radial-gradient(80% 70% at 88% 22%,oklch(0.52 0.17 12/.85),transparent 64%), radial-gradient(100% 90% at 50% 108%,oklch(0.32 0.1 300/.9),transparent 70%), linear-gradient(160deg,oklch(0.34 0.08 40),oklch(0.2 0.05 300));
}

.bga-04__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: .5;
  mix-blend-mode: luminosity;
}

.bga-04__grain {
  position: absolute;
  inset: -20%;
  pointer-events: none;
  opacity: var(--bga-04-op);
  mix-blend-mode: var(--bga-04-blend);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
  animation: bga-04-jitter .72s steps(6) infinite;
}

@keyframes bga-04-jitter {
  0% {
    translate: 0 0;
  }

  16% {
    translate: -4% -6%;
  }

  32% {
    translate: 5% -3%;
  }

  48% {
    translate: -3% 5%;
  }

  64% {
    translate: 4% 4%;
  }

  80% {
    translate: -5% 2%;
  }

  100% {
    translate: 0 0;
  }
}

.bga-04__vig {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(120% 100% at 50% 45%,transparent 42%,oklch(0.1 0.03 30/.72) 100%);
}

.bga-04__stage {
  position: relative;
  z-index: 2;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: clamp(22px,3.2vw,36px);
  padding: clamp(24px,5vw,72px);
  text-align: center;
}

.bga-04__head {
  display: grid;
  gap: 14px;
  justify-items: center;
  max-width: 860px;
}

.bga-04__eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: oklch(0.9 0.08 60/.85);
}

.bga-04__title {
  font-size: clamp(30px,6vw,64px);
  font-weight: 600;
  line-height: 1.03;
  letter-spacing: -.05em;
  text-wrap: balance;
  text-shadow: 0 3px 40px oklch(0.1 0.03 30/.5);
}

.bga-04__sub {
  max-width: 54ch;
  font-size: clamp(14px,1.35vw,17px);
  line-height: 1.58;
  color: oklch(0.95 0.02 60/.78);
  text-wrap: pretty;
}

.bga-04__controls {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 26px;
  justify-content: center;
  padding: 14px 20px;
  border-radius: 18px;
  background: oklch(0.16 0.03 30/.42);
  border: 1px solid oklch(1 0 0/.14);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

.bga-04__set {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px;
}

.bga-04__lab {
  font-size: 10.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: oklch(1 0 0/.55);
  margin-inline-end: 3px;
}

.bga-04__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.bga-04__chip {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 14px;
  border-radius: 99px;
  cursor: pointer;
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12px;
  color: oklch(1 0 0/.75);
  background: oklch(1 0 0/.07);
  border: 1px solid oklch(1 0 0/.14);
  transition: background .25s,color .25s;
}

.bga-04__chip:hover {
  background: oklch(1 0 0/.15);
}

.bga-04__sr:checked + .bga-04__chip {
  background: oklch(0.99 0.005 90);
  color: oklch(0.22 0.05 30);
  border-color: transparent;
}

.bga-04__sr:focus-visible + .bga-04__chip {
  outline: 3px solid oklch(0.99 0.005 90);
  outline-offset: 3px;
}

.bga-04__spec {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: oklch(1 0 0/.45);
  text-wrap: balance;
  max-width: 70ch;
}

.bga-04:has(#bga-04-i1:checked) {
  --bga-04-op: .16;
}

.bga-04:has(#bga-04-i3:checked) {
  --bga-04-op: .75;
}

.bga-04:has(#bga-04-b2:checked) {
  --bga-04-blend: soft-light;
}

.bga-04:has(#bga-04-b3:checked) {
  --bga-04-blend: multiply;
}

@media (prefers-reduced-motion: reduce) {
  .bga-04__grain {
    animation: none;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Background Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Grain & Noise Texture Overlay
Source: https://codefronts.com/motion/css-background-animations/grain-and-noise-texture-overlay/

The texture that makes a flat gradient look printed instead of rendered. An inline SVG feTurbulence filter generates real fractal noise as a data URI — no PNG request, no 200KB tiling image — and a steps() animation jitters it eight times a second for authentic film grain rather than a frozen static layer. Three intensities and three blend modes are switchable so you can dial it against your own art direction.
## HTML
```html
<section class="bga-04" aria-label="Film grain and noise texture overlay background demo">
  <div class="bga-04__base" aria-hidden="true">
    <img class="bga-04__photo" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=1800&auto=format&fit=crop" alt="" width="1800" height="1200" loading="lazy" decoding="async">
  </div>
  <div class="bga-04__grain" aria-hidden="true"></div>
  <div class="bga-04__vig" aria-hidden="true"></div>

  <div class="bga-04__stage">
    <header class="bga-04__head">
      <p class="bga-04__eyebrow">Texture · 04</p>
      <h1 class="bga-04__title">Grain is the difference between rendered and printed.</h1>
      <p class="bga-04__sub">Fractal noise generated by an inline SVG filter, tiled seamlessly, jittered on a six-step clock. No image request, no canvas loop.</p>
    </header>

    <div class="bga-04__controls" role="group" aria-label="Grain options">
      <div class="bga-04__set">
        <span class="bga-04__lab">Intensity</span>
        <input class="bga-04__sr" type="radio" name="bga-04-i" id="bga-04-i1">
        <label class="bga-04__chip" for="bga-04-i1">Whisper</label>
        <input class="bga-04__sr" type="radio" name="bga-04-i" id="bga-04-i2" checked>
        <label class="bga-04__chip" for="bga-04-i2">35mm</label>
        <input class="bga-04__sr" type="radio" name="bga-04-i" id="bga-04-i3">
        <label class="bga-04__chip" for="bga-04-i3">Pushed</label>
      </div>
      <div class="bga-04__set">
        <span class="bga-04__lab">Blend</span>
        <input class="bga-04__sr" type="radio" name="bga-04-b" id="bga-04-b1" checked>
        <label class="bga-04__chip" for="bga-04-b1">overlay</label>
        <input class="bga-04__sr" type="radio" name="bga-04-b" id="bga-04-b2">
        <label class="bga-04__chip" for="bga-04-b2">soft-light</label>
        <input class="bga-04__sr" type="radio" name="bga-04-b" id="bga-04-b3">
        <label class="bga-04__chip" for="bga-04-b3">multiply</label>
      </div>
    </div>
    <p class="bga-04__spec">feTurbulence fractalNoise · baseFrequency .82 · numOctaves 4 · stitchTiles stitch · steps(6) jitter</p>
  </div>
</section>
```
## CSS
```css
.bga-04,
.bga-04 *,
.bga-04 *::before,
.bga-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bga-04 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  --bga-04-op: .42;
  --bga-04-blend: overlay;
  --bga-04-freq: .82;
  --bga-04-ink: oklch(0.99 0.005 90);
  background: oklch(0.28 0.06 32);
  color: var(--bga-04-ink);
  font-family: ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
  -webkit-font-smoothing: antialiased;
}

.bga-04__base {
  position: absolute;
  inset: 0;
  background: radial-gradient(90% 70% at 12% 8%,oklch(0.68 0.18 44/.9),transparent 62%), radial-gradient(80% 70% at 88% 22%,oklch(0.52 0.17 12/.85),transparent 64%), radial-gradient(100% 90% at 50% 108%,oklch(0.32 0.1 300/.9),transparent 70%), linear-gradient(160deg,oklch(0.34 0.08 40),oklch(0.2 0.05 300));
}

.bga-04__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: .5;
  mix-blend-mode: luminosity;
}

.bga-04__grain {
  position: absolute;
  inset: -20%;
  pointer-events: none;
  opacity: var(--bga-04-op);
  mix-blend-mode: var(--bga-04-blend);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
  animation: bga-04-jitter .72s steps(6) infinite;
}

@keyframes bga-04-jitter {
  0% {
    translate: 0 0;
  }

  16% {
    translate: -4% -6%;
  }

  32% {
    translate: 5% -3%;
  }

  48% {
    translate: -3% 5%;
  }

  64% {
    translate: 4% 4%;
  }

  80% {
    translate: -5% 2%;
  }

  100% {
    translate: 0 0;
  }
}

.bga-04__vig {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(120% 100% at 50% 45%,transparent 42%,oklch(0.1 0.03 30/.72) 100%);
}

.bga-04__stage {
  position: relative;
  z-index: 2;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: clamp(22px,3.2vw,36px);
  padding: clamp(24px,5vw,72px);
  text-align: center;
}

.bga-04__head {
  display: grid;
  gap: 14px;
  justify-items: center;
  max-width: 860px;
}

.bga-04__eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: oklch(0.9 0.08 60/.85);
}

.bga-04__title {
  font-size: clamp(30px,6vw,64px);
  font-weight: 600;
  line-height: 1.03;
  letter-spacing: -.05em;
  text-wrap: balance;
  text-shadow: 0 3px 40px oklch(0.1 0.03 30/.5);
}

.bga-04__sub {
  max-width: 54ch;
  font-size: clamp(14px,1.35vw,17px);
  line-height: 1.58;
  color: oklch(0.95 0.02 60/.78);
  text-wrap: pretty;
}

.bga-04__controls {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 26px;
  justify-content: center;
  padding: 14px 20px;
  border-radius: 18px;
  background: oklch(0.16 0.03 30/.42);
  border: 1px solid oklch(1 0 0/.14);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

.bga-04__set {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px;
}

.bga-04__lab {
  font-size: 10.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: oklch(1 0 0/.55);
  margin-inline-end: 3px;
}

.bga-04__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.bga-04__chip {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 14px;
  border-radius: 99px;
  cursor: pointer;
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12px;
  color: oklch(1 0 0/.75);
  background: oklch(1 0 0/.07);
  border: 1px solid oklch(1 0 0/.14);
  transition: background .25s,color .25s;
}

.bga-04__chip:hover {
  background: oklch(1 0 0/.15);
}

.bga-04__sr:checked + .bga-04__chip {
  background: oklch(0.99 0.005 90);
  color: oklch(0.22 0.05 30);
  border-color: transparent;
}

.bga-04__sr:focus-visible + .bga-04__chip {
  outline: 3px solid oklch(0.99 0.005 90);
  outline-offset: 3px;
}

.bga-04__spec {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: oklch(1 0 0/.45);
  text-wrap: balance;
  max-width: 70ch;
}

.bga-04:has(#bga-04-i1:checked) {
  --bga-04-op: .16;
}

.bga-04:has(#bga-04-i3:checked) {
  --bga-04-op: .75;
}

.bga-04:has(#bga-04-b2:checked) {
  --bga-04-blend: soft-light;
}

.bga-04:has(#bga-04-b3:checked) {
  --bga-04-blend: multiply;
}

@media (prefers-reduced-motion: reduce) {
  .bga-04__grain {
    animation: none;
  }
}
```

How this works

The noise is generated, not downloaded. An SVG filter with feTurbulence is inlined as a url("data:image/svg+xml,...") background:

<filter id='n'>
  <feTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' stitchTiles='stitch'/>
  <feColorMatrix type='saturate' values='0'/>
</filter>
<rect width='100%' height='100%' filter='url(%23n)'/>

baseFrequency is grain size — lower is coarser, like faster film stock. numOctaves adds fractal detail (2 is cheap and clean, 5 is expensive and organic). stitchTiles='stitch' is the important one: it makes the noise tile seamlessly, so the layer can repeat across any viewport without a visible seam grid.

Static grain looks like a dirty screen. Real grain moves, so the layer is animated with steps() and integer translations — never a smooth transition, which would read as a sliding texture instead of resampled film:

animation: jitter .7s steps(6) infinite;
@keyframes jitter { 10% { translate: -5% -10%; } 30% { translate: 3% -15%; } ... }

The plate underneath is a real photograph (blended at luminosity over the gradient, so the gradient still drives the colour and a 404 leaves a perfectly good background). Grain over photography is the actual use case — on a flat fill it reads as screen dirt.

Blending is what integrates it. mix-blend-mode: overlay keeps midtones and adds contrast-aware speckle; soft-light is gentler; multiply darkens like ink on uncoated stock. The layer is scaled to 140% so the translation never exposes an edge.

Because the filter output is cached as an image by the browser, this costs one texture upload and then pure compositing — dramatically cheaper than a canvas noise loop, which many tutorials still recommend.

Make it yours

  • baseFrequency between 0.6 and 0.9 reads as 35mm grain; 0.2-0.4 reads as coarse newsprint; above 1.2 it becomes fine sensor noise.
  • Add <feColorMatrix type='saturate' values='1'/> instead of 0 for chromatic colour noise — closer to a pushed high-ISO digital sensor.
  • Freeze the animation and raise the opacity for print-style paper texture; animate it and drop the opacity for cinema grain.
  • Apply the same data URI as a mask-image instead of a background to erode edges of shapes — an instant risograph / letterpress effect.

Gotchas — read before shipping

  • feTurbulence is generated on the CPU at paint time. Render it once as a fixed-size tiled layer; never put it on a filter that re-evaluates every frame, and never animate baseFrequency in CSS on a full-screen element.
  • Grain over text reduces legibility at small sizes. Keep the overlay under 20% opacity, or exclude the text container via isolation: isolate.
  • Some data-URI SVGs break if the # in an id is not escaped as %23 — this is the most common copy-paste failure of this technique.
  • mix-blend-mode forces a stacking context and can disable GPU rasterisation of what is beneath it in older Safari. Test scroll performance on iOS.
  • Heavy grain can trigger banding artefacts in video encoders if the page is being screen-recorded — reduce octaves for demo reels.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome any modern.

SVG filters as CSS background data URIs, mix-blend-mode and steps() animations are all supported in every engine since 2016. :has() (Chrome 105 / Safari 15.4 / Firefox 121) only powers the option chips; without it the default intensity renders.

Search CodeFronts

Loading…