Technique
Grainy gradients
Add film grain to a CSS gradient with a stitched feTurbulence tile at the right opacity: 1–4% removes banding on wide surfaces, 8–15% is the stylistic grain. Live generator, the technique explained, copyable CSS and SVG.
Export
CSS with the interpolation space, Tailwind v4, SVG with correct geometry, a DTCG token, and the PNG.
Apply this to a brand → sign in
.gradient {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.802' numOctaves='2' seed='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='3' intercept='-1'/%3E%3CfeFuncG type='linear' slope='3' intercept='-1'/%3E%3CfeFuncB type='linear' slope='3' intercept='-1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix type='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -12'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.016'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E"), linear-gradient(135deg, #ff512f 0%, #fe572e 7.14%, #fd5e2d 14.29%, #fd632c 21.43%, #fc692b 28.57%, #fb6e2a 35.71%, #fa7328 42.86%, #f97827 50%, #f87d26 57.14%, #f78224 64.29%, #f58622 71.43%, #f48b20 78.57%, #f38f1e 85.71%, #f1941c 92.86%, #f09819 100%);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.802' numOctaves='2' seed='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='3' intercept='-1'/%3E%3CfeFuncG type='linear' slope='3' intercept='-1'/%3E%3CfeFuncB type='linear' slope='3' intercept='-1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix type='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -12'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.016'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E"), linear-gradient(135deg in oklab, #ff512f 0%, #f09819 100%);
background-size: 160px 160px, auto;
background-blend-mode: soft-light, normal;
}
/* or keep the gradient untouched and overlay the grain: */
.gradient {
position: relative;
isolation: isolate;
}
.gradient::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.802' numOctaves='2' seed='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='3' intercept='-1'/%3E%3CfeFuncG type='linear' slope='3' intercept='-1'/%3E%3CfeFuncB type='linear' slope='3' intercept='-1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix type='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -12'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
background-size: 160px 160px;
image-rendering: pixelated;
mix-blend-mode: soft-light;
opacity: 0.0156;
}How it works
Eight bits per channel is 256 levels. A gradient that moves lightness by 20% across a 1600px hero has about 51 steps to spend over 1600px — a visible band every ~31px. Wide, low-contrast gradients band worst, which is exactly the "subtle brand background" case. More stops do nothing; the renderer still quantises to 8 bits. The actual fix is noise.
Grain versus dither
Dither adds the noise before quantisation (trunc(f + rnd)) so the rounding error averages out — it removes banding, and it needs a shader, a canvas or a pre-baked lossless image. Grain adds noise after (trunc(f) + rnd) — every CSS technique is this — so it masks the step rather than removing it, which is why the anti-banding default is so low: about 1% (≈2.5 LSB peak to peak) hides the band, 2–5% reads as texture, 8–15% is the film-grain look everyone searches for. Blend it with soft-light and the noise fades out automatically at black and white.
The recipe
The canonical source is Jimmy Chion's "Grainy Gradients" on CSS-Tricks: an SVG feTurbulence filter (type fractalNoise, stitchTiles="stitch" — mandatory, or the tile seams) pushed to black-and-white speckle with contrast() / brightness(). Two refinements here: baseFrequency and opacity move together on one slider (at 14: frequency 0.802, opacity 0.0156), and the noise is rasterised once into a 160px data-URI tile instead of running a live SVG filter on every paint. The overlay form keeps the gradient untouched — a ::after at the slider opacity, mix-blend-mode: soft-light.
.gradient {
position: relative;
isolation: isolate;
}
.gradient::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image: url("data:image/svg+xml,…");
background-size: 160px 160px;
image-rendering: pixelated;
mix-blend-mode: soft-light;
opacity: 0.0156;
}The report on every gradient measures the widest band in pixels at 1440px and, when it is over 4px on a subtle ramp, suggests the grain amount that hides it. That is the number the "apply grain" button in the maker uses.
Starting point, as code
.gradient {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.802' numOctaves='2' seed='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='3' intercept='-1'/%3E%3CfeFuncG type='linear' slope='3' intercept='-1'/%3E%3CfeFuncB type='linear' slope='3' intercept='-1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix type='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -12'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.016'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E"), linear-gradient(135deg, #ff512f 0%, #fe572e 7.14%, #fd5e2d 14.29%, #fd632c 21.43%, #fc692b 28.57%, #fb6e2a 35.71%, #fa7328 42.86%, #f97827 50%, #f87d26 57.14%, #f78224 64.29%, #f58622 71.43%, #f48b20 78.57%, #f38f1e 85.71%, #f1941c 92.86%, #f09819 100%);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.802' numOctaves='2' seed='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='3' intercept='-1'/%3E%3CfeFuncG type='linear' slope='3' intercept='-1'/%3E%3CfeFuncB type='linear' slope='3' intercept='-1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix type='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -12'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.016'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E"), linear-gradient(135deg in oklab, #ff512f 0%, #f09819 100%);
background-size: 160px 160px, auto;
background-blend-mode: soft-light, normal;
}bg-linear-to-br/oklab from-[#ff512f] to-[#f09819]
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="600" viewBox="0 0 1440 600"><defs><linearGradient id="bs-gradient" gradientUnits="userSpaceOnUse" x1="210" y1="-210" x2="1230" y2="810"><stop offset="0%" stop-color="#ff512f"/><stop offset="7.14%" stop-color="#fe572e"/><stop offset="14.29%" stop-color="#fd5e2d"/><stop offset="21.43%" stop-color="#fd632c"/><stop offset="28.57%" stop-color="#fc692b"/><stop offset="35.71%" stop-color="#fb6e2a"/><stop offset="42.86%" stop-color="#fa7328"/><stop offset="50%" stop-color="#f97827"/><stop offset="57.14%" stop-color="#f87d26"/><stop offset="64.29%" stop-color="#f78224"/><stop offset="71.43%" stop-color="#f58622"/><stop offset="78.57%" stop-color="#f48b20"/><stop offset="85.71%" stop-color="#f38f1e"/><stop offset="92.86%" stop-color="#f1941c"/><stop offset="100%" stop-color="#f09819"/></linearGradient></defs><rect width="1440" height="600" fill="url(#bs-gradient)"/></svg>
Questions
- How much grain removes banding?
- 1–4% overlay opacity is enough to hide 8-bit banding on wide, subtle gradients; 8–15% is a visible, stylistic film grain.
- Grain or dither?
- Dither adds noise before quantisation and removes banding, but needs a shader, canvas or pre-baked lossless image. Every CSS grain technique adds noise after quantisation, so it masks the band — which is why ~1% opacity is enough for anti-banding and 8–15% is the stylistic look.
- Is feTurbulence slow?
- A live SVG filter is expensive per paint. Rasterise the noise once into a small stitched tile (a data-URI) and tile it — that is what the export does.
More
Point any MCP client at the shared gradients endpoint and your agent gets the same engine as this page — make_gradient returns CSS with the interpolation space, Tailwind v4, SVG, a PNG URL, a DTCG token and the contrast report in one call; brand_gradients derives the whole contact sheet from a brand or a palette. Read-only, no key. Every gradient is a /gradients/new/… link: open it, refine, paste it back.
https://mcp.brandsweets.com/gradients/mcpclaude mcp add --transport http brandsweets-gradients https://mcp.brandsweets.com/gradients/mcp
›make a sunset hero gradient with enough grain to hide banding at 1440px and give me the CSS
Want the agent to remember your picks and write them into a living brand? Create a free BrandSweets workspace.