How it works
Noise from a single filter primitive
No image files, no canvas, no JavaScript: the grain is computed by the browser from an SVG filter, at whatever resolution the screen needs.
1. Generate: feTurbulence
<feTurbulence> is a built-in SVG filter primitive that implements Ken Perlin's
noise function. For every pixel in the filter region it computes a pseudo-random but smoothly
varying value per color channel (red, green, blue, alpha). There is no input image: the noise
is generated from scratch, so it stays crisp at any size and zoom level.
- type
fractalNoisesums the noise layers directly and looks soft and cloudy.turbulencesums their absolute values, which creates sharp creases, like veins in marble or licks of flame.- baseFrequency
- How many noise cycles fit into one pixel. Around
0.6–1you get pixel-sized grain; around0.01you get large, slow clouds. The attribute accepts two numbers (x and y), and the Stretch slider sets y as a multiple of x. A small ratio such as0.05smears the noise horizontally into a brushed-metal look. - numOctaves
- How many layers are added together. Each extra octave doubles the frequency and halves the amplitude, adding finer detail on top. Beyond about 5 the difference is invisible, but every octave costs another full pass of the noise function.
- seed
- Picks a different, but repeatable, random pattern.
- stitchTiles
- Adjusts the frequency slightly so the noise wraps seamlessly at the edges of the filter region. That only matters if you tile the result, e.g. as a repeating background image.
2. Shape: color matrix and transfer functions
Raw turbulence is a pastel, semi-transparent mess. Two more primitives turn it into grain:
<feColorMatrix type="saturate" values="0">removes the color, so only the brightness noise remains (Monochrome).<feComponentTransfer>maps every channel through a linear function,out = slope × in + intercept. Setting the slope to the contrast and the intercept to0.5 − 0.5 × contraststretches the values around mid-grey, so the grain gets punchier without getting brighter or darker overall; Brightness shifts the intercept. A last<feFuncA slope="0" intercept="1">discards the noisy alpha channel and makes the layer opaque (unless you enable Noisy alpha).
color-interpolation-filters="sRGB" matters. SVG filters compute in linear RGB by
default, which makes the noise look washed out and lighter than the same numbers in sRGB.
3. Composite: blend it onto the page
The filter is applied to a full-size <rect> inside an absolutely
positioned <svg> that covers the stage. On its own that would be a grey static
screen. The grain becomes texture through compositing: opacity sets how strong
it is, and mix-blend-mode decides how it mixes with the colors below. overlay and soft-light lighten the light parts and darken the dark parts, so the gradient
keeps its colors and just gains texture. isolation: isolate on the container keeps
the blend from reaching anything outside it.
This is the markup and CSS for your current settings:
<svg aria-hidden="true">
<filter id="noise" x="0" y="0" width="100%" height="100%"
color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.8 0.8"
numOctaves="4" seed="0"
stitchTiles="stitch" />
<feColorMatrix type="saturate" values="0" />
<feComponentTransfer>
<feFuncR type="linear" slope="1.5" intercept="-0.25" />
<feFuncG type="linear" slope="1.5" intercept="-0.25" />
<feFuncB type="linear" slope="1.5" intercept="-0.25" />
<feFuncA type="linear" slope="0" intercept="1" />
</feComponentTransfer>
</filter>
<rect width="100%" height="100%" filter="url(#noise)" />
</svg> .stage { position: relative; isolation: isolate; }
.stage > svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0.35;
mix-blend-mode: overlay;
} 4. Animate: film grain
Real film grain changes from frame to frame. The component imitates that by changing the seed on a requestAnimationFrame loop, throttled to the chosen frames
per second. It is skipped entirely when the user has asked for reduced motion.
Two caveats. Every new seed makes the browser recompute the filter for the whole area, so keep the frame rate low (8–12 fps already reads as film). And coarse noise is a photosensitivity hazard: with a low base frequency, whole regions jump between light and dark on every frame. WCAG 2.3.1 allows at most three flashes per second, where a flash is a change of 10% or more in the average luminance of an area about 341×256 px. Measured at the most extreme settings, a base frequency of 0.02 already reaches that change in a single frame, while 0.08 stays around 0.013. So the component only animates at full speed when the coarser axis is at 0.1 or above, and caps everything else at 3 fps.
Performance notes
- A static noise layer is rendered once and then cached by the browser, so it is cheap. It is recomputed when the element is resized or the filter attributes change.
- For large, static surfaces, the same filter can be put into a data-URI SVG and used as a
tiled
background-image. WithstitchTiles="stitch"the seams disappear. - Noise breaks up color banding in smooth gradients and blurs, which is why the frosted glass effect reuses this component as its grain layer.