Noise and grain effects on website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Implementing Noise/Grain Effects on a Website

Grain (film grain, noise texture) is one of the most frequently requested visual effects in modern web design. It removes screen sterility, adds texture, and makes gradients more organic. It can be implemented in several ways—from static SVG filters to animated Canvas noise.

Method 1: SVG filter + CSS (simplest)

The browser applies procedural noise through SVG feTurbulence. Almost zero CPU/GPU load.

<!-- Hidden SVG with filter -->
<svg xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0">
  <defs>
    <filter id="grain-filter" x="0%" y="0%" width="100%" height="100%"
            color-interpolation-filters="sRGB">
      <feTurbulence
        type="fractalNoise"
        baseFrequency="0.65"
        numOctaves="3"
        stitchTiles="stitch"
        result="noise"
      />
      <feColorMatrix type="saturate" values="0" in="noise" result="grayNoise"/>
      <feBlend in="SourceGraphic" in2="grayNoise" mode="overlay" result="blended"/>
      <feComponentTransfer in="blended">
        <feFuncA type="linear" slope="1"/>
      </feComponentTransfer>
    </filter>
  </defs>
</svg>
.grain-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  pointer-events: none;
  opacity: 0.15;
  filter: url(#grain-filter);
  background: transparent;
}

/* Or on a specific element */
.hero-with-grain {
  position: relative;
}

.hero-with-grain::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0.12;
  filter: url(#grain-filter);
  pointer-events: none;
}

Static—the noise doesn't move. Good for texturing gradients.

Method 2: CSS pseudo-element with base64 PNG

Pre-rendered PNG with noise, tiled via background-size. Fewer artifacts in some browsers.

.grain-texture::after {
  content: '';
  position: fixed;
  inset: -200%;  /* extend beyond for animation */
  width: 400%;
  height: 400%;
  background-image: url('/textures/grain.png');
  background-size: 200px 200px;
  opacity: 0.08;
  pointer-events: none;
  z-index: 9999;
  animation: grain-shift 0.2s steps(1) infinite;
}

@keyframes grain-shift {
  0%  { transform: translate(0, 0); }
  10% { transform: translate(-5%, -10%); }
  20% { transform: translate(-15%, 5%); }
  30% { transform: translate(7%, -25%); }
  40% { transform: translate(-5%, 25%); }
  50% { transform: translate(-15%, 10%); }
  60% { transform: translate(15%, 0%); }
  70% { transform: translate(0%, 15%); }
  80% { transform: translate(3%, 35%); }
  90% { transform: translate(-10%, 10%); }
  100%{ transform: translate(0%, 5%); }
}

Generate grain.png via Node.js:

// scripts/generate-grain.js
const { createCanvas } = require('canvas')
const fs = require('fs')

const SIZE = 256
const canvas = createCanvas(SIZE, SIZE)
const ctx = canvas.getContext('2d')

const imageData = ctx.createImageData(SIZE, SIZE)
const data = imageData.data

for (let i = 0; i < data.length; i += 4) {
  const value = Math.floor(Math.random() * 255)
  data[i] = value      // R
  data[i + 1] = value  // G
  data[i + 2] = value  // B
  data[i + 3] = 255    // A
}

ctx.putImageData(imageData, 0, 0)
fs.writeFileSync('./public/textures/grain.png', canvas.toBuffer('image/png'))

Method 3: Canvas with animated noise

Full control: update speed, grain size, opacity, color.

class GrainCanvas {
  private canvas: HTMLCanvasElement
  private ctx: CanvasRenderingContext2D
  private rafId: number | null = null
  private frameCount = 0
  private readonly FRAME_SKIP = 2  // update every N frames

  constructor(container: HTMLElement = document.body, opacity = 0.1) {
    this.canvas = document.createElement('canvas')
    this.canvas.style.cssText = `
      position: fixed;
      inset: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      z-index: 9999;
      opacity: ${opacity};
      mix-blend-mode: overlay;
    `
    container.appendChild(this.canvas)

    this.ctx = this.canvas.getContext('2d')!
    this.resize()
    window.addEventListener('resize', this.resize)
    this.start()
  }

  private resize = () => {
    // Use reduced resolution for performance
    const scale = 0.5
    this.canvas.width = window.innerWidth * scale
    this.canvas.height = window.innerHeight * scale
  }

  private generateNoise() {
    const { width, height } = this.canvas
    const imageData = this.ctx.createImageData(width, height)
    const buffer = new Uint32Array(imageData.data.buffer)

    for (let i = 0; i < buffer.length; i++) {
      const v = (Math.random() * 256) | 0
      // Packed RGBA (little-endian): AABBGGRR
      buffer[i] = (255 << 24) | (v << 16) | (v << 8) | v
    }

    this.ctx.putImageData(imageData, 0, 0)
  }

  private start() {
    const tick = () => {
      this.frameCount++
      if (this.frameCount % this.FRAME_SKIP === 0) {
        this.generateNoise()
      }
      this.rafId = requestAnimationFrame(tick)
    }
    this.rafId = requestAnimationFrame(tick)
  }

  destroy() {
    if (this.rafId) cancelAnimationFrame(this.rafId)
    window.removeEventListener('resize', this.resize)
    this.canvas.remove()
  }
}

new GrainCanvas(document.body, 0.08)

Method 4: WebGL shader noise (GLSL)

For grain over a WebGL scene or procedural noise with frequency control:

// Fragment shader — animated film grain
uniform float uTime;
uniform float uIntensity;
uniform vec2 uResolution;

varying vec2 vUv;

// Pseudo-random function
float rand(vec2 co) {
  return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
}

void main() {
  // Change seed each frame — grain "lives"
  vec2 seed = vUv + fract(uTime * 37.0);
  float grain = rand(seed * uResolution) * 2.0 - 1.0;

  // Add to existing color
  vec4 base = texture2D(uTexture, vUv);
  base.rgb += grain * uIntensity;

  gl_FragColor = base;
}

Grain over gradient: eliminating banding

Gradients on screens with limited color depth show bands. Grain effectively masks banding:

.gradient-section {
  background: linear-gradient(135deg, #1a0050 0%, #0a1628 50%, #001a2e 100%);
  position: relative;
}

.gradient-section::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: url('/textures/grain.png');
  background-size: 150px;
  opacity: 0.05;
  animation: grain-shift 0.3s steps(1) infinite;
  pointer-events: none;
}

Performance

Method CPU GPU Animation
SVG feTurbulence static ~0 low no
CSS pseudo + PNG ~0 low yes
Canvas medium ~0 yes
WebGL shader ~0 minimal yes

Full-resolution Canvas at 60fps creates overhead. Solutions:

  • Reduce canvas size (scale = 0.5) and stretch via CSS
  • Update every 2–3 frames (FRAME_SKIP)
  • OffscreenCanvas + Worker for separate thread
// OffscreenCanvas in Web Worker
// main.js
const canvas = document.getElementById('grain')
const offscreen = canvas.transferControlToOffscreen()
const worker = new Worker('/workers/grain-worker.js')
worker.postMessage({ canvas: offscreen }, [offscreen])

Timeline

SVG or PNG method with CSS animation — 2–3 hours. Canvas with intensity adjustment, prefers-reduced-motion toggle and React component — 1 day.