25 CSS Background Animations04 / 25

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.

Published

Live Demo
Try it

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…