{"id":6066,"date":"2025-06-13T09:37:36","date_gmt":"2025-06-13T14:37:36","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=6066"},"modified":"2025-06-23T10:50:20","modified_gmt":"2025-06-23T15:50:20","slug":"grainy-gradients","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/grainy-gradients\/","title":{"rendered":"Grainy Gradients"},"content":{"rendered":"\n<p>You know when you set a <code>background<\/code> gradient or a gradient <code>mask<\/code> and you get an ugly&nbsp;<a href=\"https:\/\/en.wikipedia.org\/wiki\/Colour_banding\">banding<\/a> effect? If you can&#8217;t picture what I mean, here&#8217;s an example:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"450\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451853285-2c07e7e7-53b6-4cc7-b7fe-2b251e7c8fd0.png?resize=900%2C450&#038;ssl=1\" alt=\"A left to right pinkinsh orange to dark grey gradient exhibiting banding.\" class=\"wp-image-6069\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451853285-2c07e7e7-53b6-4cc7-b7fe-2b251e7c8fd0.png?w=900&amp;ssl=1 900w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451853285-2c07e7e7-53b6-4cc7-b7fe-2b251e7c8fd0.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451853285-2c07e7e7-53b6-4cc7-b7fe-2b251e7c8fd0.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><figcaption class=\"wp-element-caption\">&nbsp;example gradient with banding<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Previous Solutions<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#previous-solutions\"><\/a><\/h2>\n\n\n\n<p>Over time, I&#8217;ve seen a couple of approaches commonly recommended for solving this. The first is to&nbsp;<a href=\"https:\/\/css-tricks.com\/easing-linear-gradients\/\">simply introduce more stops<\/a>&nbsp;(gradient &#8220;easing&#8221;), which I&#8217;m not really keen on doing, even if I can just generate them in a Sass loop and never need to know about them. The second one is to&nbsp;<a href=\"https:\/\/graphicdesign.stackexchange.com\/a\/36045\">make the gradient noisy<\/a>. Let&#8217;s do that.<\/p>\n\n\n\n<p>The way I first went about making gradients grainy was to have a gradient layer and a noise layer (using pseudo-elements for the layers) and then blend them together. I first did this&nbsp;<a href=\"https:\/\/youtu.be\/xmrfj5qrXAw\">in response to a question<\/a>&nbsp;asked on X. That video became one of my most watched ones ever, which isn&#8217;t something I&#8217;m happy about anymore because I&#8217;ve come to find that technique to be overly complicated, like scratching behind the right ear with the left foot.<\/p>\n\n\n\n<p>A few months later, I saw&nbsp;<a href=\"https:\/\/css-tricks.com\/grainy-gradients\/\">an article<\/a>&nbsp;that was doing something similar: placing a gradient layer and a noise layer one on top of the other. Unlike my approach, it wasn&#8217;t blending the two layers and instead was relying on one end of the gradient being transparent to allow the noise to show through. For the other end to be something other than transparent, it would layer an overlay and blend it. Just like my layered pseudos approach&#8230; too complicated! Not to mention that the <code>contrast()<\/code> and <code>brightness()<\/code> tampering (meant to highlight the grain) make this only work for certain gradient inputs and they greatly alter the saturation and luminosity of the original gradient palette.<\/p>\n\n\n\n<p>In time, I would improve upon my initial idea and, almost half a decade later, I would make&nbsp;<a href=\"https:\/\/www.youtube.com\/watch?v=cA39j6p3Yho\">a second video<\/a>&nbsp;on the topic, presenting a much simplified technique. Basically, the gradient would get fed into an SVG&nbsp;<code>filter<\/code>, which would generate a noise layer, desaturate it and then place it on top of the input gradient. No external files, no base64-ing anything, no separate (pseudo)element layers for the noise and the gradient.<\/p>\n\n\n\n<p>Still, it didn&#8217;t take long before I wasn&#8217;t happy with this solution anymore, either.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The big problem with layering the noise and the gradient<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#the-big-problem-with-layering-the-noise-and-the-gradient\"><\/a><\/h2>\n\n\n\n<p>The problem with all of these solutions so far is that they&#8217;re changing the gradient. Depending on the particular technique we use, we always end up with a gradient that&#8217;s either darker, brighter, or more saturated than our original one.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_qEdbEQZ\" src=\"\/\/codepen.io\/anon\/embed\/qEdbEQZ?height=570&amp;theme-id=47434&amp;slug-hash=qEdbEQZ&amp;default-tab=result\" height=\"570\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed qEdbEQZ\" title=\"CodePen Embed qEdbEQZ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>We can reduce the noise opacity, but in doing so, our gradient becomes less grainy and the efficiency of fixing banding this way decreases.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A better solution<\/h2>\n\n\n\n<p><a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#a-better-solution\"><\/a>How about not layering the the noise layer and instead using it as a&nbsp;<a href=\"https:\/\/www.smashingmagazine.com\/2021\/09\/deep-dive-wonderful-world-svg-displacement-filtering\/\">displacement map<\/a>?<\/p>\n\n\n\n<p>What this does is use two of the four RGBA channels of the noise layer to determine how the individual pixels of the input gradient are shifted along the&nbsp;<em>x<\/em>&nbsp;and&nbsp;<em>y<\/em>&nbsp;axes.<\/p>\n\n\n\n<p>Both the&nbsp;<code>filter<\/code>&nbsp;input (our gradient) and the noise layer can be taken to be 2D grids of pixels. Each pixel of our input gradient gets displaced based on the two selected channel values of its corresponding pixel in the noise layer (used as a displacement map).<\/p>\n\n\n\n<p>A channel value below&nbsp;<code>50%<\/code>&nbsp;means moving in the positive direction of the axis, a channel value above&nbsp;<code>50%<\/code>&nbsp;means moving in the negative direction of the axis and a channel value of exactly&nbsp;<code>50%<\/code>&nbsp;means not moving at all.<\/p>\n\n\n\n<p>The displacement formula for a generic channel value of <code>C<\/code> and a displacement <code>scale<\/code> of <code>S<\/code> is the following:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">(.5 - C)*S<\/pre>\n\n\n\n<p>If we use the red channel <code>R<\/code> for displacement along the\u00a0<em>x<\/em> axis and the alpha channel <code>A<\/code> for displacement along the\u00a0<em>y<\/em>\u00a0axis, then we have:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">dx = (.5 - R)*S<br>dy = (.5 - A)*S<\/pre>\n\n\n\n<p>Note that the values for both&nbsp;<code>R<\/code>&nbsp;and&nbsp;<code>A<\/code>&nbsp;are in the&nbsp;<code>[0, 1]<\/code>&nbsp;interval (meaning channel values are zeroed at <code>0<\/code> and maxed out at&nbsp;<code>1<\/code>), so the difference between the parenthesis is in the&nbsp;<code>[-.5, .5]<\/code>&nbsp;interval.<\/p>\n\n\n\n<p>The bigger the\u00a0<code>scale<\/code>\u00a0value\u00a0<code>S<\/code>\u00a0is, the more the gradient pixels mix along the gradient axis depending on the red\u00a0<code>R<\/code>\u00a0and alpha <code>A<\/code> channel values of the displacement map generated by <code>feTurbulence<\/code>.<\/p>\n\n\n\n<p>Let&#8217;s see our code!<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'grain'<\/span> <span class=\"hljs-attr\">color-interpolation-filters<\/span>=<span class=\"hljs-string\">'sRGB'<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feTurbulence<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">'fractalNoise'<\/span> <span class=\"hljs-attr\">baseFrequency<\/span>=<span class=\"hljs-string\">'.9713'<\/span> <span class=\"hljs-attr\">numOctaves<\/span>=<span class=\"hljs-string\">'4'<\/span>\/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feDisplacementMap<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceGraphic'<\/span> <span class=\"hljs-attr\">scale<\/span>=<span class=\"hljs-string\">'150'<\/span> <span class=\"hljs-attr\">xChannelSelector<\/span>=<span class=\"hljs-string\">'R'<\/span>\/&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Since the <code>&lt;svg&gt;<\/code> element is only used to hold our <code>filter<\/code> (and the only thing a <code>filter<\/code> does is apply a graphical effect on an already existing element), it is functionally the same as a <code>&lt;style&gt;<\/code> element, so we zero its dimensions and hide it from screen readers using <code>aria-hidden<\/code>. And, in the CSS, we also take it out of the document flow (via <code>absolute<\/code> or <code>fixed<\/code> positioning) so it doesn&#8217;t affect our layout in any way (which could happen otherwise, even if its dimensions are zeroed).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">svg<\/span><span class=\"hljs-selector-attr\">&#91;height=<span class=\"hljs-string\">'0'<\/span>]<\/span><span class=\"hljs-selector-attr\">&#91;aria-hidden=<span class=\"hljs-string\">'true'<\/span>]<\/span> { <span class=\"hljs-attribute\">position<\/span>: fixed }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The <code>&lt;filter&gt;<\/code> element also has a second attribute beside its <code>id<\/code>. We aren&#8217;t going into it here because I don&#8217;t really understand it myself. Just know that, in order to get our desired result cross-browser, we always need to set this attribute to <code>sRGB<\/code> whenever we&#8217;re doing anything with the RGB channels in the <code>filter<\/code>. The <code>sRGB<\/code> value isn&#8217;t the default one (<code>linearRGB<\/code> is), but it&#8217;s the one we likely want most of the time and the only one that works properly cross-browser.<\/p>\n\n\n\n<p>The <a href=\"https:\/\/drafts.fxtf.org\/filter-effects\/#feTurbulenceElement\"><code>feTurbulence<\/code><\/a> primitive creates a fine-grained noise layer. Again, we aren&#8217;t going into how this works in the back because I haven&#8217;t been able to really understand any of the explanations I&#8217;ve found or I&#8217;ve been recommended for the life of me.<\/p>\n\n\n\n<p>Just know that the <code>baseFrequency<\/code> values (which you can think of as being the number of waves per pixel) need to be positive, that integer values produce just blank and that bigger values mean a finer grained noise. And that <code>numOctaves<\/code> values above the default <code>1<\/code> allow us to get a better-looking noise without having to layer the results of multiple <code>feTurbulence<\/code> primitives with different <code>baseFrequency<\/code> values. In practice, I pretty much never use <code>numOctaves<\/code> values bigger than <code>3<\/code> or at most <code>4<\/code> as I find above that, the visual gain really can&#8217;t justify the performance cost.<\/p>\n\n\n\n<p>We also switch here from the default <code>type<\/code> of <code>turbulence<\/code> to <code>fractalNoise<\/code>, which is what&#8217;s suited for creating a noise layer.<\/p>\n\n\n\n<p>This noise is then used as a displacement map (the second input, <code>in2<\/code>, which is by default the result of the previous primitive, <code>feTurbulence<\/code> here, so we don&#8217;t need to set it explicitly) for the <code>filter<\/code> input (<code>SourceGraphic<\/code>). We use a <code>scale<\/code> value of <code>150<\/code>, which means that the maximum an input pixel can be displaced by in either direction of the <code>x<\/code> or <code>y<\/code> axis is half of that (<code>75px<\/code>) in the event the channel used for <em>x<\/em> or <em>y<\/em> axis displacement is either zeroed (<code>0<\/code>) or maxed out (<code>1<\/code>) there. The channel used for the <em>y<\/em> axis displacement is the default alpha <code>A<\/code>, so we don&#8217;t need to set it explicitly, we only set it for the <em>x<\/em> axis displacement.<\/p>\n\n\n\n<p>We&#8217;re using absolute pixel displacement here, as relative displacement (which requires the <code>primitiveUnits<\/code> attribute to be set to <code>objectBoundingBox<\/code> on the <code>&lt;filter&gt;<\/code> element) is not explicitly defined in the spec, so Chrome, Firefox and Safari each <a href=\"https:\/\/github.com\/w3c\/fxtf-drafts\/issues\/596\">implement it in a different way<\/a> from the other two for non-square <code>filter<\/code> inputs. I wish that could be a joke, but it&#8217;s not. This is why nobody really uses SVG filters much \u2014 a lot about them just doesn&#8217;t work. Not consistently across browsers anyway.<\/p>\n\n\n\n<p>At this point, our result looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"450\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451859318-728b1207-4225-473e-a773-70511b3f1552.png?resize=900%2C450&#038;ssl=1\" alt=\"Grainy gradient with dithered edges. A bright pink outline shows us the boundary of the filter input. Within this boundary, we have transparent pixels. Outside it, we have opaque pixels.\" class=\"wp-image-6072\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451859318-728b1207-4225-473e-a773-70511b3f1552.png?w=900&amp;ssl=1 900w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451859318-728b1207-4225-473e-a773-70511b3f1552.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451859318-728b1207-4225-473e-a773-70511b3f1552.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><\/figure>\n\n\n\n<p>Not quite what we want. The dashed bright pink line shows us where the boundary of the <code>filter<\/code>&nbsp;input gradient box was. Along the edges, we have both transparent pixels <em>inside<\/em>&nbsp;the initial gradient box&nbsp;<em>and<\/em>&nbsp;opaque pixels&nbsp;<em>outside<\/em>&nbsp;the initial gradient box. Two different problems, each needing to get fixed in a different way.<\/p>\n\n\n\n<p>To cover up the transparent pixels <em>inside<\/em> the initial gradient box, we layer the initial gradient underneath the one scrambled by <code>feDisplacementMap<\/code>. We do this using <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/SVG\/Reference\/Element\/feBlend\"><code>feBlend<\/code><\/a> with the default <code>mode<\/code> of <code>normal<\/code> (so we don&#8217;t need to set it explicitly), which means no blending, just put one layer on top of the other. The bottom layer is specified by the second input (<code>in2<\/code>) and in our case, we want it to be the <code>SourceGraphic<\/code>. The top layer is specified by the first input (<code>in<\/code>) and we don&#8217;t need to set it explicitly because, by default, it&#8217;s the result of the previous primitive (<code>feDisplacementMap<\/code> here), which is exactly what we need in this case.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'grain'<\/span> <span class=\"hljs-attr\">color-interpolation-filters<\/span>=<span class=\"hljs-string\">'sRGB'<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feTurbulence<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">'fractalNoise'<\/span> <span class=\"hljs-attr\">baseFrequency<\/span>=<span class=\"hljs-string\">'.9713'<\/span> <span class=\"hljs-attr\">numOctaves<\/span>=<span class=\"hljs-string\">'4'<\/span>\/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feDisplacementMap<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceGraphic'<\/span> <span class=\"hljs-attr\">scale<\/span>=<span class=\"hljs-string\">'150'<\/span> <span class=\"hljs-attr\">xChannelSelector<\/span>=<span class=\"hljs-string\">'R'<\/span>\/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feBlend<\/span> <span class=\"hljs-attr\">in2<\/span>=<span class=\"hljs-string\">'SourceGraphic'<\/span>\/&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>I&#8217;ve seen a lot of tutorials using <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/SVG\/Reference\/Element\/feComposite\"><code>feComposite<\/code><\/a> with the default <code>operator<\/code> of <code>over<\/code> or <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/SVG\/Reference\/Element\/feMerge\"><code>feMerge<\/code><\/a> to place layers one on top of another, but <code>feBlend<\/code> with the default <code>mode<\/code> of <code>normal<\/code> produces the exact same result, I find it to be simpler than <code>feMerge<\/code> in the case of just two layers and it&#8217;s fewer characters than <code>feComposite<\/code>.<\/p>\n\n\n\n<p>To get rid of the opaque pixels  <em>outside<\/em>  the initial gradient box, we restrict the <a href=\"https:\/\/drafts.fxtf.org\/filter-effects\/#FilterEffectsRegion\"><code>filter<\/code>  region<\/a> to its exact input box \u2014 starting from the  <code>0,0<\/code>  point of this input and covering  <code>100%<\/code>  of it along both the  <em>x<\/em>  and  <em>y<\/em>  axis (by default, the  <code>filter<\/code>  region starts from <code>-10%,-10%<\/code> and covers  <code>120%<\/code>  of the input box along each of the two axes). This means explicitly setting the <code>x<\/code>,  <code>y<\/code>,  <code>width<\/code> and <code>height<\/code> attributes:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'grain'<\/span> <span class=\"hljs-attr\">color-interpolation-filters<\/span>=<span class=\"hljs-string\">'sRGB'<\/span> \n\t  <span class=\"hljs-attr\">x<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">y<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'1'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'1'<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feTurbulence<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">'fractalNoise'<\/span> <span class=\"hljs-attr\">baseFrequency<\/span>=<span class=\"hljs-string\">'.9713'<\/span> <span class=\"hljs-attr\">numOctaves<\/span>=<span class=\"hljs-string\">'4'<\/span>\/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feDisplacementMap<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceGraphic'<\/span> <span class=\"hljs-attr\">scale<\/span>=<span class=\"hljs-string\">'150'<\/span> <span class=\"hljs-attr\">xChannelSelector<\/span>=<span class=\"hljs-string\">'R'<\/span>\/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feBlend<\/span> <span class=\"hljs-attr\">in2<\/span>=<span class=\"hljs-string\">'SourceGraphic'<\/span>\/&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Another option to get rid of this second problem would be to use&nbsp;<code>clip-path: inset(0)<\/code> on the element we apply this grainy&nbsp;<code>filter<\/code>&nbsp;on. This is one situation where it&#8217;s convenient that <code>clip-path<\/code>&nbsp;<a href=\"https:\/\/bsky.app\/profile\/anatudor.bsky.social\/post\/3lmgrfcnbsc2k\">gets applied&nbsp;<em>after<\/em><\/a>&nbsp;<code>filter<\/code>&nbsp;(the order in the CSS doesn&#8217;t matter here).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grad-box<\/span> {\n  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">linear-gradient<\/span>(<span class=\"hljs-number\">90deg<\/span>, #a9613a, #<span class=\"hljs-number\">1<\/span>e1816);\n  <span class=\"hljs-attribute\">clip-path<\/span>: <span class=\"hljs-built_in\">inset<\/span>(<span class=\"hljs-number\">0<\/span>);\n  <span class=\"hljs-attribute\">filter<\/span>: <span class=\"hljs-built_in\">url<\/span>(#grain)\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"450\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451866616-3fc25f74-226d-4067-bf74-2cf8c446cbe5.png?resize=900%2C450&#038;ssl=1\" alt=\"Grainy gradient with sharp edges, no tansparent pixels within, no opaque pixels outside.\" class=\"wp-image-6075\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451866616-3fc25f74-226d-4067-bf74-2cf8c446cbe5.png?w=900&amp;ssl=1 900w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451866616-3fc25f74-226d-4067-bf74-2cf8c446cbe5.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451866616-3fc25f74-226d-4067-bf74-2cf8c446cbe5.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><figcaption class=\"wp-element-caption\">the desired result<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">A problem with this solution<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#a-problem-with-this-solution\"><\/a><\/h3>\n\n\n\n<p>The inconvenient part about this&nbsp;<code>filter<\/code>&nbsp;is that it applies to the entire element, not just its gradient&nbsp;<code>background<\/code>. And maybe we want this element to <em>also<\/em> have text content and a <code>box-shadow<\/code>. Consider the case when before applying the&nbsp;<code>filter<\/code>&nbsp;we set a&nbsp;<code>box-shadow<\/code>&nbsp;and add text content:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"450\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451867920-1d51206d-fdea-4473-b84f-173467951fc7.png?resize=900%2C450&#038;ssl=1\" alt=\"Card with a banded gradient, text and box-shadow.\" class=\"wp-image-6105\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451867920-1d51206d-fdea-4473-b84f-173467951fc7.png?w=900&amp;ssl=1 900w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451867920-1d51206d-fdea-4473-b84f-173467951fc7.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451867920-1d51206d-fdea-4473-b84f-173467951fc7.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><figcaption class=\"wp-element-caption\">the case when we also have a shadow and text<\/figcaption><\/figure>\n\n\n\n<p>In this case, applying the <code>filter<\/code> to the entire element causes all kinds of problems. The text &#8220;dissolves&#8221; into the gradient, the black <code>box-shadow<\/code> outside the box has some pixels displaced <em>inside<\/em> the box over the gradient &#8211; this is really noticeable in the brighter parts of this gradient. Furthermore, if we were to use the <code>clip-path<\/code> fix for the gradient pixels displaced <em>outside<\/em> the initial gradient box, this would also cut away the outer shadow.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"450\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451869235-fc980f9a-2ad7-4f45-9d15-8a575205a83d.png?resize=900%2C450&#038;ssl=1\" alt=\"Previous card with a banded gradient, text and box-shadow, now with a filter pplied on it too. This has unpleasant side effects as dscribed above.\" class=\"wp-image-6104\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451869235-fc980f9a-2ad7-4f45-9d15-8a575205a83d.png?w=900&amp;ssl=1 900w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451869235-fc980f9a-2ad7-4f45-9d15-8a575205a83d.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451869235-fc980f9a-2ad7-4f45-9d15-8a575205a83d.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><figcaption class=\"wp-element-caption\">problems arising when we apply the grainy filter on the entire element<\/figcaption><\/figure>\n\n\n\n<p>The current solution would be to put this gradient in an absolutely positioned pseudo behind the text content (<code>z-index: -1<\/code>), covering the entire <code>padding-box<\/code> of its parent (<a href=\"https:\/\/bsky.app\/profile\/anatudor.bsky.social\/post\/3lfesl2dpj22u\"><code>inset: 0<\/code><\/a>). This separates the parent&#8217;s&nbsp;<code>box-shadow<\/code>&nbsp;and text from the gradient on the pseudo, so applying the&nbsp;<code>filter<\/code>&nbsp;on the pseudo doesn&#8217;t affect the parent&#8217;s <code>box-shadow<\/code>&nbsp;and text.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grad-box<\/span> { <span class=\"hljs-comment\">\/* relevant styles *\/<\/span>\n  <span class=\"hljs-attribute\">positon<\/span>: relative; <span class=\"hljs-comment\">\/* needed for absolutely positioned pseudo *\/<\/span>\n  <span class=\"hljs-attribute\">box-shadow<\/span>: -<span class=\"hljs-number\">2px<\/span> <span class=\"hljs-number\">2px<\/span> <span class=\"hljs-number\">8px<\/span> <span class=\"hljs-number\">#000<\/span>;\n\t\n  &amp;::before {\n    <span class=\"hljs-attribute\">position<\/span>: absolute;\n    <span class=\"hljs-attribute\">inset<\/span>: <span class=\"hljs-number\">0<\/span>;\n    <span class=\"hljs-attribute\">z-index<\/span>: -<span class=\"hljs-number\">1<\/span>;\n    <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">linear-gradient<\/span>(<span class=\"hljs-number\">90deg<\/span>, #a9613a, #<span class=\"hljs-number\">1<\/span>e1816);\n    <span class=\"hljs-attribute\">filter<\/span>: <span class=\"hljs-built_in\">url<\/span>(#grain);\n    <span class=\"hljs-attribute\">clip-path<\/span>: <span class=\"hljs-built_in\">inset<\/span>(<span class=\"hljs-number\">0<\/span>);\n    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">''<\/span> <span class=\"hljs-comment\">\/* pseudo won't show up without it *\/<\/span>\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"450\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451873184-d4a8b150-73ac-4166-bbea-7250387a0a2b.png?resize=900%2C450&#038;ssl=1\" alt=\"Previous card with a gradient, text and box-shadow, except now the gradient is grain, which fixes the banding issue.\" class=\"wp-image-6103\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451873184-d4a8b150-73ac-4166-bbea-7250387a0a2b.png?w=900&amp;ssl=1 900w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451873184-d4a8b150-73ac-4166-bbea-7250387a0a2b.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/451873184-d4a8b150-73ac-4166-bbea-7250387a0a2b.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><figcaption class=\"wp-element-caption\">the desired result when having a shadow and text content (<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/gbbBWzL\">live demo<\/a>)<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Improving things for the future<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#improving-things-for-the-future\"><\/a><\/h3>\n\n\n\n<p>While this works fine, it doesn&#8217;t feel ideal to have to use up a pseudo we might need for something else and, ugh, also have to add all the styles for positioning it along all three axes (the&nbsp;<em>z<\/em>&nbsp;axis is included here too because we need to place the pseudo <em>behind<\/em> the text content).<\/p>\n\n\n\n<p>And we do have a better option! We can apply the filter&nbsp;<em>only<\/em> on the gradient <code>background<\/code> layer using the&nbsp;<code>filter()<\/code>&nbsp;<em>function<\/em>.<\/p>\n\n\n\n<p>This is not the same as the&nbsp;<code>filter<\/code>&nbsp;<em>property<\/em>! It&#8217;s a&nbsp;<em>function<\/em>&nbsp;that outputs an image and takes as arguments an image (which can be a CSS gradient too) and a filter chain. And it can be used anywhere we can use an image in CSS \u2014 as a <code>background-image<\/code>,&nbsp;<code>border-image<\/code>,&nbsp;<code>mask-image<\/code>&#8230; even&nbsp;<code>shape-outside<\/code>!<\/p>\n\n\n\n<p>In our particular case, this would simplify the code as follows:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grad-box<\/span> { <span class=\"hljs-comment\">\/* relevant styles *\/<\/span>\n  <span class=\"hljs-attribute\">box-shadow<\/span>: -<span class=\"hljs-number\">2px<\/span> <span class=\"hljs-number\">2px<\/span> <span class=\"hljs-number\">8px<\/span> <span class=\"hljs-number\">#000<\/span>;\n  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">filter<\/span>(linear-gradient(<span class=\"hljs-number\">90deg<\/span>, #a9613a, #<span class=\"hljs-number\">1<\/span>e1816), <span class=\"hljs-built_in\">url<\/span>(#grain));\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Note that in this case we must restrict the&nbsp;<code>filter<\/code>&nbsp;region from the&nbsp;<code>&lt;filter&gt;<\/code>&nbsp;element attributes, otherwise we run into&nbsp;<a href=\"https:\/\/bugs.webkit.org\/show_bug.cgi?id=291190\">a really weird bug<\/a>&nbsp;in the one browser supporting this, Safari.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"450\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452022538-bf4ed7b8-ccc9-43cd-96e1-771a5cac899b.png?resize=900%2C450&#038;ssl=1\" alt=\"Safari problem: it's trying to fit the filter output, including what goes outside the input image box, into the input image box, basically scaling down the image to make room for its pixels displaced outside its initial boundary by the filter.\" class=\"wp-image-6102\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452022538-bf4ed7b8-ccc9-43cd-96e1-771a5cac899b.png?w=900&amp;ssl=1 900w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452022538-bf4ed7b8-ccc9-43cd-96e1-771a5cac899b.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452022538-bf4ed7b8-ccc9-43cd-96e1-771a5cac899b.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><figcaption class=\"wp-element-caption\">the Safari problem when we don&#8217;t restrict the&nbsp;<code>filter<\/code>&nbsp;region<\/figcaption><\/figure>\n\n\n\n<p>Because, while Safari has supported the <code>filter()<\/code> function since 2015, for about a decade, sadly&nbsp;<a href=\"https:\/\/github.com\/web-platform-tests\/interop\/issues\/717\">no other browser has followed<\/a>. There are bugs open for both&nbsp;<a href=\"https:\/\/issues.chromium.org\/issues\/41208242\">Chrome<\/a>&nbsp;and&nbsp;<a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1191043\">Firefox<\/a>&nbsp;in case anyone wants to show interest in them implementing this.<\/p>\n\n\n\n<p>Here is the&nbsp;<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/LEVxYoL\">live demo<\/a>, but keep in mind it only works in Safari.<\/p>\n\n\n\n<p>This would come in really handy not just for the cases when we want to have text content or visual touches (like&nbsp;<code>box-shadow<\/code>) that remain unaffected by the noise <code>filter<\/code>, but especially for masking. Banding is always a problem when using <code>radial-gradient()<\/code> for a&nbsp;<code>mask<\/code>&nbsp;and, while we can layer multiple (pseudo)elements instead of&nbsp;<code>background<\/code>&nbsp;layers and\/ or borders, masking is a trickier problem.<\/p>\n\n\n\n<p>For example, consider a conic spotlight. That is, a&nbsp;<code>conic-gradient()<\/code>&nbsp;masked by a radial one. In this case, it would really help us to be able to apply a grain <code>filter<\/code> directly to the&nbsp;<code>mask<\/code>&nbsp;gradient.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.conic-spotlight<\/span> {\n  <span class=\"hljs-attribute\">background<\/span>: \n    <span class=\"hljs-built_in\">conic-gradient<\/span>(from <span class=\"hljs-number\">180deg<\/span> - .<span class=\"hljs-number\">5<\/span>*$a at <span class=\"hljs-number\">50%<\/span> <span class=\"hljs-number\">0%<\/span>, \n                   $side-c, #<span class=\"hljs-number\">342443<\/span>, $side-c $a);\n  <span class=\"hljs-attribute\">mask<\/span>: \n    <span class=\"hljs-built_in\">filter<\/span>(\n      radial-gradient(circle closest-side, red, <span class=\"hljs-number\">65%<\/span>, #<span class=\"hljs-number\">0000<\/span>), \n      <span class=\"hljs-built_in\">url<\/span>(#grain))\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>In this particular case, the grain&nbsp;<code>filter<\/code>&nbsp;is even simpler, as we don&#8217;t need to layer the non-grainy input gradient underneath the grainy one (so we ditch that final <code>feBlend<\/code> primitive). Again, remember we need to restrict the <code>filter<\/code>&nbsp;region from the <code>&lt;filter&gt;<\/code> element attributes.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'grain'<\/span> <span class=\"hljs-attr\">color-interpolation-filters<\/span>=<span class=\"hljs-string\">'sRGB'<\/span> <span class=\"hljs-attr\">x<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">y<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'1'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'1'<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feTurbulence<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">'fractalNoise'<\/span> <span class=\"hljs-attr\">baseFrequency<\/span>=<span class=\"hljs-string\">'.9713'<\/span>\/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feDisplacementMap<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceGraphic'<\/span> <span class=\"hljs-attr\">scale<\/span>=<span class=\"hljs-string\">'40'<\/span> <span class=\"hljs-attr\">xChannelSelector<\/span>=<span class=\"hljs-string\">'R'<\/span>\/&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here is the&nbsp;<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/dPoNXWp\">live demo<\/a>. Keep in mind it only works in Safari.<\/p>\n\n\n\n<p>Since we can&#8217;t yet do this cross-browser, our options depend today on our constraints, the exact result we&#8217;re going for.<\/p>\n\n\n\n<p>Do we need an image backdrop behind the spotlight? In this case, we apply the radial <code>mask<\/code>&nbsp;on the <code>.conic-spotlight<\/code> element and, since, just like <code>clip-path<\/code>, <code>mask<\/code> gets applied <em>after<\/em> <code>filter<\/code>, we add a wrapper around this element to set the&nbsp;<code>filter<\/code>&nbsp;on it. Alternatively, we could set the conic spotlight&nbsp;<code>background<\/code>&nbsp;and the radial&nbsp;<code>mask<\/code>&nbsp;on a pseudo of our&nbsp;<code>.conic-spotlight<\/code>&nbsp;and set the&nbsp;<code>filter<\/code>&nbsp;on the actual element.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"SCSS\" data-shcb-language-slug=\"scss\"><span><code class=\"hljs language-scss\"><span class=\"hljs-selector-class\">.conic-spotlight<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: grid;\n  <span class=\"hljs-attribute\">filter<\/span>: url(#grain);\n\t\n  &amp;<span class=\"hljs-selector-pseudo\">::before<\/span> {\n    <span class=\"hljs-attribute\">background<\/span>: \n      conic-gradient(from <span class=\"hljs-number\">180deg<\/span> - .<span class=\"hljs-number\">5<\/span>*<span class=\"hljs-variable\">$a<\/span> at <span class=\"hljs-number\">50%<\/span> <span class=\"hljs-number\">0%<\/span>, \n                     <span class=\"hljs-variable\">$side-c<\/span>, <span class=\"hljs-number\">#342443<\/span>, <span class=\"hljs-variable\">$side-c<\/span> <span class=\"hljs-variable\">$a<\/span>);\n    <span class=\"hljs-attribute\">mask<\/span>: radial-gradient(circle closest-side, red, <span class=\"hljs-number\">65%<\/span>, <span class=\"hljs-number\">#0000<\/span>);\n    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">''<\/span>\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">SCSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">scss<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If however we only need a solid backdrop (a black one for example), then we could use a second gradient layer as a radial cover on top of the&nbsp;<code>conic-gradient()<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"SCSS\" data-shcb-language-slug=\"scss\"><span><code class=\"hljs language-scss\"><span class=\"hljs-selector-tag\">body<\/span> { <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-variable\">$back-c<\/span> }\n\n.conic-spotlight {\n  background:\n    radial-gradient(circle closest-side, <span class=\"hljs-number\">#0000<\/span>, <span class=\"hljs-number\">65%<\/span>, <span class=\"hljs-variable\">$back-c<\/span>), \n    conic-gradient(from <span class=\"hljs-number\">180deg<\/span> - .<span class=\"hljs-number\">5<\/span>*<span class=\"hljs-variable\">$a<\/span> at <span class=\"hljs-number\">50%<\/span> <span class=\"hljs-number\">0%<\/span>, \n                   <span class=\"hljs-variable\">$side-c<\/span>, <span class=\"hljs-number\">#342443<\/span>, <span class=\"hljs-variable\">$side-c<\/span> <span class=\"hljs-variable\">$a<\/span>);\n  <span class=\"hljs-attribute\">filter<\/span>: url(#grain)\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">SCSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">scss<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_xxMvLWy\" src=\"\/\/codepen.io\/anon\/embed\/xxMvLWy?height=450&amp;theme-id=47434&amp;slug-hash=xxMvLWy&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed xxMvLWy\" title=\"CodePen Embed xxMvLWy\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Note that neither of these two emulate the Safari-only demo exactly because they apply the grain <code>filter<\/code> on the whole thing, not just on the <code>radial-gradient()<\/code> (which allows us to get rid of the <code>mask<\/code> banding, but preserve it for the <code>conic-gradient()<\/code> to give the radiating rays effect). We could tweak the second approach to make the cover a separate pseudo-element instead of a <code>background<\/code> layer and apply the grain <code>filter<\/code> just on that pseudo, but it&#8217;s still more complicated than the <code>filter()<\/code> approach. Which is why it would be very good to have it cross-browser.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Some more examples<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#some-more-examples\"><\/a><\/h2>\n\n\n\n<p>Let&#8217;s see a few more interesting demos where we&#8217;ve made visuals grainy!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Grainy image shadows<\/h3>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"988\" height=\"555\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452726618-e0586989-c1cc-491e-b2b2-e0b8b602f393.png?resize=988%2C555&#038;ssl=1\" alt=\"a grid of square images, each with a grainy shadow that's a blurred copy of itself\" class=\"wp-image-6101\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452726618-e0586989-c1cc-491e-b2b2-e0b8b602f393.png?w=988&amp;ssl=1 988w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452726618-e0586989-c1cc-491e-b2b2-e0b8b602f393.png?resize=300%2C169&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452726618-e0586989-c1cc-491e-b2b2-e0b8b602f393.png?resize=768%2C431&amp;ssl=1 768w\" sizes=\"auto, (max-width: 988px) 100vw, 988px\" \/><figcaption class=\"wp-element-caption\">realistic grainy image shadows<\/figcaption><\/figure>\n\n\n\n<p><a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#grainy-image-shadows\"><\/a>Shadows or blurred elements can also exhibit banding issues where their edges fade. In this demo, we&#8217;re using a slightly more complex <code>filter<\/code> to first&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/SVG\/Reference\/Element\/feGaussianBlur\">blur<\/a>&nbsp;and&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/SVG\/Reference\/Element\/feOffset\">offset<\/a>&nbsp;the input image, then using the <code>feTurbulence<\/code> and <code>feDisplacementMap<\/code> combination to make this blurred and offset input copy grainy. We also decrease its alpha a tiny little bit (basically multiplying it with&nbsp;<code>.9<\/code>). Finally, we&#8217;re placing the original <code>filter<\/code> input image on top of this blurred, offset, grainy and slightly faded copy.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">- let d = .1;\n\nsvg(width='0' height='0' aria-hidden='true')\n  filter#shadow(x='-100%' y='-100%' width='300%' height='300%'\n                color-interpolation-filters='sRGB'\n                primitiveUnits='objectBoundingBox')\n    \/\/- blur image\n    feGaussianBlur(stdDeviation=d)\n    \/\/- then offset it and save it as 'in'\n    feOffset(dx=d dy=d result='in')\n    \/\/- generate noise\n    feTurbulence(type='fractalNoise' baseFrequency='.9713')\n    \/\/- use noise as displacement map to scramble a bit the blurred &amp; offset image\n    feDisplacementMap(in='in' scale=2*d xChannelSelector='R')\n    \/\/- decrease alpha a little bit\n    feComponentTransfer\n      feFuncA(type='linear' slope='.9')\n    \/\/- add original image on top\n    feBlend(in='SourceGraphic')<\/pre>\n\n\n\n<p>Since our input images are square here, we can use relative length values (by setting <code>primitiveUnits<\/code> to <code>ObjectBoundingBox<\/code>) and still get the same result cross-browser. A relative offset of&nbsp;<code>1<\/code>&nbsp;is equal to the square image edge length, both for the <code>dx<\/code>&nbsp;and <code>dy<\/code> attributes of&nbsp;<code>feOffset<\/code>&nbsp;and for the <code>scale<\/code> attribute of <code>feDisplacementMap<\/code>.<\/p>\n\n\n\n<p>In our case, the&nbsp;<code>dx<\/code>&nbsp;and&nbsp;<code>dy<\/code>&nbsp;offsets being set to&nbsp;<code>.1<\/code>&nbsp;means we offset the blurred square image copy by&nbsp;<code>10%<\/code>&nbsp;of its edge length along each of the two axes. And the displacement <code>scale<\/code> being set to&nbsp;<code>.2<\/code>&nbsp;means any pixel of the blurred and offset copy may be displaced by at most half of that (half being&nbsp;<code>10%<\/code>&nbsp;of the square image edge), with plus or with minus, along both the&nbsp;<code>x<\/code>&nbsp;and&nbsp;<code>y<\/code>&nbsp;axes. And it gets displaced by that much when the selected channel (given by <code>xChannelSelector<\/code> and <code>yChannelSelector<\/code>) of the corresponding map pixel is either zeroed (in which case it&#8217;s displaced in the positive direction) or maxed out (negative displacement).<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_OJYwgpe\" src=\"\/\/codepen.io\/anon\/embed\/OJYwgpe?height=550&amp;theme-id=47434&amp;slug-hash=OJYwgpe&amp;default-tab=result\" height=\"550\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed OJYwgpe\" title=\"CodePen Embed OJYwgpe\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>The shadow doesn&#8217;t need to be a copy of the input image, it can also be a plain rectangle:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'shadow'<\/span> <span class=\"hljs-attr\">x<\/span>=<span class=\"hljs-string\">'-50%'<\/span> <span class=\"hljs-attr\">y<\/span>=<span class=\"hljs-string\">'-50%'<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'200%'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'200%'<\/span>\n          <span class=\"hljs-attr\">color-interpolation-filters<\/span>=<span class=\"hljs-string\">'sRGB'<\/span>\n          <span class=\"hljs-attr\">primitiveUnits<\/span>=<span class=\"hljs-string\">'objectBoundingBox'<\/span>&gt;<\/span>\n    <span class=\"hljs-comment\">&lt;!-- flood entire filter region with orangered --&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feFlood<\/span> <span class=\"hljs-attr\">flood-color<\/span>=<span class=\"hljs-string\">'orangered'<\/span>\/&gt;<\/span>\n    <span class=\"hljs-comment\">&lt;!-- restrict to rectangle of filter input (our image)  --&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feComposite<\/span> <span class=\"hljs-attr\">in2<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">operator<\/span>=<span class=\"hljs-string\">'in'<\/span>\/&gt;<\/span>\n    <span class=\"hljs-comment\">&lt;!-- blur and everything else just like before  --&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_MWPZNMw\" src=\"\/\/codepen.io\/anon\/embed\/MWPZNMw?height=510&amp;theme-id=47434&amp;slug-hash=MWPZNMw&amp;default-tab=result\" height=\"510\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed MWPZNMw\" title=\"CodePen Embed MWPZNMw\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Grainy image fade<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#grainy-image-fade\"><\/a><\/h3>\n\n\n\n<p>This is pretty similar to the previous demo, except what we displace are the semi-transparent fading edge pixels obtained using a blur. And we obviously don&#8217;t layer the original image on top.<\/p>\n\n\n\n<p>There are a couple more little tricks used here to get things just right, but they&#8217;re outside the scope of this article, so we&#8217;re not going into them here.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_LYgqPbQ\" src=\"\/\/codepen.io\/anon\/embed\/LYgqPbQ?height=500&amp;theme-id=47434&amp;slug-hash=LYgqPbQ&amp;default-tab=result\" height=\"500\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed LYgqPbQ\" title=\"CodePen Embed LYgqPbQ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Noisy gradient discs<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#noisy-gradient-discs\"><\/a><\/h3>\n\n\n\n<p>These are created with SVG&nbsp;<code>&lt;circle&gt;<\/code>&nbsp;elements just so we can use SVG radial gradients for them. Compared to CSS <code>radial-grdient()<\/code>, SVG <code>radialGradient<\/code> has the advantage of allowing us to&nbsp;<a href=\"https:\/\/svgwg.org\/svg2-draft\/pservers.html#RadialGradientAttributes\">specify a focal point<\/a>&nbsp;(via&nbsp;<code>fx<\/code>&nbsp;and <code>fy<\/code>), which allows us to create radial gradients not possible with pure CSS.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bGJvajr\" src=\"\/\/codepen.io\/anon\/embed\/bGJvajr?height=500&amp;theme-id=47434&amp;slug-hash=bGJvajr&amp;default-tab=result\" height=\"500\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bGJvajr\" title=\"CodePen Embed bGJvajr\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>The&nbsp;<code>filter<\/code>&nbsp;is a bit more complex here because the aim was to create a specific type of noise, but the main idea is the same.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Animated single&nbsp;<code>img<\/code>&nbsp;gradient glow border<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#animated-single-img-gradient-glow-border\"><\/a><\/h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452727808-37f47164-c7f2-4340-a9bc-f07f81973d7c-2.png?resize=1024%2C576&#038;ssl=1\" alt=\"a grid of images with total or partial gradient borders, each having a glow, which is a grainy glow for every second image\" class=\"wp-image-6100\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452727808-37f47164-c7f2-4340-a9bc-f07f81973d7c-2.png?resize=1024%2C576&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452727808-37f47164-c7f2-4340-a9bc-f07f81973d7c-2.png?resize=300%2C169&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452727808-37f47164-c7f2-4340-a9bc-f07f81973d7c-2.png?resize=768%2C432&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/452727808-37f47164-c7f2-4340-a9bc-f07f81973d7c-2.png?w=1172&amp;ssl=1 1172w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">screenshot (<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/bGPMOpJ\">live demo<\/a>)<\/figcaption><\/figure>\n\n\n\n<p>Animated gradient glow borders seem to be all the rage nowadays, which is something I never imagined woukd happen when I first started playing with them almost a decade ago. But wherever there&#8217;s a fade effect like a glow, we may get banding. It&#8217;s pretty subtle in this case, but the grainy glow looks better than the no grain version.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Grainy CSS backgrounds<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#grainy-css-backgrounds\"><\/a><\/h3>\n\n\n\n<p>Another example would be this one, where I&#8217;m layering a bunch of linear gradients along the circumradii to the corners of a regular polygon in order to emulate a mesh gradient. Even when blending these gradients, subtle banding is still noticeable. Applying our standard grain&nbsp;<code>filter<\/code>&nbsp;discussed earlier fixes this problem.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_abxpmMe\" src=\"\/\/codepen.io\/anon\/embed\/abxpmMe?height=480&amp;theme-id=47434&amp;slug-hash=abxpmMe&amp;default-tab=result\" height=\"480\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed abxpmMe\" title=\"CodePen Embed abxpmMe\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Also, since we&#8217;re using&nbsp;<code>clip-path<\/code>&nbsp;to get the polygon shape and this is applied <em>after<\/em> the <code>filter<\/code>, we don&#8217;t need to worry about opaque pixels displaced&nbsp;<em>outside<\/em>&nbsp;the polygon shape by our grain&nbsp;<code>filter<\/code>. This means we don&#8217;t need to bother with setting the&nbsp;<code>filter<\/code> region via the <code>&lt;filter&gt;<\/code> element attributes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Grainy SVG backgrounds<a href=\"https:\/\/gist.github.com\/thebabydino\/effda894a4a80060d25639f54058a3ad#grainy-svg-backgrounds\"><\/a><\/h3>\n\n\n\n<p>The idea here is we layer a bunch of different SVG shapes, give them various fills (plain, <code>linearGradient<\/code> or <code>radialGradient<\/code> ones), blur them and then finally apply a grain&nbsp;<code>filter<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"616\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/450825545-32a13512-1b71-47bf-86eb-ddc424e6e6bb.png?resize=1024%2C616&#038;ssl=1\" alt=\"a 3\u2a2f3 grid of grainy abstract backgrounds\" class=\"wp-image-6096\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/450825545-32a13512-1b71-47bf-86eb-ddc424e6e6bb.png?resize=1024%2C616&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/450825545-32a13512-1b71-47bf-86eb-ddc424e6e6bb.png?resize=300%2C181&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/450825545-32a13512-1b71-47bf-86eb-ddc424e6e6bb.png?resize=768%2C462&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/450825545-32a13512-1b71-47bf-86eb-ddc424e6e6bb.png?resize=1536%2C924&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/450825545-32a13512-1b71-47bf-86eb-ddc424e6e6bb.png?w=1600&amp;ssl=1 1600w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">grainy SVG backgrounds (<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/azONXNb\/\">live demo<\/a>)<\/figcaption><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>This is about reducing banding effects in gradients by introducing noise. A nice approach is a displacement map using SVG filters.<\/p>\n","protected":false},"author":32,"featured_media":6150,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[201,91],"class_list":["post-6066","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-gradients","tag-svg"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Grainy-Gradients.jpg?fit=1140%2C676&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6066","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/32"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=6066"}],"version-history":[{"count":13,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6066\/revisions"}],"predecessor-version":[{"id":6322,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6066\/revisions\/6322"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/6150"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=6066"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=6066"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=6066"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}