{"id":7855,"date":"2025-12-01T12:14:22","date_gmt":"2025-12-01T17:14:22","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=7855"},"modified":"2025-12-03T18:12:31","modified_gmt":"2025-12-03T23:12:31","slug":"non-square-image-blur-extensions","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/non-square-image-blur-extensions\/","title":{"rendered":"Non-Square Image Blur Extensions"},"content":{"rendered":"\n<p>I recently came across <a href=\"https:\/\/codepen.io\/vii120\/pen\/raxYedQ?editors=0100\">this CodePen demo<\/a> by Vivi Tseng, which creates the blur extension effect by placing a square <code>div<\/code> with a <code>blur()<\/code> beneath the <code>img<\/code> element and I couldn&#8217;t help but think a simpler solution <em>should<\/em> be possible with a single <code>img<\/code> element and minimal CSS.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/article_target_result.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot collage, showing the desired result in two stages. First, on the left we have the base case with the non-square images being padded with a blurred version of themselves up to a square along the axis of their shorter side. Second, on the right, we have an extra touch, the non-square images fade smoothly into their blurred copy on the sides.\" class=\"wp-image-7860\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/article_target_result.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/article_target_result.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/article_target_result.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/article_target_result.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">the result we&#8217;re aiming for<\/figcaption><\/figure>\n\n\n\n<p>So let&#8217;s first take a look at how to approach things for a simple, four CSS declarations solution. And then we&#8217;ll be going through how to tackle extra constraints, such as better support, an extra touch like fading the image into its blur extension and not knowing what orientation an image might have.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-simple-solution-dissected\">The Simple Solution, Dissected<\/h2>\n\n\n\n<p>We have an <code>img<\/code> element:<\/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\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">'my-image.jpg'<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">'image description'<\/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<h3 class=\"wp-block-heading\" id=\"the-1st-declaration\">The 1st Declaration<\/h3>\n\n\n\n<p>We start out by setting either a <code>width<\/code> or a <code>height<\/code> for our <code>img<\/code>. In theory, it doesn&#8217;t matter. In practice, other layout constraints often make using <code>width<\/code> more convenient.<\/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\">width<\/span>: <span class=\"hljs-selector-tag\">min<\/span>(100%, 23<span class=\"hljs-selector-tag\">em<\/span>)<\/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>This is like setting both a <code>width<\/code> and a <code>max-width<\/code> in one declaration thanks to the <code>min()<\/code>. It doesn&#8217;t allow the image to get bigger than <code>23em<\/code>, nor does it allow it to overflow.<\/p>\n\n\n\n<p>Note that setting the <code>width<\/code> this way is making some assumptions about the overall layout that may not always be true. For example, if the <code>width<\/code> of the <code>img<\/code> is constrained by that of a <code>grid<\/code> column, then it&#8217;s probably better to use the <code>min()<\/code> to size the column and then set the <code>width<\/code> or <code>max-width<\/code> of the <code>img<\/code> to <code>100%<\/code>. But since the page layout isn\u2019t central to the technique covered in this article, we\u2019re leaving that discussion aside.<\/p>\n\n\n\n<p>So far, this is what have for four example images arranged in a <code>2\u00d72<\/code> grid.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"740\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_0_size.png?resize=1024%2C740&#038;ssl=1\" alt=\"Screenshot of what the first line of code produces in a larger layout context: a 2\u00d72 grid containing 4 non-square images, both landscape and portrait ones. They all have the same width, but the portrait ones stretch their grid rows down.\" class=\"wp-image-7868\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_0_size.png?resize=1024%2C740&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_0_size.png?resize=300%2C217&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_0_size.png?resize=768%2C555&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_0_size.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">result after setting the <code>width<\/code><\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"the-2nd-declaration\">The 2nd Declaration<\/h3>\n\n\n\n<p>Next, we make our <code>img<\/code> occupy a square box:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">aspect-ratio<\/span>: 1<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><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>Since this technique refers to non-square images, an <code>aspect-ratio<\/code> of <code>1<\/code> stretches our <code>img<\/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=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_1_ratio.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot of what the first two lines of code produce in a larger layout context: a 2\u00d72 square grid containing 4 images, each having either a portrait or landscape orientation, but distorted to fit their containg square boxes.\" class=\"wp-image-7869\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_1_ratio.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_1_ratio.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_1_ratio.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_1_ratio.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">result after setting the <code>aspect-ratio<\/code><\/figcaption><\/figure>\n\n\n\n<p>The stretching doesn&#8217;t look good, which leads us to the next step.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"the-3rd-declaration\">The 3rd Declaration<\/h3>\n\n\n\n<p>The fix for the distortion problem is to use <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/Reference\/Properties\/object-fit\"><code>object-fit<\/code><\/a>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">object-fit<\/span>: <span class=\"hljs-selector-tag\">contain<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><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>contain<\/code> value scales the actual image until its longer side exactly fits the square box of the <code>img<\/code>, leaving empty space around the shorter side. By contrast, <code>cover<\/code> scales the actual image until its shorter side exactly fits the square box of the <code>img<\/code>, cropping the ends of the longer side.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_KwVYjzN\/1f8e53f79e207072d845b85f25cc1eb9\" src=\"\/\/codepen.io\/anon\/embed\/KwVYjzN\/1f8e53f79e207072d845b85f25cc1eb9?height=740&amp;theme-id=1&amp;slug-hash=KwVYjzN\/1f8e53f79e207072d845b85f25cc1eb9&amp;default-tab=result\" height=\"740\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed KwVYjzN\/1f8e53f79e207072d845b85f25cc1eb9\" title=\"CodePen Embed KwVYjzN\/1f8e53f79e207072d845b85f25cc1eb9\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>In both cases, the image is middle aligned&nbsp;with its square box along both axes by default, though we can change that via&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/Reference\/Properties\/object-position\"><code>object-position<\/code><\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_2_alignment.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot of what the first three lines of code produce in a larger layout context: a 2\u00d72 square grid containing 4 non-square images, each tightly fit in the middle of its containing square box, its longer edge sized to exactly fit the square edge, while the image keeps its intrinsic aspect ratio.\" class=\"wp-image-7872\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_2_alignment.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_2_alignment.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_2_alignment.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_2_alignment.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">where we are at this point<\/figcaption><\/figure>\n\n\n\n<p>Background images can also use the <code>contain<\/code> and <code>cover<\/code> values for <code>background-size<\/code> &#8211; we&#8217;ll get to that in a moment.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"the-4th-declaration\">The 4th Declaration<\/h3>\n\n\n\n<p>Finally, we set the <code>background<\/code> of the square <code>img<\/code> element to a a blurred, slightly darkened and desaturated copy of the image. Using a <code>cover<\/code> value for <code>background-size<\/code> ensures the shorter side of the <code>background-image<\/code> exactly fits the square box of the <code>img<\/code> element, while the ends of the longer side are cropped.<\/p>\n\n\n\n<p>This filtered <code>background<\/code> is mostly hidden under the actual image, but can be seen around its shorter side, filling out the remaining space to the boundary of the square <code>img<\/code> box.<\/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-tag\">background<\/span>: \n  <span class=\"hljs-selector-tag\">filter<\/span>(\n    <span class=\"hljs-selector-tag\">src<\/span>(<span class=\"hljs-selector-tag\">attr<\/span>(<span class=\"hljs-selector-tag\">src<\/span>)), \n    <span class=\"hljs-selector-tag\">blur<\/span>(8<span class=\"hljs-selector-tag\">px<\/span>) <span class=\"hljs-selector-tag\">brightness<\/span>(<span class=\"hljs-selector-class\">.8<\/span>) <span class=\"hljs-selector-tag\">contrast<\/span>(<span class=\"hljs-selector-class\">.7<\/span>)\n  ) \n  50% \/ <span class=\"hljs-selector-tag\">cover<\/span><\/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<p>There&#8217;s a lot in that one declaration, so let&#8217;s go through it step by step.<\/p>\n\n\n\n<p>The <a href=\"https:\/\/www.w3.org\/TR\/filter-effects-1\/#FilterCSSImageValue\"><code>filter()<\/code><\/a> <em>function<\/em> is a lesser known CSS feature, but it has been around for over a decade. Unlike the widely used <code>filter<\/code> <em>property<\/em>, which applies visual effects to an element, <code>filter()<\/code> takes an image and a filter chain as arguments and returns the filtered image. Because it produces an image value, we can feed it to any CSS property that accepts an image: <code>background-image<\/code>, <code>border-image-source<\/code>, <code>mask-image<\/code> and so on.<\/p>\n\n\n\n<p>It&#8217;s a CSS feature I&#8217;ve <a href=\"https:\/\/frontendmasters.com\/blog\/grainy-gradients\/\">talked about before<\/a> as it allows us to apply a filter <em>only<\/em> to the <code>background<\/code> of an element, for example making a card&#8217;s gradient <code>background<\/code> grainy without affecting its text content.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/grainy_gradient.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. Shows a card with a grainy light to dark blue radial gradient background. The card also has the text hello! in the middle, unaffected by the filter that makes the background gradient noisy.\" class=\"wp-image-7876\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/grainy_gradient.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/grainy_gradient.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/grainy_gradient.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/grainy_gradient.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">a card with a grainy gradient background<\/figcaption><\/figure>\n\n\n\n<p>The <a href=\"https:\/\/www.w3.org\/TR\/css-values\/#funcdef-src\"><code>src()<\/code><\/a> function is even more obscure. Like <code>url()<\/code>, it represents a URL, but unlike <code>url()<\/code>, it allows other CSS functions inside, such as <code>var()<\/code> or <code>attr()<\/code>.<\/p>\n\n\n\n<p>The <a href=\"https:\/\/developer.chrome.com\/blog\/advanced-attr\">revamped <code>attr()<\/code><\/a> can now be used for more than just <code>content<\/code> values. In our case, we use it to supply the <code>src()<\/code> function with the actual <code>src<\/code> attribute of the <code>img<\/code> element. Extracting the image URL from the <code>src<\/code> attribute allows us to have the filtered <code>background<\/code> of the <code>img<\/code> stay in sync with the actual image while avoiding duplication.<\/p>\n\n\n\n<p>Note the <code>background-position<\/code>, which we must set to <code>50%<\/code> here.<\/p>\n\n\n\n<p>First, this is because when the <code>background-size<\/code> is specified <em>in the shorthand<\/em>, then the <code>background-position<\/code> must be provided there too.<\/p>\n\n\n\n<p>Second, it&#8217;s because the default <code>background-position<\/code> is <code>0\u202f0<\/code>, which pins the top left corner of the <code>background-image<\/code> to the top left corner of the box. So we must explicitly override that in order to middle align the blurred <code>background-image<\/code> along both axes, just like the actual image with <code>object-fit: contain<\/code> is.<\/p>\n\n\n\n<p>When specifying a single <code>background-position<\/code> value, this applies to the <em>x<\/em> axis, while the <em>y<\/em> axis value defaults to <code>50%<\/code>, <em>regardless<\/em> of the <em>x<\/em> axis value. In our case, the <em>x<\/em> axis value <em>happens<\/em> to be <code>50%<\/code> too, but if it was <code>0<\/code>, the <em>y<\/em> axis value would still default to <code>50%<\/code>.<\/p>\n\n\n\n<p>So in the end, our CSS is:<\/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-tag\">img<\/span> {\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">min<\/span>(<span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">23em<\/span>) <span class=\"hljs-comment\">\/* 1 *\/<\/span>;\n  <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span> <span class=\"hljs-comment\">\/* 2 *\/<\/span>;\n  <span class=\"hljs-attribute\">object-fit<\/span>: contain <span class=\"hljs-comment\">\/* 3 *\/<\/span>;\n  <span class=\"hljs-attribute\">background<\/span>: \n    <span class=\"hljs-built_in\">filter<\/span>(\n      src(attr(src)), \n      <span class=\"hljs-built_in\">url<\/span>(#blur) <span class=\"hljs-built_in\">brightness<\/span>(<span class=\"hljs-number\">0.8<\/span>) <span class=\"hljs-built_in\">contrast<\/span>(<span class=\"hljs-number\">0.7<\/span>)\n    )\n    <span class=\"hljs-number\">50%<\/span> \/ cover; <span class=\"hljs-comment\">\/* 4 *\/<\/span>\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<p>This approach doesn&#8217;t require any duplication or knowing anything about the image.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"support-sadness\">Support Sadness<\/h2>\n\n\n\n<p>The problem with the final declaration and the reason why&nbsp;you aren&#8217;t seeing any live demo here yet is, as you may have guessed, support!<\/p>\n\n\n\n<p>While <code>filter()<\/code> has been available in Safari for over a decade, Chrome (<a href=\"https:\/\/issues.chromium.org\/issues\/41208242\">541698<\/a>) and Firefox (<a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1191043\">1191043<\/a>) <em>still<\/em> haven&#8217;t followed.<\/p>\n\n\n\n<p>The improved <code>attr()<\/code> only works in Chromium browsers for now. It isn&#8217;t yet supported by Firefox (<a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=435426\">435426<\/a>) or Safari (<a href=\"https:\/\/bugs.webkit.org\/show_bug.cgi?id=26609\">26609<\/a>), though the Firefox bug has seen quite a bit of activity lately.<\/p>\n\n\n\n<p>Consequently, no single browser currently supports both <code>filter()<\/code> and the enhanced <code>attr()<\/code>.<\/p>\n\n\n\n<p>On top of that, no browser currently implements <code>src()<\/code>. There are issues open in Firefox (<a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1707923\">1707923<\/a>) and Safari (<a href=\"https:\/\/bugs.webkit.org\/show_bug.cgi?id=296953\">296953<\/a>) and I&#8217;ve also opened one for Chrome (<a href=\"https:\/\/issues.chromium.org\/issues\/457465864\">457465864<\/a>) since search didn&#8217;t bring up one there.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><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\/11\/base_3_support.png?resize=1024%2C576&#038;ssl=1\" alt=\"Table screenshot. Shows the support status for the mentioned functions across the three major browsers\/ engines: Chrome, Firefox and Safari. `filter()` has been supported in Safari since version 9, which came out in October 2015. `attr()` has been supported in Chrome since version 133, which came out in February 2025.\" class=\"wp-image-7877\" style=\"width:689px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_3_support.png?resize=1024%2C576&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_3_support.png?resize=300%2C169&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_3_support.png?resize=768%2C432&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_3_support.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">current support situation<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"current-options\">Current Options<\/h2>\n\n\n\n<p>Browser support won\u2019t improve overnight, so today let&#8217;s focus on what solutions we can find that work at least in one current browser.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"using-a-css-variable-for-the-filter-image\">Using a CSS Variable for the <code>filter()<\/code> Image<\/h3>\n\n\n\n<p>Duplicating the image URL in a CSS variable isn\u2019t ideal, but it\u2019s the best we currently have. Even in this scenario, using an HTML preprocessor like Pug lets us keep things DRY (the rest of the HTML blocks in this article will be in Pug):<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">- let src = 'my-image.jpg'\n\nimg(src=src alt='image description' style=`--img: url(${src})`)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><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>The compiled HTML with the duplicated URL looks as follows:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" 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\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">'my-image.jpg'<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">'image description'<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">'--img: url(my-image.jpg)'<\/span> \/&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><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>In the CSS, we replace the unsupported <code>src(attr(src))<\/code> with the CSS variable we&#8217;ve set in the <code>style<\/code> attribute if our <code>img<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">background<\/span>: \n  <span class=\"hljs-selector-tag\">filter<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--img<\/span>), <span class=\"hljs-selector-tag\">blur<\/span>(8<span class=\"hljs-selector-tag\">px<\/span>) <span class=\"hljs-selector-tag\">brightness<\/span>(<span class=\"hljs-selector-class\">.8<\/span>) <span class=\"hljs-selector-tag\">contrast<\/span>(<span class=\"hljs-selector-class\">.7<\/span>)) \n  50% \/ <span class=\"hljs-selector-tag\">cover<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><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>Since only Safari supports the <code>filter()<\/code> function for now, we cannot test this out in Chrome or Firefox. Linux users may still experiment with Safari\u2019s WebKit engine via the <a href=\"https:\/\/flathub.org\/en\/apps\/org.gnome.Epiphany\">Epiphany<\/a> (GNOME Web) browser.<\/p>\n\n\n\n<p>When actually testing, the result doesn&#8217;t seem quite right. The problem becomes obvious if the page backdrop is a small size pattern, for example <a href=\"https:\/\/css-tricks.com\/background-patterns-simplified-by-conic-gradients\/#aa-checkerboard\">a checkerboard one<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. The same 2\u00d72 square grid containing 4 non-square images, each tightly contained in the middle of square box. The difference is that now the empty space along the axis of the smaller image edge has been filled with a blurred version of the same image. Not everything is right though: the pixels of this blurred padding that are close to any of the square box edges are now also semi-transparent.\" class=\"wp-image-7897\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">the result we have so far in Safari<\/figcaption><\/figure>\n\n\n\n<p>This happens because of how the CSS <code>blur()<\/code> filter works and, in particular, the effect it has on the pixels close to the image edge.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_cause.png?resize=1024%2C575&#038;ssl=1\" alt=\"Illustration showing what happens. We have a portrait image used as a `cover` background for a square element. The image boundary thus extends outside the element boundary above and below. When blurring the image via the `filter()` function, the pixels close to the image boundary become transparent. After blurring, when the top and bottom ends of the portrait image are cropped by `background-size: cover`, the result still has semi-transparent edge pixels along the lateral sides.\" class=\"wp-image-7898\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_cause.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_cause.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_cause.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_cause.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">illustrating the mechanism of our problem<\/figcaption><\/figure>\n\n\n\n<p>Blurring operates on each pixel\u2019s RGBA channels individually. Taking the alpha channel in particular, the result is a combination of the current pixel&#8217;s alpha value with the alphas of the surrounding pixels.<\/p>\n\n\n\n<p>Near the image boundary, things get trickier. Some of the pixels around our current one lie <em>outside<\/em> the image. The CSS <code>blur()<\/code> filter treats the out\u2011of\u2011bounds pixels as fully transparent. Consequently, the resulting alpha becomes subunitary, which makes the pixels close to the edge semi\u2011transparent. This is most obvious when we have a patterned backdrop showing through the semi-transparent pixels.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_blur_process.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot of an interactive slide from an older talk. Shows the blurring process. For this, we have `4px\u00d74px` element, which is basically a `4\u00d74` grid of pixels, each holding its RGBA channel values. We also have a `3\u00d73` grid of weights - the size of this grid depends on the blur radius. This grid of weights can be moved around such that at least one of its cells overlaps a cell of the image grid. The grid of weights is always centered around the pixel currently modified by the blurring process. For the alpha channel, the weights on the grid are each multiplied with the alphas of the pixels they overlap. The alpha of the pixels within the element boundary is `1`, the alpha of those outside is `0`.  The weights are `4` in the middle of the grid, `2` in all other cells of the mid row\/ column and `1` in the corners, so the further away a pixel is from the current one, the less it influences its RGBA values post blur. In the case when the grid of weights is centered on a corner pixel of the element, then it overlaps 4 pixels of the element: the one underneath, whose alpha of `1` gets multiplied with a weight of `4` the two adjacent to it, whose alpha of `1` gets multiplied with a weight of `2` and the one it has a corner in common with, whose alpha of `1` gets multiplied with a weight of `1`. Summing up, we have `4 + 2*2 + 1 = 9` (all other weights get multiplied with the alphas of the pixels outside the element's bpoundary, which are taken to be transparent). This gets divided by the sum of all weights on the grid, which is `16`, giving us `.5625`.\" class=\"wp-image-7899\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_blur_process.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_blur_process.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_blur_process.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_blur_process.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">how blurring works, illustrated with a <code>4px\u00d74px<\/code> element and a <code>3\u00d73<\/code> weight grid; the weights are multiplied with the alphas of the corresponding pixels and the linear combination is divided by the sum of all weights<\/figcaption><\/figure>\n\n\n\n<p>In this case, we only have semi-transparent pixels along two opposing edges because the image is <em>first<\/em> blurred within the <code>filter()<\/code> function, <em>then<\/em> used as a <code>background-image<\/code> that gets cropped by <code>background-size: cover<\/code>.<\/p>\n\n\n\n<p>The solution is to switch from a pure CSS <code>blur()<\/code> to an SVG <code>filter<\/code>. This is pretty straightforward. The SVG <code>filter<\/code> contains a single <code>feGaussianBlur<\/code> primitive, which provides an <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/SVG\/Reference\/Attribute\/edgeMode\"><code>edgeMode<\/code><\/a> attribute to control what happens at the image edges. The default value of <code>none<\/code> produces the same result as the CSS <code>blur()<\/code>. By contrast, a value of <code>duplicate<\/code> prevents the unwanted edge semi-transparency.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">svg(width='0' height='0' aria-hidden='true')\n  filter#blur(color-interpolation-filters='sRGB')\n    feGaussianBlur(stdDeviation='8' edgeMode='duplicate')<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><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>The <code>svg<\/code> element exists solely to host the <code>filter<\/code>. It is functionally the same as a <code>style<\/code> element, so we zero its dimensions, hide it from screen readers and, in the CSS, remove it from the document flow with <code>position: fixed<\/code>.<\/p>\n\n\n\n<p>When an SVG <code>filter<\/code> modifies RGB values, we must set its <code>color-interpolation-filters<\/code> attribute to <code>sRGB<\/code>. This is the case here, since the <code>feGaussianBlur<\/code> combines the RGBA channels of every pixel with those of the pixels around it and, in the case of images, adjacent pixels normally have different RGB values.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_corrected.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. The same 2\u00d72 square grid containing 4 non-square images, each tightly contained in the middle square box and with a blur extension filling the empty cell space along the axis of its shorter edge. The problem of the semi-transparent pixels close to the cell edges is now fixed: all pixels of the blur extension are now fully opaque, something made obvious by the checkerboard pattern backdrop behind.\" class=\"wp-image-7900\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_corrected.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_corrected.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_corrected.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_edge_issue_corrected.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">the correct simple blur extension (Safari only <a href=\"https:\/\/codepen.io\/thebabydino\/pen\/gbPZyKZ\">live demo<\/a>)<\/figcaption><\/figure>\n\n\n\n<p>Remember this approach works only in Safari, since both the <code>filter()<\/code> function and the <code>edgeMode<\/code> attribute are still unsupported in Chrome and Firefox.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"getting-around-poor-filter-support-for-a-cross-browser-solution\">Getting Around Poor <code>filter()<\/code> Support for a Cross-Browser Solution<\/h3>\n\n\n\n<p>The lack of <code>filter()<\/code> support prevents us from blurring just the <code>background-image<\/code>. Consequently, a single <code>img<\/code> element is not enough for us to have two layers of the same image, an unaltered one on top and a blurred one at the bottom.<\/p>\n\n\n\n<p>Therefore, we need to wrap the <code>img<\/code> in a <code>.wrap<\/code> element and move the CSS variable holding the image URL onto this wrapper.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table\"><span class='shcb-loc'><span>- let src = 'my-image.jpg'\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><mark class='shcb-loc'><span>.wrap(style=`--img: url(${src})`)\n<\/span><\/mark><span class='shcb-loc'><span>  img(src=src alt='image description')\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><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>Moving on to the CSS, we first size the wrapper instead of the <code>img<\/code>, which we now force to take its parent&#8217;s <code>width<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-class\">.wrap<\/span> {\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">min<\/span>(<span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">23em<\/span>);\n<\/span><\/mark><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  img {\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n<\/span><\/mark><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">object-fit<\/span>: contain;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><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-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_made_cross_browser_outlines.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. The same 2\u00d72 grid with 4 square `img` elements, each containing a non-square image and, at the same time, tightly fit within a wrapper occupying one of the 4 grid cells. There seems to be a bit of an outline mismatch at the bottom of each image, as if there's a bit of extra space.\" class=\"wp-image-7902\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_made_cross_browser_outlines.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_made_cross_browser_outlines.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_made_cross_browser_outlines.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_made_cross_browser_outlines.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">square <code>img<\/code> elements with <code>object-fit: contain<\/code>, tightly fit within their wrapper<\/figcaption><\/figure>\n\n\n\n<p>Next, we set the wrapper\u2019s <code>background<\/code> to the CSS variable copy of the <code>src<\/code>. This <code>background<\/code> is sized using <code>cover<\/code> so it completely fills the square box of the <code>.wrap<\/code> without distortion.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">background<\/span>: <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--img<\/span>) 50% \/ <span class=\"hljs-selector-tag\">cover<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><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>Finally, the <code>img<\/code> gets a <code>backdrop-filter()<\/code> to blur the <code>background<\/code> of its wrapper set at the previous step.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">backdrop-filter<\/span>: <span class=\"hljs-selector-tag\">blur<\/span>(8<span class=\"hljs-selector-tag\">px<\/span>) <span class=\"hljs-selector-tag\">brightness<\/span>(<span class=\"hljs-selector-class\">.8<\/span>) <span class=\"hljs-selector-tag\">contrast<\/span>(<span class=\"hljs-selector-class\">.7<\/span>)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><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>We have just one problem: under each square image, there&#8217;s <a href=\"https:\/\/stackoverflow.com\/a\/7774854\/1397351\">a bit of unwanted space<\/a> that causes its wrapper to extend a few pixels further. And if we look carefully at the previous screenshot, we can see we had the same problem there too, it&#8217;s just more obvious now, especially when zooming in on it.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_tiny_problem.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. The same 2\u00d72 grid from before, zoomed in for a better look at the bottom of the top two images on it, a landscape and a portrait one specifically. There is a bit of space underneath them, which stretches their parent `div` down, so, while the image boxes still have an `aspect-ratio` of `1`, their wrappers (filled by a background image identical to the actual image source) are now a bit taller than they are wider.\" class=\"wp-image-7904\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_tiny_problem.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_tiny_problem.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_tiny_problem.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/base_tiny_problem.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">a close look at the problem<\/figcaption><\/figure>\n\n\n\n<p>The fix is to set a <code>display<\/code> property. It can be <code>block<\/code> on the <code>img<\/code> itself, or <code>grid<\/code> on its <code>.wrap<\/code> parent. I often prefer the <code>grid<\/code> approach nowadays for other benefits, but strictly when it comes to solving this particular problem at this particular point, it makes no difference which we use.<\/p>\n\n\n\n<p>Consequently, the final CSS for the base version of a cross\u2011browser blur extension is:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-class\">.wrap<\/span> {\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-attribute\">display<\/span>: grid;\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">min<\/span>(<span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">23em<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">var<\/span>(--img) <span class=\"hljs-number\">50%<\/span> \/ cover;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  img {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">object-fit<\/span>: contain;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">backdrop-filter<\/span>: \n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">8px<\/span>) <span class=\"hljs-built_in\">brightness<\/span>(<span class=\"hljs-number\">0.8<\/span>) <span class=\"hljs-built_in\">contrast<\/span>(<span class=\"hljs-number\">0.7<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><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<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ZYWEggJ\" src=\"\/\/codepen.io\/anon\/embed\/ZYWEggJ?height=680&amp;theme-id=1&amp;slug-hash=ZYWEggJ&amp;default-tab=result\" height=\"680\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ZYWEggJ\" title=\"CodePen Embed ZYWEggJ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-extra-fade-touch\">The Extra Fade Touch<\/h2>\n\n\n\n<p>The demo that inspired this deep dive also fades the image at the edges where it meets its blurred copy.<\/p>\n\n\n\n<p>In this case, the <code>img<\/code> element can&#8217;t be a square box with <code>object-fit: contain<\/code> for the actual image content. That would cause the edge fade to happen at the edges of the square <code>img<\/code> box, not at the edges of the actual non-square image contained within.<\/p>\n\n\n\n<p>However, when the <code>img<\/code> element isn&#8217;t square anymore, it doesn&#8217;t have those transparent bands on the sides of the actual image and doesn&#8217;t cover the entire area of its parent anymore. That means we cannot set the <code>backdrop-filter<\/code> on the <code>img<\/code> to blur its wrapper&#8217;s <code>background<\/code> anymore.<\/p>\n\n\n\n<p>The solution is to add an extra layer in between the <code>img<\/code> and its parent and set the <code>backdrop-filter<\/code> there. This can be the <code>::before<\/code> pseudo-element of the wrapper, which we stack in the same <code>grid-area<\/code> as the <code>img<\/code>.<\/p>\n\n\n\n<p>Doing so illustrates one of the advantages of a <code>grid<\/code> on the wrapper to prevent that extra space below the <code>img<\/code>: the use of <code>grid<\/code> <a href=\"https:\/\/bsky.app\/profile\/anatudor.bsky.social\/post\/3ley64k4zsc2q\">simplifies stacking<\/a> elements and pseudo\u2011elements.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.wrap<\/span> {\n  <span class=\"hljs-comment\">\/* same styles as before _for now_ *\/<\/span>\n\n  &amp;::before,\n  img {\n    <span class=\"hljs-attribute\">grid-area<\/span>: <span class=\"hljs-number\">1<\/span>\/ <span class=\"hljs-number\">1<\/span>;\n  }\n\n  &amp;<span class=\"hljs-selector-pseudo\">::before<\/span> {\n    <span class=\"hljs-attribute\">backdrop-filter<\/span>:\n      <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">8px<\/span>) <span class=\"hljs-built_in\">brightness<\/span>(<span class=\"hljs-number\">0.8<\/span>) <span class=\"hljs-built_in\">contrast<\/span>(<span class=\"hljs-number\">0.7<\/span>);\n    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\"<\/span>;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><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>Setting the <code>backdrop-filter<\/code> moves the <code>::before<\/code> pseudo-element on top of the <code>img<\/code> and applies the blur to it as well. So we need to we move the <code>img<\/code> back on top with a positive <code>z-index<\/code>. A value of <code>1<\/code> suffices, no need to forget the finger on the <code>9<\/code> key for eternity.<\/p>\n\n\n\n<p>The stacking order is not the only problem we need to solve after these changes to the code. While the wrapper must remain square, setting <code>aspect\u2011ratio<\/code> on it can\u2019t guarantee that anymore once we have a non-square <code>img<\/code> within. The image\u2019s intrinsic dimensions interfere with the wrapper\u2019s sizing, so we need a different approach to keep the wrapper square.<\/p>\n\n\n\n<p>That is, the following CSS fails to limit the <code>height<\/code> in the case of portrait images:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.wrap<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: grid;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">min<\/span>(<span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">23em<\/span>);\n  <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n\n  img {\n    <span class=\"hljs-attribute\">max-width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n    <span class=\"hljs-attribute\">max-height<\/span>: <span class=\"hljs-number\">100%<\/span>;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><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 screenshot below, where we&#8217;ve removed the wrapper <code>background<\/code> and the pseudo <code>backdrop-filter<\/code> for simplicity, illustrates this: a portrait image will stretch its wrapper vertically and, consequently, the grid cell the wrapper is contained in.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_an.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. A 2\u00d72 grid with a tentatively square box in each, each of these containing a non-square image, whose width is maxed out to `100%` of the parent box width. The height of an image is also maxed out to that of its square box parent in theory. In practice, this fails: the image `height` is not limited, it stretches its parent box and consequently, its parent's containing grid cell.\" class=\"wp-image-7905\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_an.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_an.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_an.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_an.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">when <code>aspect-ratio<\/code> fails<\/figcaption><\/figure>\n\n\n\n<p>We can fix this problem by explicitly setting both the <code>width<\/code> and the <code>height<\/code> of the <code>.wrap<\/code> to the same value. However, if we want to avoid that repetition, we can make the wrapper an inline <code>container<\/code> and set the <code>max-width<\/code> and <code>max-height<\/code> of the <code>img<\/code> to the wrapper&#8217;s <code>width<\/code>, now known as <code>100cqw<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-class\">.wrap<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">display<\/span>: grid;\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-attribute\">container-type<\/span>: inline-size;\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">min<\/span>(<span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">23em<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  img {\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">max-width<\/span>: <span class=\"hljs-number\">100<\/span>cqw; \n<\/span><\/mark><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">max-height<\/span>: <span class=\"hljs-number\">100<\/span>cqw; \n<\/span><\/mark><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><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-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_workaround_an.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. A 2\u00d72 square grid with a non-square image. The longer side of each image maxes out at the edge length of the square grid cell. The shorter side length is given by the intrinsic aspect ratio of each image. All images are start aligned along the axis of their shorter side.\" class=\"wp-image-7906\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_workaround_an.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_workaround_an.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_workaround_an.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_base_layout_issue_workaround_an.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">the container fix<\/figcaption><\/figure>\n\n\n\n<p>Next, we set <code>place-self: center<\/code> on the <code>img<\/code> to middle align it along the axis of its shorter side.<\/p>\n\n\n\n<p>Reintroducing the wrapper <code>background<\/code> and the <code>backdrop-filter<\/code> of the <code>::before<\/code> pseudo-element, the CSS we have so far looks as follows:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.wrap<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: grid;\n  <span class=\"hljs-attribute\">container-type<\/span>: inline-size;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">min<\/span>(<span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">23em<\/span>);\n  <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">var<\/span>(--img) <span class=\"hljs-number\">50%<\/span> \/ cover;\n\n  &amp;::before,\n  img {\n    <span class=\"hljs-attribute\">grid-area<\/span>: <span class=\"hljs-number\">1<\/span>\/ <span class=\"hljs-number\">1<\/span>;\n  }\n\n  &amp;<span class=\"hljs-selector-pseudo\">::before<\/span> {\n    <span class=\"hljs-attribute\">backdrop-filter<\/span>: <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">8px<\/span>) <span class=\"hljs-built_in\">brightness<\/span>(<span class=\"hljs-number\">0.8<\/span>) <span class=\"hljs-built_in\">contrast<\/span>(<span class=\"hljs-number\">0.7<\/span>);\n    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\"<\/span>;\n  }\n\n  <span class=\"hljs-selector-tag\">img<\/span> {\n    <span class=\"hljs-attribute\">place-self<\/span>: center;\n    <span class=\"hljs-attribute\">z-index<\/span>: <span class=\"hljs-number\">1<\/span>;\n    <span class=\"hljs-attribute\">max-width<\/span>: <span class=\"hljs-number\">100<\/span>cqw;\n    <span class=\"hljs-attribute\">max-height<\/span>: <span class=\"hljs-number\">100<\/span>cqw;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><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>At this point, it produces the same result as before, without the fade effect.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_before_fade.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. A 2\u00d72 square grid with a non-square image contained and centered within each of the 4 square cells. The non-square images are padded with a blurred version of themselves up the containing square edges along the axis of their shorter side.\" class=\"wp-image-7907\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_before_fade.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_before_fade.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_before_fade.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_before_fade.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">where we are: same visual result as before<\/figcaption><\/figure>\n\n\n\n<p>You might wonder why we still use <code>backdrop-filter<\/code> instead of setting the image <code>background<\/code> on the <code>::before<\/code> and blurring it, like the original demo does with a <code>div<\/code> sibling of the <code>img<\/code>. The problem with applying a CSS <code>blur()<\/code> on an element is the same one we previously encountered when applying it on an image background: the pixels close to the edges become semi\u2011transparent, which is especially noticeable against a patterned page backdrop. The original demo suffers from this as well.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_edge_issue_original_an.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot of the original demo, with a checkerboard backdrop behind the result - the non-square image with a blur padding to square. The square boundary is also highlighted, so we can see how we have semi-transparent pixels close to the edges within the square boundary.\" class=\"wp-image-7908\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_edge_issue_original_an.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_edge_issue_original_an.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_edge_issue_original_an.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_edge_issue_original_an.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">the problem with blurring elements, illustrated by the original demo<\/figcaption><\/figure>\n\n\n\n<p>Finally, we use a linear gradient <code>mask<\/code> to fade the edges of the image found within the square. The question is: how do we know along which axis this <code>linear-gradient()<\/code> needs to go?<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"known-aspect-ratio\">Known Aspect Ratio<\/h3>\n\n\n\n<p>The original demo gives portrait images a <code>.vertical<\/code> class and uses it to set a different <code>mask<\/code> gradient. This is an option, though I&#8217;d rather use a <code>data-orientation<\/code> attribute instead of a class and only set another direction of the <code>linear-gradient()<\/code> for portrait images instead of setting the entire <code>mask<\/code> again.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-attr\">&#91;data-orientation]<\/span> {\n  <span class=\"hljs-attribute\">--prc<\/span>: <span class=\"hljs-number\">20%<\/span>;\n  <span class=\"hljs-attribute\">mask<\/span>: <span class=\"hljs-built_in\">linear-gradient<\/span>(\n    var(--dir,) <span class=\"hljs-number\">#0000<\/span>,\n    red <span class=\"hljs-built_in\">var<\/span>(--prc) <span class=\"hljs-built_in\">calc<\/span>(<span class=\"hljs-number\">100%<\/span> - var(--prc)),\n    <span class=\"hljs-number\">#0000<\/span>\n  );\n}\n\n<span class=\"hljs-selector-attr\">&#91;data-orientation=<span class=\"hljs-string\">\"portrait\"<\/span>]<\/span> {\n  <span class=\"hljs-attribute\">--dir<\/span>: <span class=\"hljs-number\">90deg<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><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>You might wonder whether those CSS variable values are valid. They are, it&#8217;s not a case of typos sneaking into the code. Using an empty string as a value (or fallback) for a custom property is perfectly fine. A value that ends with a comma is also valid \u2014 the comma simply separates the direction (<code>90deg<\/code>) from the list of gradient stops.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_QwNwWOJ\/491f2da43ccfdc5f4d15bde2dae57f06\" src=\"\/\/codepen.io\/anon\/embed\/QwNwWOJ\/491f2da43ccfdc5f4d15bde2dae57f06?height=680&amp;theme-id=1&amp;slug-hash=QwNwWOJ\/491f2da43ccfdc5f4d15bde2dae57f06&amp;default-tab=result\" height=\"680\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed QwNwWOJ\/491f2da43ccfdc5f4d15bde2dae57f06\" title=\"CodePen Embed QwNwWOJ\/491f2da43ccfdc5f4d15bde2dae57f06\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Alternatively, we may be given an <code>img<\/code> aspect ratio, either as a CSS variable or as an attribute. Thanks to the updated <code>attr()<\/code> function, we can now use attribute values in calculations, though, as previously mentioned, this is not yet supported cross-browser.<\/p>\n\n\n\n<p>An aspect ratio is a <code>width\/height<\/code> ratio. If the first number, the width, is smaller (for example in the case of a <code>2\/3<\/code> aspect ratio), we have a portrait image. If the second number, the height, is smaller (for example in the case of a <code>3\/2<\/code> aspect ratio), we have a landscape image. You can see this illustrated by the interactive demo below:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_XWpyowX\" src=\"\/\/codepen.io\/anon\/embed\/XWpyowX?height=690&amp;theme-id=1&amp;slug-hash=XWpyowX&amp;default-tab=result\" height=\"690\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed XWpyowX\" title=\"CodePen Embed XWpyowX\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Coming back to our demo, we can determine whether our image is a portrait or landscape one by using the <code>sign()<\/code> function.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">--sgn<\/span>: <span class=\"hljs-selector-tag\">sign<\/span>(1 <span class=\"hljs-selector-tag\">-<\/span> <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--img-r<\/span>))<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><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>Subtracting the given aspect ratio from\u202f<code>1<\/code> gives us a negative number for landscape images (aspect ratio greater than\u202f<code>1<\/code>) and a positive number for portrait images (aspect ratio less than\u202f<code>1<\/code>). The <code>sign()<\/code> function therefore returns <code>-1<\/code> for landscape images and <code>1<\/code> for portrait ones.<\/p>\n\n\n\n<p>This allows us to calculate the <code>mask<\/code> gradient angle as:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">calc<\/span>((1 + <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--sgn<\/span>))*45<span class=\"hljs-selector-tag\">deg<\/span>)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><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>If the sign is <code>-1<\/code> (landscape), the expression inside the parentheses evaluates to\u202f<code>0<\/code>, giving us an angle of <code>0\u00b745\u00b0 = 0\u00b0<\/code>. If the sign is <code>1<\/code> (portrait), the expression inside the parentheses evaluates to\u202f<code>2<\/code>, giving us an angle of <code>2\u00b745\u00b0 = 90\u00b0<\/code>.<\/p>\n\n\n\n<p>So the CSS to fade the image edges along the needed axis based on its given aspect ratio looks as follows:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-tag\">img<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">--p<\/span>: <span class=\"hljs-number\">20%<\/span>;\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-attribute\">--sgn<\/span>: <span class=\"hljs-built_in\">sign<\/span>(<span class=\"hljs-number\">1<\/span> - var(--img-r));\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-comment\">\/* same as before *\/<\/span>\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-attribute\">mask<\/span>: <span class=\"hljs-built_in\">linear-gradient<\/span>(calc((<span class=\"hljs-number\">1<\/span> + var(--sgn)) * <span class=\"hljs-number\">45deg<\/span>),\n<\/span><\/mark><span class='shcb-loc'><span>    <span class=\"hljs-number\">#0000<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    red <span class=\"hljs-built_in\">var<\/span>(--p) <span class=\"hljs-built_in\">calc<\/span>(<span class=\"hljs-number\">100%<\/span> - var(--p)),\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-number\">#0000<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  );\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><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>Square images are a special case. Their aspect ratio is exactly\u202f <code>1<\/code>, so the sign of the difference evaluates to\u202f<code>0<\/code> and the gradient angle becomes\u202f<code>45\u00b0<\/code>. If we don\u2019t want a diagonal fade for square images, we can make the gradient\u2019s outer stops opaque instead of transparent.<\/p>\n\n\n\n<p>To do this, we introduce a binary flag:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">--bit<\/span>: <span class=\"hljs-selector-tag\">calc<\/span>(1 <span class=\"hljs-selector-tag\">-<\/span> <span class=\"hljs-selector-tag\">abs<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--sgn<\/span>)))<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><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>If the sign is <code>\u00b11<\/code> (not a square image), then its absolute value is <code>1<\/code> and subtracting that from <code>1<\/code> gives us <code>0<\/code> for our bit flag. If the sign is <code>0<\/code> (square image), then it&#8217;s still <code>0<\/code> in absolute value and subtracting that from <code>1<\/code> gives us <code>1<\/code> for our bit flag.<\/p>\n\n\n\n<p>Finally, we use this flag as the alpha channel of the <code>mask<\/code> gradient\u2019s end stops:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-25\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">--end<\/span>: <span class=\"hljs-selector-tag\">rgb<\/span>(0 0 0\/ <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--bit<\/span>))<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><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>So our final CSS for the fade effect when given the aspect ratio, including square image guardrails, looks as follows:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-tag\">img<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">--p<\/span>: <span class=\"hljs-number\">20%<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">--sgn<\/span>: <span class=\"hljs-built_in\">sign<\/span>(<span class=\"hljs-number\">1<\/span> - var(--img-r));\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-attribute\">--end<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span> \/ calc(<span class=\"hljs-number\">1<\/span> - abs(var(--sgn)))); \n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-comment\">\/* same as before *\/<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">mask<\/span>: \n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-built_in\">linear-gradient<\/span>(calc((<span class=\"hljs-number\">1<\/span> + var(--sgn))*<span class=\"hljs-number\">45deg<\/span>), \n<\/span><\/span><mark class='shcb-loc'><span>      <span class=\"hljs-built_in\">var<\/span>(--end),  \n<\/span><\/mark><span class='shcb-loc'><span>      red <span class=\"hljs-built_in\">var<\/span>(--p) <span class=\"hljs-built_in\">calc<\/span>(<span class=\"hljs-number\">100%<\/span> - var(--p)), \n<\/span><\/span><mark class='shcb-loc'><span>      <span class=\"hljs-built_in\">var<\/span>(--end))\n<\/span><\/mark><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><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<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_KwzwOJV\/2ddedd28e3c841f156876f12cb1f427d\" src=\"\/\/codepen.io\/anon\/embed\/KwzwOJV\/2ddedd28e3c841f156876f12cb1f427d?height=680&amp;theme-id=1&amp;slug-hash=KwzwOJV\/2ddedd28e3c841f156876f12cb1f427d&amp;default-tab=result\" height=\"680\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed KwzwOJV\/2ddedd28e3c841f156876f12cb1f427d\" title=\"CodePen Embed KwzwOJV\/2ddedd28e3c841f156876f12cb1f427d\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"unknown-aspect-ratio\">Unknown Aspect Ratio<\/h3>\n\n\n\n<p>I can think of two solutions when also having this constraint:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>an SVG&nbsp;<code>filter<\/code>&nbsp;one, using the result of a&nbsp;<code>feGaussianBlur<\/code>&nbsp;with&nbsp;<code>edgeMode<\/code>&nbsp;set to&nbsp;<code>duplicate<\/code>&nbsp;as a kind of mask for the image; it only works in Safari and even there, depending on the viewport size, it may behave oddly<\/li>\n\n\n\n<li>a pure CSS one, duplicating the&nbsp;<code>img<\/code>&nbsp;inside yet another&nbsp;<code>div<\/code>&nbsp;which we also make a&nbsp;<code>container<\/code>&nbsp;so we can compute the aspect ratio of the&nbsp;<code>img<\/code>&nbsp;within by dividing the&nbsp;<code>container<\/code>&nbsp;dimensions (<code>100cqw\/100cqh<\/code>); it can be made to work cross-browser, but requires two extra elements in addition to the previous&nbsp;<code>.wrap<\/code>&nbsp;around the original&nbsp;<code>img<\/code><\/li>\n<\/ul>\n\n\n\n<p>Neither solution is ideal, though I would prefer the SVG <code>filter<\/code> &#8220;mask&#8221; <em>if<\/em> it worked properly cross-browser. However, exploring both was an interesting exercise, so let&#8217;s take a quick look at each option.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"direction-agnostic-masking-via-an-svg-filter-safari-only\">Direction-Agnostic Masking via an SVG <code>filter<\/code> (Safari Only)<\/h4>\n\n\n\n<p>We\u2019re rolling back the changes we&#8217;ve made for the image fade so far: removing the <code>container<\/code> specific styles, restoring the square <code>aspect\u2011ratio<\/code> of our <code>img<\/code> and setting <code>object-fit<\/code> on it again. The <code>img<\/code> also receives a semi-transparent <code>background<\/code> with an alpha of <code>.5<\/code>; the actual RGB values are irrelevant.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-27\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.wrap<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: grid;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">min<\/span>(<span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">23em<\/span>);\n  <span class=\"hljs-comment\">\/* removed the blurred image copy on parent _for now_ *\/<\/span>\n\n  img {\n    <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n    <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n    <span class=\"hljs-attribute\">object-fit<\/span>: contain;\n    <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(var(--rgb) \/ <span class=\"hljs-number\">0.5<\/span>);\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><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>For now, we&#8217;ve also removed the parent <code>background<\/code> and the <code>::before<\/code> pseudo to make it easier to understand what&#8217;s going on. We&#8217;ll be adding them back later.<\/p>\n\n\n\n<p>We now have our <code>filter<\/code> input: a square <code>img<\/code> element with the fully opaque actual image contained in the middle and semi-transparent <code>background<\/code> bands around it&#8217;s shorter side filling out the square<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_base_setup_an.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot of what we have before applying the `filter`. A 2\u00d72 square grid containing 4 non-square images, centered along the axis of their shorter side. Each image is padded with a semi-transparent background to fill the containing square.\" class=\"wp-image-7909\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_base_setup_an.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_base_setup_an.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_base_setup_an.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_base_setup_an.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">our pre-filter setup<\/figcaption><\/figure>\n\n\n\n<p>Keep this in mind: the <code>filter<\/code> input is the square box of the <code>img<\/code> as highlighted above. This is why these screenshots highlight the square boundaries.<\/p>\n\n\n\n<p>The base setup for the <code>filter<\/code> is the same as before, we just set <code>primitiveUnits<\/code> to <code>objectBoundingBox<\/code> in addition to that. This makes all length values inside relative to the size of the <code>filter<\/code> input &#8211; in our case, that&#8217;s the edge length of the <code>img<\/code> square.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-28\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">svg(width='0' height='0' aria-hidden='true')\n  filter#fade(color-interpolation-filters='sRGB' primitiveUnits='objectBoundingBox')<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-28\"><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>First, we extract just the opaque image contained within the <code>img<\/code> square and save this <code>result<\/code> as <code>image<\/code>. We do this with a <a href=\"https:\/\/webplatform.github.io\/docs\/svg\/elements\/feComponentTransfer\/\"><code>feComponentTransfer<\/code><\/a> that zeroes the alpha of all input pixels with an alpha of <code>.5<\/code> &#8211; in our case, the semi-transparent <code>background<\/code> pixels on the sides of the image. We only need those to force our <code>filter<\/code> input into a square \u2014 we&#8217;ll see in a moment why that&#8217;s important. But they don&#8217;t matter otherwise, so we promptly discard them inside the <code>filter<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-29\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table\"><span class='shcb-loc'><span>svg(width='0' height='0' aria-hidden='true')\n<\/span><\/span><span class='shcb-loc'><span>  filter#fade(color-interpolation-filters='sRGB' primitiveUnits='objectBoundingBox')\n<\/span><\/span><mark class='shcb-loc'><span>    feComponentTransfer(result='image') \n<\/span><\/mark><mark class='shcb-loc'><span>      feFuncA(type='table' tableValues='0 0 1')\n<\/span><\/mark><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><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><code>feComponentTransfer<\/code> allows us to modify the channels of its input individually. We only need to modify the alpha here, so we use <code>feFuncA<\/code>.<\/p>\n\n\n\n<p>With 3 space-separated numbers for <code>tableValues<\/code>, the <code>[0, 1]<\/code> alpha interval is divided into <code>2 = 3 - 1<\/code> equal sub-intervals (<code>[0, .5)<\/code> and <code>[.5, 1]<\/code>). The three sub-interval ends (<code>0<\/code>, <code>.5<\/code> and <code>1<\/code>) are then mapped to the <code>tableValues<\/code>.<\/p>\n\n\n\n<p>The mapping for our <code>filter<\/code> is as follows:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>an input alpha of&nbsp;<code>0<\/code>&nbsp;is mapped to&nbsp;<code>0<\/code>; this means the empty transparent space around the&nbsp;<code>filter<\/code>&nbsp;input square, reserved so we don&#8217;t cut out things like box shadows remains transparent<\/li>\n\n\n\n<li>an input alpha of&nbsp;<code>.5<\/code>&nbsp;is mapped to&nbsp;<code>0<\/code>; this means the semi-transparent strips filling up the square on the sides of the image become fully transparent<\/li>\n\n\n\n<li>an input alpha of&nbsp;<code>1<\/code>&nbsp;is mapped to&nbsp;<code>1<\/code>; the fully opaque image in the middle of the input square remains fully opaque<\/li>\n<\/ul>\n\n\n\n<p>Consequently, all the alphas from the first sub-interval <code>[0, .5)<\/code> get mapped to <code>0<\/code>, while the second sub-interval <code>[.5, 1]<\/code> linearly maps to <code>[0, 1]<\/code>. For example, an alpha of <code>.25<\/code> is mapped to <code>0<\/code>, an alpha of <code>.6<\/code> is mapped to <code>.2<\/code>, an alpha of <code>.8<\/code> is mapped to <code>.6<\/code> and so on.<\/p>\n\n\n\n<p>Here, we only encounter the three endpoint alphas (<code>0<\/code>,\u202f<code>.5<\/code>,\u202f<code>1<\/code>), never any intermediate values. However, it&#8217;s still useful to understand how this mapping works, should other opacity levels ever arise.<\/p>\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\/11\/xtra_method_1_graph.png?resize=1024%2C576&#038;ssl=1\" alt=\"A graph of the mapping between the input alphas and the output alphas specified by a `tableValues` attribute set to `0 0 1`. Per this graph, for an input alpha of `0`, we get an output alpha of `0`; for an input alpha of `.5`, we get an output alpha of `0`; and for an input alpha of `1`, we get an output alpha of `1`. These three points are connected by two line segments corresponding to the mapings of the `[0, .5)` and `[.5, 1]` subintervals.\" class=\"wp-image-7910\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_graph.png?resize=1024%2C576&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_graph.png?resize=300%2C169&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_graph.png?resize=768%2C432&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_graph.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">graphing how this <code>feComponentTransfer<\/code> works<\/figcaption><\/figure>\n\n\n\n<p>Now that we have extracted just the actual image, we blur it. As we have <code>primitiveUnits<\/code> set to <code>objectBoundingBox<\/code>, the blur radius, set via the <code>stdDeviation<\/code> attribute, is relative to the edge length of the <code>filter<\/code> input square. Having the <code>edgeMode<\/code> of <code>duplicate<\/code> prevents pixels close to the edges from becoming semi-transparent.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-30\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table\"><span class='shcb-loc'><span>svg(width='0' height='0' aria-hidden='true')\n<\/span><\/span><span class='shcb-loc'><span>  filter#fade(color-interpolation-filters='sRGB' primitiveUnits='objectBoundingBox')\n<\/span><\/span><span class='shcb-loc'><span>    feComponentTransfer(result='image')\n<\/span><\/span><span class='shcb-loc'><span>      feFuncA(type='table' tableValues='0 0 1')\n<\/span><\/span><mark class='shcb-loc'><span>    feGaussianBlur(stdDeviation='.1' edgeMode='duplicate')\n<\/span><\/mark><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><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>The blur is why we need the <code>filter<\/code> input to be square and for more than one reason.<\/p>\n\n\n\n<p>First, when <code>primitiveUnits<\/code> is set to <code>objectBoundingBox<\/code> and the <code>filter<\/code> input is not square, browsers may compute length values differently, leading to inconsistent results. This matters less here, given setting <code>edgeMode<\/code> to <code>duplicate<\/code> only works in Safari, so getting consistent results cross-browser is already off the table.<\/p>\n\n\n\n<p>Second and most important is that <code>edgeMode<\/code> applies to the edges of the <code>filter<\/code> input, the square box in our case, <em>not<\/em> the edges of a shape within it, such as the edges of the rectangular image that don&#8217;t coincide with the edges of the containing square. Consequently, having <code>edgeMode<\/code> set to <code>duplicate<\/code> means the blur doesn&#8217;t affect the alpha of pixels if they&#8217;re close to the input square edge, but it does affect their alpha if they are close to the longer edges of the image inside the <code>filter<\/code> input square.<\/p>\n\n\n\n<p>Without <code>edgeMode='duplicate'<\/code>, this is the result we get:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_no_edge_mode_an.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot of what we have after applying the `filter`. A 2\u00d72 square grid containing 4 non-square images, each exactly fitting its containing square along the axis of its longer edges and centered along the axis of its shorter edge. These images are also blurred. We have semi-transparent pixels close to the original image boundaries along all four edges.\" class=\"wp-image-7912\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_no_edge_mode_an.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_no_edge_mode_an.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_no_edge_mode_an.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_no_edge_mode_an.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">result without <code>edgeMode='duplicate'<\/code><\/figcaption><\/figure>\n\n\n\n<p>With it, our result looks like below &#8211; it gives us an alpha fade of our image along the axis of its shorter side, but not along the axis of its longer side as well.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_with_edge_mode_an.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot of what we have after applying the `filter`. A 2\u00d72 square grid containing 4 non-square images, each exactly fitting its containing square along the axis of its longer edges and centered along the axis of its shorter edge. These images are also blurred. We now have semi-transparent pixels close to the original image boundaries only along the image edges that are within the square, but not for those that are on the square edges.\" class=\"wp-image-7913\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_with_edge_mode_an.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_with_edge_mode_an.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_with_edge_mode_an.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_blur_with_edge_mode_an.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">result with <code>edgeMode='duplicate'<\/code><\/figcaption><\/figure>\n\n\n\n<p>The blur result looks like something we could use as an alpha mask. The problem is the blur result extends beyond the original image boundary. That is, blurring produces semitransparent pixels on both sides of the original boundary, with those at the boundary having an alpha in the middle of the <code>[0, 1]<\/code> interval, far from fully transparent.<\/p>\n\n\n\n<p>Let&#8217;s say we were to use this blur result as an alpha mask for the previously extracted image.<\/p>\n\n\n\n<p>We do this via a <code>feComposite<\/code> with an <code>operator<\/code> value of <code>in<\/code>. The <code>in<\/code> operator works similarly to <a href=\"https:\/\/css-tricks.com\/mask-compositing-the-crash-course\/\"><code>mask<\/code> compositing<\/a> using <code>intersect<\/code>: it multiplies the alphas of its two inputs, <code>in<\/code> and <code>in2<\/code>, then uses the result together with the RGB channel of its first input <code>in<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-31\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table\"><span class='shcb-loc'><span>svg(width='0' height='0' aria-hidden='true')\n<\/span><\/span><span class='shcb-loc'><span>  filter#fade(color-interpolation-filters='sRGB' primitiveUnits='objectBoundingBox')\n<\/span><\/span><span class='shcb-loc'><span>    feComponentTransfer(result='image')\n<\/span><\/span><span class='shcb-loc'><span>      feFuncA(type='table' tableValues='0 0 1')\n<\/span><\/span><span class='shcb-loc'><span>    feGaussianBlur(stdDeviation='.1' edgeMode='duplicate')\n<\/span><\/span><mark class='shcb-loc'><span>    feComposite(in='image' operator='in')\n<\/span><\/mark><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-31\"><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 alpha of the image layer sharply goes from <code>1<\/code> inside to <code>0<\/code> outside when we cross the boundary, the result of multiplying it with the alpha of its blurred version sharply goes from the middle of the <code>[0, 1]<\/code> interval right before we cross the boundary to <code>0<\/code> as soon as we&#8217;ve crossed it. This means we get an awkward-looking cutoff instead of the image smoothly fading to transparency at the edges.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_cut_abruptly_an.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot of what happens when using the blur result as an alpha mask, illustrated via the same 2\u00d72 square grid containing a non-square image in each of the 4 cells. There is an abrupt cut at the longer image edges inside the square, Each image starts fading towards transparent towards those longer edges inside the square, but it's still from fall transparency when we get to the edges, so we get an abrupt cut from semi-transparent, but with a still high enough alpha, to fully transparent.\" class=\"wp-image-7915\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_cut_abruptly_an.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_cut_abruptly_an.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_cut_abruptly_an.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_cut_abruptly_an.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">not quite the edge fade we wanted<\/figcaption><\/figure>\n\n\n\n<p>So before the compositing step, we need to adjust the blur result alpha such that it goes to <code>0<\/code> at the longer edges of the image (found inside the square), where it&#8217;s currently in the middle of the <code>[0, 1]<\/code> interval. So we need to re-map the alpha range: any value up to <code>.5<\/code> is zeroed, while values in the <code>[.5, 1]<\/code> sub-interval are stretched linearly to the full <code>[0, 1]<\/code> range.<\/p>\n\n\n\n<p>We achieve this by inserting a second <code>feComponentTransfer<\/code> with a <code>feFuncA<\/code> whose <code>tableValues<\/code> attribute is set to <code>0 0 1<\/code> &#8211; just like before, this maps <code>0<\/code> to <code>0<\/code>, <code>.5<\/code> to <code>0<\/code> and <code>1<\/code> to <code>1<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-32\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table\"><span class='shcb-loc'><span>svg(width='0' height='0' aria-hidden='true')\n<\/span><\/span><span class='shcb-loc'><span>  filter#fade(color-interpolation-filters='sRGB' primitiveUnits='objectBoundingBox')\n<\/span><\/span><span class='shcb-loc'><span>    feComponentTransfer(result='image')\n<\/span><\/span><span class='shcb-loc'><span>      feFuncA(type='table' tableValues='0 0 1')\n<\/span><\/span><span class='shcb-loc'><span>    feGaussianBlur(stdDeviation='.1' edgeMode='duplicate')\n<\/span><\/span><mark class='shcb-loc'><span>    feComponentTransfer\n<\/span><\/mark><mark class='shcb-loc'><span>      feFuncA(type='table' tableValues='0 0 1')\n<\/span><\/mark><span class='shcb-loc'><span>    feComposite(in='image' operator='in')\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-32\"><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>The subsequent <code>feComposite<\/code> then gives us a nice fade effect along the axis of the shorter image edge: from fully transparent to fully opaque and back.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_fade_an.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot of what happens when using the blur result passed through the individual alpha mapping first as an alpha mask, illustrated via the same 2\u00d72 square grid containing a non-square image in each of the 4 cells. We don't have the abrupt cut at the longer image edges inside the square anymore, Each image fades smoothly towards transparent at the longer edges inside the containing square.\" class=\"wp-image-7916\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_fade_an.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_fade_an.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_fade_an.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_fade_an.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">just the masked image result<\/figcaption><\/figure>\n\n\n\n<p>A neat trick here is to use of out\u2011of\u2011range values in <code>tableValues<\/code> since output alphas are clamped to the <code>[0, 1]<\/code> interval anyway.<\/p>\n\n\n\n<p>Using <code>-1 1<\/code> creates a linear mapping that stretches the input interval <code>[0,\u202f1]<\/code> to <code>[-1,\u202f1]<\/code>. Clamping this output interval to <code>[0, 1]<\/code> results in the first half of it (corresponding to the input sub-interval <code>[0, .5)<\/code>) being zeroed and the second half remaining <code>[0, 1]<\/code>.<\/p>\n\n\n\n<p>The following graph shows the relationship:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"717\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_graph_2.png?resize=1024%2C717&#038;ssl=1\" alt=\"A comparative graph of the mappings between the input alphas and the output alphas specified by `tableValues` attributes set to`-1 1` and `0 0 1`. In the case of the first mapping, for an input alpha of `0`, we get an output alpha of `1` and for an input alpha of `1`, we get an output alpha of `1`. These two points are connected by a straight line. In the case of the second mapping, for an input alpha of `0`, we get an output alpha of `0`; for an input alpha of `.5`, we get an output alpha of `0`; and for an input alpha of `1`, we get an output alpha of `1`. These three points are connected by two line segments corresponding to the mapings of the `[0, .5)` and `[.5, 1]` subintervals. In practice, the output alphas get clamped to the `0, 1` interval so the result of the two mappings ends up being the same.\" class=\"wp-image-7917\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_graph_2.png?resize=1024%2C717&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_graph_2.png?resize=300%2C210&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_graph_2.png?resize=768%2C538&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_graph_2.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">graphing our <code>feComponentTransfer<\/code> options<\/figcaption><\/figure>\n\n\n\n<p>While the gain is modest here, saving a single character, the same technique can make a dramatic difference in other cases. For example, <code>-9 1<\/code> replaces <code>10<\/code> separate zeros followed by a <code>1<\/code>.<\/p>\n\n\n\n<p>The final step for this solution is to reintroduce the parent <code>background<\/code> blurred by the <code>backdrop-filter<\/code> of the <code>::before<\/code> pseudo.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_overlay_an.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. Shows the same 2\u00d72 square grid containing a non-square image in each of the 4 cells, each image being padded with a blurred version of itself up to the square edges. We also have an extra touch, the non-square images fade smoothly into their blurred copy padding on the sides.\" class=\"wp-image-7918\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_overlay_an.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_overlay_an.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_overlay_an.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_1_smooth_overlay_an.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">the final result (<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/NPNqdLM\/f6969d0428ca34f6812334de78a61fee\">live demo<\/a>, Safari only and may be buggy at certain viewport sizes)<\/figcaption><\/figure>\n\n\n\n<p>There are two key differences between this result and the <code>mask<\/code>\u2011based fade from the previous section.<\/p>\n\n\n\n<p>First, the blur radius and, therefore, the fading distance is relative to the square edge, not to the smaller image edge. This provides more consistency, but it can also erode narrow images too much, not leaving them any opaque part in the middle.<\/p>\n\n\n\n<p>Second, unlike a <code>linear-gradient()<\/code> mask, blurring produces a non\u2011linear fade. This feels more natural, but it is also more difficult to understand and, consequently, to control.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"getting-mask-angle-from-aspect-ratio-of-container-for-image-copy-cross-browser\">Getting mask angle from aspect ratio of container for image copy (cross-browser)<\/h4>\n\n\n\n<p>In this case, we need to make a couple of additions to our HTML:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-33\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table\"><span class='shcb-loc'><span>- let src = 'my-image.jpg'\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>.wrap(style=`--img: url(${src})`)\n<\/span><\/span><mark class='shcb-loc'><span>  img(src=src aria-hidden='true')\n<\/span><\/mark><mark class='shcb-loc'><span>  .rect\n<\/span><\/mark><mark class='shcb-loc'><span>    img(src=src alt='image description')\n<\/span><\/mark><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-33\"><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>The markup may look verbose, but every element serves a purpose. The outer <code>img<\/code> passes its relative intrinsic dimensions to its sibling <code>.rect<\/code> via the <code>grid<\/code> cell they&#8217;re both stacked in. That is, their containing <code>grid<\/code> cell is sized after the outer <code>img<\/code> and in turn sizes the <code>.rect<\/code>. The <code>.rect<\/code> is a container, which allows its inner <code>img<\/code> to know its dimensions as <code>100cqw<\/code> and <code>100cqh<\/code> in order to compute its aspect ratio. This is then used to compute the <code>mask<\/code> gradient angle just like before.<\/p>\n\n\n\n<p>Moving on to the CSS, several adjustments are needed here too.<\/p>\n\n\n\n<p>First, we make the wrapper a <code>container<\/code> again so its children can reference its width as <code>100cqw<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-34\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.wrap<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: grid;\n  <span class=\"hljs-attribute\">container-type<\/span>: inline-size;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">min<\/span>(<span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">23em<\/span>);\n  <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">var<\/span>(--img) <span class=\"hljs-number\">50%<\/span>\/ cover\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-34\"><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 contrast to the scenario when we knew the intrinsic aspect ratio of the image or at least its orientation, we don&#8217;t use <code>place-self: center<\/code> on the <code>img<\/code> to middle align it along the axis of its smallest side within the one cell of its parent&#8217;s <code>grid<\/code>. Instead, using <code>place-content: center<\/code> on the <code>.wrap<\/code>, we middle align its entire single-cell <code>grid<\/code>, which now takes the dimensions of the outer <code>img<\/code>, along the axis of the image&#8217;s shorter side.<\/p>\n\n\n\n<p>This is highlighted by the DevTools overlay in the screenshot below. The dotted purple line marks the boundary of the <code>.wrap<\/code> element, which we&#8217;ve made a <code>container<\/code>, while the solid orange line marks the boundary of its one cell <code>grid<\/code>, middle aligned within along the smaller axis of the image it contains.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_boxes.png?resize=1024%2C575&#038;ssl=1\" alt=\"Screenshot. Shows the same 2\u00d72 square grid containing a non-square image, centered along the axis of its shorter side in each of the 4 square cells. There are DevTools overlays highlighting the square boundary (the wrapper) and the rectangular image boundary.\" class=\"wp-image-7919\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_boxes.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_boxes.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_boxes.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_boxes.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">our boxes<\/figcaption><\/figure>\n\n\n\n<p>The screenshot also shows that the <code>::before<\/code> pseudo-element, which we use to blur the wrapper <code>background<\/code>, still needs to cover its entire parent in order to achieve the blur effect, breaking out of the middle aligned <code>grid<\/code> instead of staying confined to it.<\/p>\n\n\n\n<p>Our only option here is to absolutely position the <code>::before<\/code> pseudo-element and give it <code>inset: 0<\/code> to make it cover its entire parent.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-35\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-class\">.wrap<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">display<\/span>: grid;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">container-type<\/span>: inline-size;\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-attribute\">position<\/span>: relative;\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-built_in\">min<\/span>(<span class=\"hljs-number\">100%<\/span>, <span class=\"hljs-number\">23em<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">var<\/span>(--img) <span class=\"hljs-number\">50%<\/span> \/ cover;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  &amp;::before {\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">position<\/span>: absolute; \n<\/span><\/mark><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">inset<\/span>: <span class=\"hljs-number\">0<\/span>;\n<\/span><\/mark><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">backdrop-filter<\/span>: <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">8px<\/span>) <span class=\"hljs-built_in\">brightness<\/span>(<span class=\"hljs-number\">0.8<\/span>) <span class=\"hljs-built_in\">contrast<\/span>(<span class=\"hljs-number\">0.7<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  &gt; * {\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">grid-area<\/span>: <span class=\"hljs-number\">1<\/span>\/ <span class=\"hljs-number\">1<\/span>;\n<\/span><\/mark><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-35\"><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>With the <code>::before<\/code> pseudo\u2011element absolutely positioned, only the <code>.wrap<\/code> children remain in the single\u2011cell <code>grid<\/code>. Consequently, the <code>::before<\/code> no longer requires a <code>grid-area<\/code> declaration.<\/p>\n\n\n\n<p>We previously middle aligned the <code>grid<\/code> within its <code>.wrap<\/code> container. Now we&#8217;re looking at how the <code>grid<\/code> gets sized: it stretches to fit the first <code>img<\/code>, which is now hidden, both from from screen readers and visually.<\/p>\n\n\n\n<p>We&#8217;re hiding the first <code>img<\/code> from screen readers so it&#8217;s not announced twice. We&#8217;re also hiding it visually as we don&#8217;t want it to show underneath the masked second image. This can be done in multiple ways that don&#8217;t interfere with the layout, for example setting <code>opacity<\/code> or <code>scale<\/code> to <code>0<\/code>. Setting <code>visibility: hidden<\/code> or <code>z-index: -1<\/code> works as well.<\/p>\n\n\n\n<p>Both <code>img<\/code> elements have their <code>max-width<\/code> restricted to that of their nearest container, which they know as <code>100cqw<\/code>. However, only the first one, the direct child of the <code>.wrap<\/code>, also has its <code>max-height<\/code> restricted to <code>100cqw<\/code>. Both the <code>width<\/code> and the <code>height<\/code> of the first <code>img<\/code> remain <code>auto<\/code>, so it always keeps its intrinsic aspect ratio in addition to never overflowing its square container.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-36\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-class\">.wrap<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">display<\/span>: grid;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">container-type<\/span>: inline-size;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">place-content<\/span>: center;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">position<\/span>: relative;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">var<\/span>(--img) <span class=\"hljs-number\">50%<\/span> \/ cover;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  &amp;::before {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">position<\/span>: absolute;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">inset<\/span>: <span class=\"hljs-number\">0<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">backdrop-filter<\/span>: <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">8px<\/span>) <span class=\"hljs-built_in\">brightness<\/span>(<span class=\"hljs-number\">0.8<\/span>) <span class=\"hljs-built_in\">contrast<\/span>(<span class=\"hljs-number\">0.7<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  &gt; * {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">grid-area<\/span>: <span class=\"hljs-number\">1<\/span>\/ <span class=\"hljs-number\">1<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-selector-tag\">img<\/span> {\n<\/span><\/mark><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">max-width<\/span>: <span class=\"hljs-number\">100<\/span>cqw;\n<\/span><\/mark><mark class='shcb-loc'><span>  }\n<\/span><\/mark><mark class='shcb-loc'><span>\n<\/span><\/mark><mark class='shcb-loc'><span>  <span class=\"hljs-selector-attr\">&#91;aria-hidden=<span class=\"hljs-string\">\"true\"<\/span>]<\/span> {\n<\/span><\/mark><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">max-height<\/span>: <span class=\"hljs-number\">100<\/span>cqw;\n<\/span><\/mark><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n<\/span><\/mark><mark class='shcb-loc'><span>  }\n<\/span><\/mark><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-36\"><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 core logic behind is as follows.<\/p>\n\n\n\n<p>The size of the square <code>.wrap<\/code> determines the longer edge of the first <code>img<\/code>, the one we hide both visually and from screen readers. Since the <code>.wrap<\/code> is a <code>container<\/code>, the first <code>img<\/code> knows its edge length as <code>100cqw<\/code> and uses this to set both its <code>max-width<\/code> and <code>max-height<\/code>. Since the <code>width<\/code> and <code>height<\/code> are not set for this <code>img<\/code>, they remain <code>auto<\/code>. Consequently, while the longer side of the first <code>img<\/code> is always equal to the edge length of its square <code>container<\/code> parent (the <code>.wrap<\/code> element), the shorter side is determined by the intrinsic aspect ratio of the image.<\/p>\n\n\n\n<p>Having <code>place-content: center<\/code> on the <code>.wrap<\/code> not only middle aligns its <code>grid<\/code> along both axes, but, since the <code>grid<\/code> has no explicit column or row sizing, it shrinks to its non-shrinkable content. That is, its one cell adopts the dimensions of the first <code>img<\/code>.<\/p>\n\n\n\n<p>But why doesn&#8217;t the <code>.rect<\/code> play a part in sizing the <code>grid<\/code> cell too?<\/p>\n\n\n\n<p>In this case, where it holds the second image, it&#8217;s because we don&#8217;t let it do that by making it a <code>container<\/code>. This means its size computation ignores its content. This is why its descendants can use its <code>width<\/code> and <code>height<\/code> (as <code>100cqw<\/code> and <code>100cqh<\/code> respectively) to set their own dimensions without leading to an infinite loop.<\/p>\n\n\n\n<p>Since the <code>.rect<\/code> is a <code>grid<\/code> child and its size does not depend on its content, the default is for it to stretch to fill its <code>grid-are<\/code>a. In this case, this is the one cell of the <code>grid<\/code>, the one whose dimensions are given by the first <code>img<\/code>. So our <code>.rect<\/code> takes on the dimensions of its <code>img<\/code> sibling and, therefore, has the same aspect ratio.<\/p>\n\n\n\n<p>The <code>img<\/code> child of the <code>.rect<\/code> would normally overflow, but in this case, it has its <code>max-width<\/code> limited by the <code>width<\/code> of the <code>.rect<\/code> via <code>100cqw<\/code>. This means its <code>width<\/code> is the same as that of its <code>.rect<\/code> parent. Since both <code>img<\/code> elements have the same <code>src<\/code>, they share the same intrinsic aspect ratio. But we&#8217;ve determined the <code>.rect<\/code> has the same aspect ratio too, and putting that together with its <code>img<\/code> child having the same <code>width<\/code>, it results it has the same <code>height<\/code> too.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"575\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_computation_sequence.png?resize=1024%2C575&#038;ssl=1\" alt=\"DevTools screenshot, showing the HTML: 1, a wrapper .wrap that has a `grid` layout and is an inline `container`; 2, its first child, an `img` element with `aria-hidden` set to `true`; 3, the second wrapper child and the sibling of the first `img`, a rectangular `div` box called `.rect`, which is also a `container`; 4, an inner `img` that's a child of the `.rect`. The sizing computation sequence is as follows: square wrapper (1) edge length gives longer edge length of 1st `img` (2) as `100cqw`; longer edge length of 1st `img` (2) &amp; image's intrinsic aspect ratio give shorter edge length; rectangle `.rect` (3) is a `container` =&gt; content doesn't influence its size; stretches to fill grid cell whose size is given by 1st `img` (2) (occupying same `grid-area`); inner `img` (4) takes the dimensions of rectangle `.rect` parent (3); since parent (3) is a `container`, it knows its dimensions as `100cqw\u00d7100cqh`.\" class=\"wp-image-7920\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_computation_sequence.png?resize=1024%2C575&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_computation_sequence.png?resize=300%2C168&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_computation_sequence.png?resize=768%2C431&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/xtra_method_2_computation_sequence.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">the sizing computation sequence, illustrated on the HTML structure as seen in DevTools<\/figcaption><\/figure>\n\n\n\n<p>Consequently, the <code>.rect<\/code>, both <code>img<\/code> elements and the single-cell <code>grid<\/code> of the <code>.wrap<\/code> all occupy the same area on the page.<\/p>\n\n\n\n<p>This all serves one purpose: to compute the image intrinsic aspect ratio. Since the inner <code>img<\/code> knows the dimensions of its <code>.rect<\/code> parent and this has the same aspect ratio, the one we&#8217;re looking for, we can get our desired result by dividing the <code>.rect<\/code> dimensions, known by its child <code>img<\/code> as <code>100cqw<\/code> and <code>100cqh<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-37\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-class\">.wrap<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">display<\/span>: grid;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">container-type<\/span>: inline-size;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">place-content<\/span>: center;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">position<\/span>: relative;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">var<\/span>(--img) <span class=\"hljs-number\">50%<\/span> \/ cover;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  &amp;::before {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">position<\/span>: absolute;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">inset<\/span>: <span class=\"hljs-number\">0<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">backdrop-filter<\/span>: <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">8px<\/span>) <span class=\"hljs-built_in\">brightness<\/span>(<span class=\"hljs-number\">0.8<\/span>) <span class=\"hljs-built_in\">contrast<\/span>(<span class=\"hljs-number\">0.7<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  &gt; * {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">grid-area<\/span>: <span class=\"hljs-number\">1<\/span>\/ <span class=\"hljs-number\">1<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-selector-tag\">img<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">max-width<\/span>: <span class=\"hljs-number\">100<\/span>cqw;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-selector-attr\">&#91;aria-hidden=<span class=\"hljs-string\">\"true\"<\/span>]<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">max-height<\/span>: <span class=\"hljs-number\">100<\/span>cqw;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-selector-class\">.rect<\/span> {\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">container-type<\/span>: size;\n<\/span><\/mark><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-selector-attr\">&#91;alt]<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">--p<\/span>: <span class=\"hljs-number\">20%<\/span>;\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">--w<\/span>: <span class=\"hljs-number\">100<\/span>cqw;\n<\/span><\/mark><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">--h<\/span>: <span class=\"hljs-number\">100<\/span>cqh;\n<\/span><\/mark><mark class='shcb-loc'><span>    <span class=\"hljs-attribute\">--img-r<\/span>: <span class=\"hljs-built_in\">var<\/span>(--w) \/ <span class=\"hljs-built_in\">var<\/span>(--h);\n<\/span><\/mark><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">--sgn<\/span>: <span class=\"hljs-built_in\">sign<\/span>(<span class=\"hljs-number\">1<\/span> - var(--img-r));\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">--end<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span> \/ calc(<span class=\"hljs-number\">1<\/span> - abs(var(--sgn))));\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">mask<\/span>: <span class=\"hljs-built_in\">linear-gradient<\/span>(\n<\/span><\/span><span class='shcb-loc'><span>      calc((<span class=\"hljs-number\">1<\/span> + var(--sgn)) * <span class=\"hljs-number\">45deg<\/span>),\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-built_in\">var<\/span>(--end),\n<\/span><\/span><span class='shcb-loc'><span>      red <span class=\"hljs-built_in\">var<\/span>(--p) <span class=\"hljs-built_in\">calc<\/span>(<span class=\"hljs-number\">100%<\/span> - var(--p)),\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-built_in\">var<\/span>(--end)\n<\/span><\/span><span class='shcb-loc'><span>    );\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-37\"><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>This should work as such and it does in Safari (tested via Epiphany). However, Chrome seems to want either <code>--sgn<\/code> or <code>--img-r<\/code> to be registered in order for this solution to work. Firefox doesn&#8217;t yet support dividing lengths. However, we can <a href=\"https:\/\/frontendmasters.com\/blog\/count-auto-fill-columns\/#extending-support\">work around this limitation<\/a> using the <a href=\"https:\/\/dev.to\/janeori\/css-type-casting-to-numeric-tanatan2-scalars-582j\"><code>tan(atan2())<\/code> trick<\/a> we also employed in the case of computing the number of <code>auto-fit<\/code> columns.<\/p>\n\n\n\n<p>So, at the end of the day, we can still have a cross-browser solution.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bNpdZVq\/adf3e41d46e68268b67cdc9be8db04f8\" src=\"\/\/codepen.io\/anon\/embed\/bNpdZVq\/adf3e41d46e68268b67cdc9be8db04f8?height=690&amp;theme-id=1&amp;slug-hash=bNpdZVq\/adf3e41d46e68268b67cdc9be8db04f8&amp;default-tab=result\" height=\"690\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bNpdZVq\/adf3e41d46e68268b67cdc9be8db04f8\" title=\"CodePen Embed bNpdZVq\/adf3e41d46e68268b67cdc9be8db04f8\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"taking-it-further\">Taking it Further<\/h2>\n\n\n\n<p>We don&#8217;t have to stop here. Our <code>backdrop-filter<\/code> also chains <code>brightness()<\/code> and <code>contrast()<\/code> after the <code>blur()<\/code>, but we could have other filters there on top of these or instead of these, for example <code>sepia()<\/code> or <code>hue-rotate()<\/code> or even an SVG filter. The possibilities are endless, so&#8230; what would you experiment with starting from here?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I recently came across this CodePen demo by Vivi Tseng, which creates the blur extension effect by placing a square div with a blur() beneath the img element and I couldn&#8217;t help but think a simpler solution should be possible with a single img element and minimal CSS. So let&#8217;s first take a look at [&hellip;]<\/p>\n","protected":false},"author":32,"featured_media":7932,"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":[],"class_list":["post-7855","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/blur-out.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7855","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=7855"}],"version-history":[{"count":29,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7855\/revisions"}],"predecessor-version":[{"id":7966,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7855\/revisions\/7966"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/7932"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=7855"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=7855"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=7855"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}