\\n <feDisplacementMap in=\'SourceGraphic\' scale=\'150\' xChannelSelector=\'R\'/>\\n </filter>\\n</svg>\\n\\n\\n
Since the <svg>
element is only used to hold our filter
(and the only thing a filter
does is apply a graphical effect on an already existing element), it is functionally the same as a <style>
element, so we zero its dimensions and hide it from screen readers using aria-hidden
. And, in the CSS, we also take it out of the document flow (via absolute
or fixed
positioning) so it doesn’t affect our layout in any way (which could happen otherwise, even if its dimensions are zeroed).
svg[height=\'0\'][aria-hidden=\'true\'] { position: fixed }
\\n\\n\\nThe <filter>
element also has a second attribute beside its id
. We aren’t going into it here because I don’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 sRGB
whenever we’re doing anything with the RGB channels in the filter
. The sRGB
value isn’t the default one (linearRGB
is), but it’s the one we likely want most of the time and the only one that works properly cross-browser.
The feTurbulence
primitive creates a fine-grained noise layer. Again, we aren’t going into how this works in the back because I haven’t been able to really understand any of the explanations I’ve found or I’ve been recommended for the life of me.
Just know that the baseFrequency
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 numOctaves
values above the default 1
allow us to get a better-looking noise without having to layer the results of multiple feTurbulence
primitives with different baseFrequency
values. In practice, I pretty much never use numOctaves
values bigger than 3
or at most 4
as I find above that, the visual gain really can’t justify the performance cost.
We also switch here from the default type
of turbulence
to fractalNoise
, which is what’s suited for creating a noise layer.
This noise is then used as a displacement map (the second input, in2
, which is by default the result of the previous primitive, feTurbulence
here, so we don’t need to set it explicitly) for the filter
input (SourceGraphic
). We use a scale
value of 150
, which means that the maximum an input pixel can be displaced by in either direction of the x
or y
axis is half of that (75px
) in the event the channel used for x or y axis displacement is either zeroed (0
) or maxed out (1
) there. The channel used for the y axis displacement is the default alpha A
, so we don’t need to set it explicitly, we only set it for the x axis displacement.
We’re using absolute pixel displacement here, as relative displacement (which requires the primitiveUnits
attribute to be set to objectBoundingBox
on the <filter>
element) is not explicitly defined in the spec, so Chrome, Firefox and Safari each implement it in a different way from the other two for non-square filter
inputs. I wish that could be a joke, but it’s not. This is why nobody really uses SVG filters much — a lot about them just doesn’t work. Not consistently across browsers anyway.
At this point, our result looks like this:
\\n\\n\\n\\nNot quite what we want. The dashed bright pink line shows us where the boundary of the filter
input gradient box was. Along the edges, we have both transparent pixels inside the initial gradient box and opaque pixels outside the initial gradient box. Two different problems, each needing to get fixed in a different way.
To cover up the transparent pixels inside the initial gradient box, we layer the initial gradient underneath the one scrambled by feDisplacementMap
. We do this using feBlend
with the default mode
of normal
(so we don’t need to set it explicitly), which meands no blending, just put one layer on top of the other. The bottom layer is specified by the second input (in2
) and in our case, we want it to be the SourceGraphic
. The top layer is specified by the first input (in
) and we don’t need to set it explicitly because, by default, it’s the result of the previous primitive (feDisplacementMap
here), which is exactly what we need in this case.
<svg width=\'0\' height=\'0\' aria-hidden=\'true\'>\\n <filter id=\'grain\' color-interpolation-filters=\'sRGB\'>\\n <feTurbulence type=\'fractalNoise\' baseFrequency=\'.9713\' numOctaves=\'4\'/>\\n <feDisplacementMap in=\'SourceGraphic\' scale=\'150\' xChannelSelector=\'R\'/>\\n <feBlend in2=\'SourceGraphic\'/>\\n </filter>\\n</svg>
\\n\\n\\nI’ve seen a lot of tutorials using feComposite
with the default operator
of over
or feMerge
to place layers one on top of another, but feBlend
with the default mode
of normal
produces the exact same result, I find it to be simpler than feMerge
in the case of just two layers and it’s fewer characters than feComposite
.
To get rid of the opaque pixels outside the initial gradient box, we restrict the filter
region to its exact input box — starting from the 0,0
point of this input and covering 100%
of it along both the x and y axis (by default, the filter
region starts from -10%,-10%
and covers 120%
of the input box along each of the two axes). This means explicitly setting the x
, y
, width
and height
attributes:
<svg width=\'0\' height=\'0\' aria-hidden=\'true\'>\\n <filter id=\'grain\' color-interpolation-filters=\'sRGB\' \\n\\t x=\'0\' y=\'0\' width=\'1\' height=\'1\'>\\n <feTurbulence type=\'fractalNoise\' baseFrequency=\'.9713\' numOctaves=\'4\'/>\\n <feDisplacementMap in=\'SourceGraphic\' scale=\'150\' xChannelSelector=\'R\'/>\\n <feBlend in2=\'SourceGraphic\'/>\\n </filter>\\n</svg>
\\n\\n\\nAnother option to get rid of this second problem would be to use clip-path: inset(0)
on the element we apply this grainy filter
on. This is one situation where it’s convenient that clip-path
gets applied after filter
(the order in the CSS doesn’t matter here).
.grad-box {\\n background: linear-gradient(90deg, #a9613a, #1e1816);\\n clip-path: inset(0);\\n filter: url(#grain)\\n}
\\n\\n\\nThe inconvenient part about this filter
is that it applies to the entire element, not just its gradient background
. And maybe we want this element to also have text content and a box-shadow
. Consider the case when before applying the filter
we set a box-shadow
and add text content:
In this case, applying the filter
to the entire element causes all kinds of problems. The text “dissolves” into the gradient, the black box-shadow
outside the box has some pixels displaced inside the box over the gradient – this is really noticeable in the brighter parts of this gradient. Furthermore, if we were to use the clip-path
fix for the gradient pixels displaced outside the initial gradient box, this would also cut away the outer shadow.
The current solution would be to put this gradient in an absolutely positioned pseudo behind the text content (z-index: -1
), covering the entire padding-box
of its parent (inset: 0
). This separates the parent’s box-shadow
and text from the gradient on the pseudo, so applying the filter
on the pseudo doesn’t affect the parent’s box-shadow
and text.
.grad-box { /* relevant styles */\\n positon: relative; /* needed for absolutely positioned pseudo */\\n box-shadow: -2px 2px 8px #000;\\n\\t\\n &::before {\\n position: absolute;\\n inset: 0;\\n z-index: -1;\\n background: linear-gradient(90deg, #a9613a, #1e1816);\\n filter: url(#grain);\\n clip-path: inset(0);\\n content: \'\' /* pseudo won\'t show up without it */\\n }\\n}
\\n\\n\\nWhile this works fine, it doesn’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 z axis is included here too because we need to place the pseudo behind the text content).
\\n\\n\\n\\nAnd we do have a better option! We can apply the filter only on the gradient background
layer using the filter()
function.
This is not the same as the filter
property! It’s a function 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 — as a background-image
, border-image
, mask-image
… even shape-outside
!
In our particular case, this would simplify the code as follows:
\\n\\n\\n.grad-box { /* relevant styles */\\n box-shadow: -2px 2px 8px #000;\\n background: filter(linear-gradient(90deg, #a9613a, #1e1816), url(#grain));\\n}
\\n\\n\\nNote that in this case we must restrict the filter
region from the <filter>
element attributes, otherwise we run into a really weird bug in the one browser supporting this, Safari.
filter
regionBecause, while Safari has supported the filter()
function since 2015, for about a decade, sadly no other browser has followed. There are bugs open for both Chrome and Firefox in case anyone wants to show interest in them implementing this.
Here is the live demo, but keep in mind it only works in Safari.
\\n\\n\\n\\nThis would come in really handy not just for the cases when we want to have text content or visual touches (like box-shadow
) that remain unaffected by the noise filter
, but especially for masking. Banding is always a problem when using radial-gradient()
for a mask
and, while we can layer multiple (pseudo)elements instead of background
layers and/ or borders, masking is a trickier problem.
For example, consider a conic spotlight. That is, a conic-gradient()
masked by a radial one. In this case, it would really help us to be able to apply a grain filter
directly to the mask
gradient.
.conic-spotlight {\\n background: \\n conic-gradient(from 180deg - .5*$a at 50% 0%, \\n $side-c, #342443, $side-c $a);\\n mask: \\n filter(\\n radial-gradient(circle closest-side, red, 65%, #0000), \\n url(#grain))\\n}
\\n\\n\\nIn this particular case, the grain filter
is even simpler, as we don’t need to layer the non-grainy input gradient underneath the grainy one (so we ditch that final feBlend
primitive). Again, remember we need to restrict the filter
region from the <filter>
element attributes.
<svg width=\'0\' height=\'0\' aria-hidden=\'true\'>\\n <filter id=\'grain\' color-interpolation-filters=\'sRGB\' x=\'0\' y=\'0\' width=\'1\' height=\'1\'>\\n <feTurbulence type=\'fractalNoise\' baseFrequency=\'.9713\'/>\\n <feDisplacementMap in=\'SourceGraphic\' scale=\'40\' xChannelSelector=\'R\'/>\\n </filter>\\n</svg>
\\n\\n\\nHere is the live demo. Keep in mind it only works in Safari.
\\n\\n\\n\\nSince we can’t yet do this cross-browser, our options depend today on our constraints, the exact result we’re going for.
\\n\\n\\n\\nDo we need an image backdrop behind the spotlight? In this case, we apply the radial mask
on the .conic-spotlight
element and, since, just like clip-path
, mask
gets applied after filter
, we add a wrapper around this element to set the filter
on it. Alternatively, we could set the conic spotlight background
and the radial mask
on a pseudo of our .conic-spotlight
and set the filter
on the actual element.
.conic-spotlight {\\n display: grid;\\n filter: url(#grain);\\n\\t\\n &::before {\\n background: \\n conic-gradient(from 180deg - .5*$a at 50% 0%, \\n $side-c, #342443, $side-c $a);\\n mask: radial-gradient(circle closest-side, red, 65%, #0000);\\n content: \'\'\\n }\\n}
\\n\\n\\nIf 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 conic-gradient()
:
body { background: $back-c }\\n\\n.conic-spotlight {\\n background:\\n radial-gradient(circle closest-side, #0000, 65%, $back-c), \\n conic-gradient(from 180deg - .5*$a at 50% 0%, \\n $side-c, #342443, $side-c $a);\\n filter: url(#grain)\\n}
\\n\\n\\nNote that neither of these two emulate the Safari-only demo exactly because they apply the grain filter
on the whole thing, not just on the radial-gradient()
(which allows us to get rid of the mask
banding, but preserve it for the conic-gradient()
to give the radiating rays effect). We could tweak the second approach to make the cover a separate pseudo-element instead of a background
layer and apply the grain filter
just on that pseudo, but it’s still more complicated than the filter()
approach. Which is why it would be very good to have it cross-browser.
Let’s see a few more interesting demos where we’ve made visuals grainy!
\\n\\n\\n\\nShadows or blurred elements can also exhibit banding issues where their edges fade. In this demo, we’re using a slightly more complex filter
to first blur and offset the input image, then using the feTurbulence
and feDisplacementMap
combination to make this blurred and offset input copy grainy. We also decrease its alpha a tiny little bit (basically multiplying it with .9
). Finally, we’re placing the original filter
input image on top of this blurred, offset, grainy and slightly faded copy.
- 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 & 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\')\\n\\n\\n\\n
Since our input images are square here, we can use relative length values (by setting primitiveUnits
to ObjectBoundingBox
) and still get the same result cross-browser. A relative offset of 1
is equal to the square image edge length, both for the dx
and dy
attributes of feOffset
and for the scale
attribute of feDisplacementMap
.
In our case, the dx
and dy
offsets being set to .1
means we offset the blurred square image copy by 10%
of its edge length along each of the two axes. And the displacement scale
being set to .2
means any pixel of the blurred and offset copy may be displaced by at most half of that (half being 10%
of the square image edge), with plus or with minus, along both the x
and y
axes. And it gets displaced by that much when the selected channel (given by xChannelSelector
and yChannelSelector
) of the corresponding map pixel is either zeroed (in which case it’s displaced in the positive direction) or maxed out (negative displacement).
The shadow doesn’t need to be a copy of the input image, it can also be a plain rectangle:
\\n\\n\\n<svg width=\'0\' height=\'0\' aria-hidden=\'true\'>\\n <filter id=\'shadow\' x=\'-50%\' y=\'-50%\' width=\'200%\' height=\'200%\'\\n color-interpolation-filters=\'sRGB\'\\n primitiveUnits=\'objectBoundingBox\'>\\n <!-- flood entire filter region with orangered --\x3e\\n <feFlood flood-color=\'orangered\'/>\\n <!-- restrict to rectangle of filter input (our image) --\x3e\\n <feComposite in2=\'SourceAlpha\' operator=\'in\'/>\\n <!-- blur and everything else just like before --\x3e\\n </filter>\\n</svg>
\\n\\n\\nThis 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’t layer the original image on top.
\\n\\n\\n\\nThere are a couple more little tricks used here to get things just right, but they’re outside the scope of this article, so we’re not going into them here.
\\n\\n\\n\\nThese are created with SVG <circle>
elements just so we can use SVG radial gradients for them. Compared to CSS radial-grdient()
, SVG radialGradient
has the advantage of allowing us to specify a focal point (via fx
and fy
), which allows us to create radial gradients not possible with pure CSS.
The filter
is a bit more complex here because the aim was to create a specific type of noise, but the main idea is the same.
img
gradient glow borderAnimated 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’s a fade effect like a glow, we may get banding. It’s pretty subtle in this case, but the grainy glow looks better than the no grain version.
\\n\\n\\n\\nAnother example would be this one, where I’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 filter
discussed earlier fixes this problem.
Also, since we’re using clip-path
to get the polygon shape and this is applied after the filter
, we don’t need to worry about opaque pixels displaced outside the polygon shape by our grain filter
. This means we don’t need to bother with setting the filter
region via the <filter>
element attributes.
The idea here is we layer a bunch of different SVG shapes, give them various fills (plain, linearGradient
or radialGradient
ones), blur them and then finally apply a grain filter
.
There was a lot of interest in our Why Can’t HTML Alone Do Includes? article. I’d like to point you to my ShopTalk Show conversation where we really get into things more with Jake Archibald.
","description":"There was a lot of interest in our Why Can’t HTML Alone Do Includes? article. I’d like to point you to my ShopTalk Show conversation where we really get into things more with Jake Archibald.","guid":"https://frontendmasters.com/blog/?p=6114","author":"Chris Coyier","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-06-12T14:30:49.603Z","media":null,"categories":["The Beat","HTML","includes","Podcast"],"attachments":null,"extra":null,"language":null},{"title":"1fr 1fr vs auto auto vs 50% 50%","url":"https://frontendmasters.com/blog/1fr-1fr-vs-auto-auto-vs-50-50/","content":"Are these columns the same?
\\n\\n\\n.grid {\\n display: grid;\\n\\n grid-template-columns: 1fr 1fr;\\n grid-template-columns: 50% 50%;\\n grid-template-columns: auto auto;\\n}
\\n\\n\\nI mean, obviously they aren’t literally the same, but you also probably won’t be surprised that they have different behavior as well. And yet…. they do kinda basically do the same thing. Two equal width columns.
\\n\\n\\nAbove is a screenshot of three different grids, using each of those different grid-template-columns
I showed above. And indeed, they all seem to do the same thing: two equal width columns. I’ve put a red line down the middle and right edge of the container for illustration purposes.
But things start to change as we do different things. For instance, if we apply some gap
between the columns. Here’s the examples with 16px
of gap
applied:
Now the grid with grid-template-columns: 50% 50%;
is busting outside of the container element. Sometimes we think of % units as being quite flexible, but here we’re rather forcefully saying each columns needs to be 50% as wide as it’s parent element, so the width of the whole grid is actually 50% + 16px + 50%
which is wider than 100%.
This is pretty awkward and largely why you don’t see columns set up using % values all that much. But it still can be valuable! The “sturdiness” of setting a column that way can be desirable, as we’ll see. If we wanted to prevent the “blowout”, we could account for the gap ourselves.
\\n\\n\\n.grid {\\n display: grid;\\n\\n /* Make each column smaller equally by half the gap */\\n grid-template-columns: repeat(2, 50% - calc(16px / 2));\\n}
\\n\\n\\nAnother unusual situation can be with auto
. That keyword has some rather special behavior, and it may be worth reading the whole bit that MDN has to say. What’s helpful to me though is to think about the “intrinsic size” of the content inside that auto
column. That can be easy to know. If a column contains only an image that is 200px wide, the intrinsic size is 200px, and the auto column will be 200px. It’s tricky though when the content is text and there are multiple auto
columns with different text.
See above how the auto
column with more text is larger than the auto
column with less text. I have no idea how to explain how that works, but it does make some intuitive sense after a while, even if it feels a bit dangerous to use since it’s hard to know exactly what it’s going to do with arbitrary text.
Let’s consider an <img>
within the columns that is a bit wider than the current width of the columns. Each of the column setups we have behaves a bit differently here.
The sturdy 50%
column remain in place, and the image overflows it. The auto
column grows, but not only to contain the image but a bit wider, as if it’s balancing the intrinsic weights across both columns (or something?).
The column using fr
units (which essentially mean “fractions of the remaining space”) grows to contain the image, but then no more, and it’s sibling fr
columns takes up the remaining space.
Interestingly, if we do the common thing and constrain the width of the image to a max-width: 100%
, the 50%
and 1fr
columns come back down to half width.
Generally, I’d say that fr
units for columns behave the most intuitively and predictably and that’s why you see more grid setups using them.
But fr
units are subject to “blowouts” which can be surprising. A way to think about it is that, however you’ve sized a column, the minimum width of that column is essentially auto
, and that can prevent it from staying sized how you want it to when there is content that pushes it wider. For instance, putting a long URL (no dashes and no spaces in a URL means it can’t “break” or wrap naturally).
You can see the columns blowing out in the auto
and 1fr
columns above. Trying to apply overflow
will not work here alone. We need to essentially give the column permission to be smaller. I typically do that like this:
.grid {\\n display: grid;\\n \\n /* prevent blowouts */\\n grid-template-columns: repeat(2, minmax(0, 1fr));\\n}
\\n\\n\\nThat minmax(0, 1fr)
still sizes the columns at 1fr
, but allows it to shrink in width below what auto
would be, meaning using overflow
will actually work.
There is more to know here, for sure. For instance, all your columns don’t need to be equal. You can mix and match as makes sense.
\\n\\n\\n.grid {\\n display: grid;\\n\\n grid-template-columns: 20% 1fr;\\n grid-template-columns: 2fr 5fr 50px;\\n grid-template-columns: auto 1fr;\\n grid-template-columns: 50ch auto 2fr 1fr;\\n}
\\n\\n\\nAnd there are more keywords that are worth knowing about, namely min-content
, max-content
, and fit-content()
. They are worth playing with particularly if you’ve found yourself in a bind where you can’t quite see to get columns to do what you want. Perhaps we can cover them in more detail later, if you’d find it interesting.