Canvas Noise Texture Background — Free Animated Film Grain Overlay
Canvas Noise Texture Background · Animations · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<section class="cnt-wrap">
<canvas class="cnt-canvas" id="cntCanvas"></canvas>
<div class="cnt-content">
<span class="cnt-tag">canvas 2d · imagedata grain</span>
<h1>Studio Nocturne</h1>
<p>A subtle film-grain layer generated by writing raw pixel data every few frames.</p>
<button class="cnt-btn">Book a session</button>
</div>
</section>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#fff;min-height:100vh}
.cnt-wrap{position:relative;min-height:100vh;overflow:hidden;display:flex;align-items:center;justify-content:center;background:radial-gradient(120% 90% at 50% 15%,#1c1a1a,#0a0a0a 65%)}
.cnt-canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;mix-blend-mode:overlay;opacity:.55}
.cnt-content{position:relative;z-index:1;text-align:center;max-width:460px;padding:26px}
.cnt-tag{display:inline-block;font-size:10.5px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#d6d3d1;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);padding:5px 12px;border-radius:99px;margin-bottom:16px}
.cnt-content h1{font-size:clamp(34px,7vw,58px);font-weight:800;letter-spacing:-.03em;font-family:Georgia,'Times New Roman',serif}
.cnt-content p{font-size:14.5px;color:#a8a29e;margin-top:12px;line-height:1.7}
.cnt-btn{margin-top:28px;padding:14px 30px;border-radius:2px;border:1px solid rgba(255,255,255,.3);background:transparent;color:#fff;font:600 13px system-ui;letter-spacing:.05em;cursor:pointer;transition:background .18s}
.cnt-btn:hover{background:rgba(255,255,255,.08)}var canvas = document.getElementById('cntCanvas');
var ctx = canvas.getContext('2d');
var wrap = document.querySelector('.cnt-wrap');
var W, H;
function resize() {
// Grain doesn't benefit from high resolution — it reads as noise at any
// density. Drawing at a fraction of the viewport size keeps the per-frame
// ImageData allocation and putImageData call cheap, then CSS scales it up.
W = canvas.width = Math.round(wrap.clientWidth * 0.35);
H = canvas.height = Math.round(wrap.clientHeight * 0.35);
}
resize();
window.addEventListener('resize', resize);
var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Writes one frame of grayscale static directly into an ImageData buffer.
// Working on the raw Uint8ClampedArray is far cheaper per-pixel than issuing
// thousands of individual fillRect calls would be.
function paintNoise() {
var imageData = ctx.createImageData(W, H);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
var shade = (Math.random() * 255) | 0;
data[i] = shade;
data[i + 1] = shade;
data[i + 2] = shade;
data[i + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
}
paintNoise();
if (!reduceMotion) {
// Regenerating every single requestAnimationFrame tick (60/sec) reads as
// flicker rather than texture, and burns CPU for no visual benefit — this
// grain only needs to refresh a handful of times per second to feel alive.
var FPS = 12;
setInterval(paintNoise, 1000 / FPS);
}Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Canvas Noise Texture Background — Raw Pixel Grain Over Dark Sections

Film grain is the texture that makes a flat dark section feel like it has depth and material to it — the difference between a plain black background and one that looks like it was shot on film. This snippet generates that grain by writing random grayscale values directly into a canvas ImageData buffer, rather than drawing thousands of individual shapes, and throttles the refresh rate so it reads as texture instead of strobing static.
Writing pixels directly instead of drawing shapes
paintNoise() creates an ImageData object sized to the canvas and walks its underlying Uint8ClampedArray four bytes at a time — red, green, blue, alpha — setting R, G, and B to the same random 0-255 value (grayscale) and alpha to fully opaque. ctx.putImageData() then blits that entire buffer to the canvas in one call. This is dramatically cheaper than the equivalent ctx.fillRect() per pixel would be, because it bypasses the canvas's normal shape-drawing pipeline entirely and writes memory directly.
Small canvas, upscaled by CSS
Grain doesn't need to be high-resolution to read as grain — random noise looks like random noise at almost any pixel density. resize() deliberately sizes the canvas backing store to 35% of the wrapper's dimensions, and CSS stretches it to fill the section. That keeps the ImageData allocation and putImageData write small and fast, since both scale with total pixel count.
Throttled refresh, not 60fps
Regenerating fresh noise on every requestAnimationFrame tick would refresh 60 times a second — fast enough to read as flickering static rather than texture, and wasteful for a background element. Instead, setInterval(paintNoise, 1000 / 12) refreshes at a deliberately low 12fps, which is fast enough to feel alive and un-static but slow enough to stay a background texture rather than a distraction.
Compositing, not just opacity
The CSS sets mix-blend-mode: overlay alongside a moderate opacity, so the grayscale noise blends with whatever color is beneath it — lightening dark areas and darkening light ones — rather than simply sitting on top as a flat gray haze. That combination is what makes the effect read as texture *in* the section rather than a layer *over* it. It also respects prefers-reduced-motion, painting a single static frame and skipping the interval entirely when that preference is set. Pair it with a noise background variant or a canvas mesh gradient for a more colorful companion texture.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA subtle grain layer overlays the dark hero section immediately.
- 2Look closely at the textureGrain refreshes about 12 times per second, not every frame.
- 3Check the blend modemix-blend-mode: overlay lets grain interact with colors beneath it.
- 4Resize the windowThe small backing-store canvas rescales and keeps refreshing.
- 5Enable reduced motionThe interval is skipped; one static grain frame renders instead.
- 6Tune the densityChange the 0.35 resolution factor or the FPS constant.
Real-world uses
Common Use Cases
ImageData buffer.Got questions?
Frequently Asked Questions
ctx.createImageData gives direct access to the canvas's underlying Uint8ClampedArray, so setting a pixel is just writing four numbers (R, G, B, A) to an array index, and ctx.putImageData blits the entire buffer to the canvas in a single call. Issuing a fillRect (or arc) per pixel instead would go through the canvas's full shape-drawing and compositing pipeline for every single pixel, which is dramatically slower at any meaningful resolution.
Regenerating fresh random noise 60 times a second (once per requestAnimationFrame tick) reads as flickering static rather than a settled texture, and does so while burning CPU for no additional visual benefit as a background element. Throttling the regeneration to roughly 12fps via setInterval keeps the grain feeling alive without the strobing quality of a full-rate refresh, and uses a fraction of the CPU.
Random noise looks like random noise at essentially any pixel density, so there is no visual benefit to generating it at full viewport resolution. Sizing the canvas backing store to about 35% of the wrapper and letting CSS stretch the element to fill it keeps both the ImageData allocation and the putImageData write small, since their cost scales directly with total pixel count.
Opacity alone would just lay a flat translucent gray haze on top of the section, which reads as a layer sitting over the content rather than texture within it. overlay blend mode instead combines the grayscale noise with whatever color is directly beneath each pixel — lightening darker areas and darkening lighter ones — so the grain appears to be part of the surface itself rather than an added film on top of it.
Move the resize() call and the paintNoise() interval setup into a mount effect, storing the interval id so it can be cleared in the cleanup function on unmount. Since the effect draws to a canvas ref rather than relying on requestAnimationFrame, there's no animation frame to cancel — just make sure the interval is torn down so it doesn't keep running against a canvas element that no longer exists in the DOM.