The short version
- A CSS noise texture is usually made with an SVG feTurbulence filter, embedded as a small data URI image and tiled over a gradient.
- The feTurbulence baseFrequency sets the grain size: higher values give finer grain, lower values give coarse, cloudy noise.
- Noise works best laid over the gradient on a pseudo-element with a blend mode such as soft-light or overlay and a low opacity.
- Grain hides gradient banding because it breaks the visible edges between neighbouring shades into fine, random variation.
- A tiled noise image is cheap to render, while applying a live SVG filter to a large element, or animating it, costs far more.
On this page
A CSS noise texture is a fine, random speckle laid over a colour or gradient to give it grain. CSS has no noise function, so the standard source is SVG: the feTurbulence filter generates noise, you wrap it in a tiny SVG, embed that as a data URI, and tile it over your gradient at low strength. The result looks like film grain or printed paper, and it quietly fixes banding, the visible stepping that appears in large, smooth gradients.
Why grain makes colour look more expensive is covered in grainy gradients. This guide is the implementation.
The SVG noise filter
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="240">
<filter id="n">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="3" stitchTiles="stitch" />
<feColorMatrix type="saturate" values="0" />
</filter>
<rect width="100%" height="100%" filter="url(#n)" />
</svg>| Attribute | Value here | What it controls |
|---|---|---|
type | fractalNoise | Soft, even grain. turbulence gives streakier, rippled noise. |
baseFrequency | 0.8 | Grain size. Around 0.6 to 0.9 is fine grain; 0.1 to 0.3 is cloudy. |
numOctaves | 3 | Layers of detail. Beyond about 4 the difference is hard to see. |
stitchTiles | stitch | Makes the tile's edges match, so repeats have no seams. |
feColorMatrix saturate 0 | 0 | Removes the random colour, leaving grey grain. |
Embedding noise as a data URI
A separate file works, but embedding saves a request and keeps the effect in one place. In a data URI, characters that mean something in URLs must be encoded: < becomes %3C, > becomes %3E, # becomes %23 and % becomes %25. Use single quotes inside the SVG so they do not clash with the double quotes around the URL.
:root {
--noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}# as %23 is the usual reason it shows nothing in Firefox.Layering noise over a gradient
Put the noise on a pseudo-element above the gradient, then control it with two dials: a blend mode, which decides how the grey grain mixes with the colour beneath, and opacity, which decides how strong it is. soft-light keeps colours clean and is the gentlest; overlay adds more contrast. pointer-events: none keeps the layer from blocking clicks.
.grain {
position: relative;
isolation: isolate;
background: linear-gradient(135deg, #1e1b4b, #7c3aed 55%, #f472b6);
}
.grain::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background-image: var(--noise);
mix-blend-mode: soft-light;
opacity: 0.45;
pointer-events: none;
}.grain stays crisp.If you prefer a single element with no pseudo-element, list the noise first in background-image and set background-blend-mode: soft-light, normal. You lose the separate opacity dial, so bake the strength into the SVG instead, for example with opacity='0.5' on the rect.
How much grain is enough
- 1
Start faint
Begin at
opacity: 0.3withsoft-light. You should barely notice it at arm's length. - 2
Check the darkest area
Banding lives in dark, slow fades. Raise opacity until the steps there dissolve, then stop.
- 3
Match the grain to the size
Full-screen heroes can take slightly coarser grain, around 0.6. Cards and buttons want fine grain, around 0.9.
- 4
Check on a phone and a large monitor
Grain that looks right on one screen can look like dust on another. Aim for texture you feel rather than see.
- 5
Check text on top
Grain must not reduce legibility. If small text sits on the texture, lower the strength or keep text on a calmer area.
Too much
- Grain visible from across the room
- Colours turn grey and dirty
- Coarse, cloudy noise on small components
- Text edges look fuzzy against the speckle
About right
- Grain noticed only up close
- Colours stay clean with soft-light
- Fine grain sized to the element
- Banding gone, text crisp
Performance of CSS noise
A tiled data URI is cheap: the browser renders the small tile once and repeats it, and a static layer is painted once. The expensive version is applying the filter live to a large element with filter: url(#noise), which runs the filter over every pixel whenever the area repaints. If you animate grain, for a flickering film look, shift the tiled layer with transform in a few jumpy steps rather than regenerating noise, and switch it off for reduced motion.
.grain {
overflow: hidden;
}
.grain::after {
inset: -50%;
animation: flicker 0.6s steps(3) infinite;
}
@keyframes flicker {
0% { transform: translate(0, 0); }
33% { transform: translate(-3%, 2%); }
66% { transform: translate(2%, -3%); }
}
@media (prefers-reduced-motion: reduce) {
.grain::after { animation: none; }
}A pre-rendered PNG or WebP noise tile of around 128 to 256 pixels is an equally good source if you already have one. Grain is also one ingredient of a wider family of surfaces, from paper to glass, covered in background texture. It is the finishing layer on a CSS mesh gradient too.
Grain is built into every Gradiently Mark as a material setting, alongside light and motion, so designs made with one carry texture without any code.

GR·W4AX·4M
Muslin Anvil
See this MarkQuestions people ask
How do I add a noise texture in CSS?
Embed an SVG with an feTurbulence filter as a data URI, tile it on a pseudo-element over your gradient, and set a blend mode such as soft-light with low opacity.
Why does my SVG noise data URI not show?
Usually an unencoded character. In the data URI, encode # as %23, % as %25 and the angle brackets as %3C and %3E, and use single quotes inside the SVG.
Does grain fix gradient banding?
Yes, in most cases. Random fine variation breaks up the hard edges between neighbouring shades, so the eye no longer sees steps.
What baseFrequency should I use for grain?
Around 0.6 to 0.9 gives fine film-like grain. Lower values such as 0.2 give large, cloudy noise.
Is CSS noise bad for performance?
A small tiled noise image is cheap. Live SVG filters on large elements and constantly regenerated noise are what cost the most.
Written by Gradiently
The team behind Gradiently, a design tool built around Marks: living gradients that make everything you design look like yours.
See our profile


