{"id":7213,"date":"2025-10-01T10:59:05","date_gmt":"2025-10-01T15:59:05","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=7213"},"modified":"2025-10-01T11:05:08","modified_gmt":"2025-10-01T16:05:08","slug":"inset-shadows-directly-on-img-elements-part-1","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/inset-shadows-directly-on-img-elements-part-1\/","title":{"rendered":"Inset Shadows Directly on img Elements (Part 1)"},"content":{"rendered":"\n<p>You might think the job of putting an inset shadow on an <code>&lt;img&gt;<\/code> is trivial: just set a <code>box-shadow<\/code> like <code>inset 0 1px 3px<\/code> and that&#8217;s it! <\/p>\n\n\n\n<p>You&#8217;d be wrong. <\/p>\n\n\n\n<p>This doesn&#8217;t work because the actual image is <em>content<\/em> for the <code>img<\/code> element. And content is painted <em>on top<\/em> of <code>box-shadow<\/code>.<\/p>\n\n\n\n<p>This problem is something that has been the topic of countless questions on <a href=\"https:\/\/stackoverflow.com\/search?q=inset+shadow+img\">Stack Overflow<\/a> as well as <a href=\"https:\/\/www.reddit.com\/r\/css\/comments\/12dm8eh\/how_to_set_inner_box_shadow_or_lineargradient_on\/\">Reddit<\/a> and <a href=\"https:\/\/exchangetuts.com\/index.php\/putting-a-inset-box-shadow-on-an-image-or-image-within-a-div-1640061124745310\">other places<\/a> on the internet. It <a href=\"https:\/\/trentwalton.com\/2010\/11\/22\/css-box-shadowinset\/\">has<\/a> <a href=\"https:\/\/designdebt.club\/inner-shadows-on-image-elements\/\">also<\/a> <a href=\"https:\/\/designshack.net\/articles\/css\/inner-shadows-in-css-images-text-and-beyond\/\">been<\/a> <a href=\"https:\/\/paulchr.ablass.me\/demo\/box-shadow-inset-img\/\">covered<\/a> <a href=\"https:\/\/habr.com\/ru\/articles\/154211\/\">in many<\/a> <a href=\"https:\/\/bavotasan.com\/2011\/adding-inset-shadow-to-image-css3\/\">articles<\/a> over the past 15 years. Now in 2025, it still made the list of pain points when dealing with <a href=\"https:\/\/2025.stateofcss.com\/en-US\/features\/#shapes_graphics_pain_points\">shapes &amp; graphics<\/a> according to the State of CSS survey.<\/p>\n\n\n\n<p>So why yet another article? Well, almost all the solutions I&#8217;ve seen so far involve at least another element stacked on top of the <code>img<\/code> (assuming they don&#8217;t straight up replace the <code>img<\/code> with a <code>div<\/code>), so that we can have a &#8220;cover&#8221; with the exact dimensions on top &#8211; this is the (pseudo)element that actually gets the <code>inset<\/code> shadow. Beyond using at the very least an extra pseudo-element for each image, this can be annoying for users, as the right click <code>img<\/code> menu is lost unless the &#8220;cover&#8221; gets <code>pointer-events: none<\/code>.<\/p>\n\n\n\n<p>I want to show you a solution that allows us to add the shadow directly on&nbsp;<code>&lt;img&gt;<\/code> elements without requiring an extra wrapper or sibling for each.<\/p>\n\n\n\n<p>This article is going to have two parts, the first (current one) going into a lot of detail about the how behind creating the basic inset black shadow with offsets, blur and spread radii and the second being a deep dive into pain points like painting the shadow and limitations tied to length values.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"base-setup\">Base setup<\/h2>\n\n\n\n<p>We have just 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<p>And a simple SVG <code>filter<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'si'<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><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>Wait, don&#8217;t run away screaming! <\/p>\n\n\n\n<p>I promise that, while SVG filters may seem scary and this technique has some limitations and quirks, it&#8217;s still easy to digest when going through it step by step, each step having interactive demos to help with understanding how things work in the back. By the end of it, you&#8217;ll have a bunch of cool new tricks to add to your web dev bag.<\/p>\n\n\n\n<p>So let&#8217;s get started!<\/p>\n\n\n\n<p>First off, our SVG <code>filter<\/code> needs to be inside an <code>svg<\/code> element. Since this element only exists to contain our <code>filter<\/code>, it is not used to display any graphics, it is functionally the same as a <code>style<\/code> element. So we zero its dimensions, hide it from screen readers and take it out of the document flow from the CSS:<\/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\">svg<\/span><span class=\"hljs-selector-attr\">&#91;height=<span class=\"hljs-string\">'0'<\/span>]<\/span><span class=\"hljs-selector-attr\">&#91;aria-hidden=<span class=\"hljs-string\">'true'<\/span>]<\/span> { <span class=\"hljs-attribute\">position<\/span>: fixed }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-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>We then apply our <code>filter<\/code> on the <code>img<\/code> element:<\/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\">img<\/span> { <span class=\"hljs-attribute\">filter<\/span>: <span class=\"hljs-built_in\">url<\/span>(#si) }<\/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>Note that the filter as it is at this point causes the <code>img<\/code> to disappear in Firefox, even as it leaves it unchanged in Chrome. And, according to <a href=\"https:\/\/www.w3.org\/TR\/filter-effects\/#FilterPrimitiveTree\">the spec<\/a>, an empty <code>filter<\/code> element means the element the <code>filter<\/code> applies to does not get rendered. So Firefox is following the spec here, even if the Chrome result is what I would have expected: an empty <code>filter<\/code> being equivalent to no <code>filter<\/code> applied.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-base-filter-content\">The base <code>filter<\/code> content<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"offset-the-alpha-map\">Offset the alpha map<\/h3>\n\n\n\n<p>We start off by offsetting the alpha map of the <code>filter<\/code> input, the <code>filter<\/code> input being our <code>img<\/code> in this case. The alpha map is basically the <code>filter<\/code> input where every single pixel has its RGB channels zeroed and its alpha channel preserved.<\/p>\n\n\n\n<p>Since here the <code>filter<\/code> input is a plain rectangular, fully opaque image, the alpha map (referenced within the SVG <code>filter<\/code> as <code>SourceAlpha<\/code>) is a fully opaque black rectangle within the boundary of our initial image, while everything around it is fully transparent. Note that if the <code>img<\/code> has a <code>border-radius<\/code> (with any kind of corner-shape), then the alpha map is going to respect that too.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" 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><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'si'<\/span>&gt;<\/span>\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feOffset<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">dx<\/span>=<span class=\"hljs-string\">'9'<\/span> <span class=\"hljs-attr\">dy<\/span>=<span class=\"hljs-string\">'13'<\/span>\/&gt;<\/span>\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><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>These <code>fe<\/code>-prefixed elements inside our <code>filter<\/code> (&#8220;fe&#8221; stands for &#8220;filter effect&#8221;) are called <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/SVG\/Reference\/Element#filter_primitive_elements\">filter primitives<\/a>. They may have zero, one, or two inputs. Primitives with zero inputs create a layer based on their other attributes (for example, <code>feTurbulence<\/code> can give us a noise layer based on a <code>baseFrequency<\/code> attribute). Primitives with one input (like <code>feOffset<\/code> here) modify that input. And finally, primitives with two inputs combine them into one result (for example, <code>feBlend<\/code> blends its two inputs using the blend mode given by its <code>mode<\/code> attribute).<\/p>\n\n\n\n<p>All of those needed for the base <code>filter<\/code> creating a simple inset black shadow have either one or two, though when we get to painting the shadow and other effects, we may need to use some with no inputs.<\/p>\n\n\n\n<p>For most of those with a single input, we don&#8217;t specify that input explicitly (by setting the <code>in<\/code> attribute) because we&#8217;re using the defaults! Filter primitive inputs are by default the result of the previous primitive or, in the case of the very first primitive, the <code>filter<\/code> input (referenced within the SVG <code>filter<\/code> as <code>SourceGraphic<\/code>).<\/p>\n\n\n\n<p><code>feOffset<\/code> in particular offsets its input along the <em>x<\/em> and\/or <em>y<\/em> axis. In our particular case, it offsets its input by <code>9px<\/code> along the <em>x<\/em> axis and by <code>13px<\/code> along the <em>y<\/em> axis.<\/p>\n\n\n\n<p>The following interactive demo illustrates how this primitive works and allows changing the <code>feOffset<\/code> attributes to see how that affects the visual result.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_RNWLGQM\/1fae2c8385119444695aae2eb8f1c05e\" src=\"\/\/codepen.io\/anon\/embed\/RNWLGQM\/1fae2c8385119444695aae2eb8f1c05e?height=660&amp;theme-id=1&amp;slug-hash=RNWLGQM\/1fae2c8385119444695aae2eb8f1c05e&amp;default-tab=result\" height=\"660\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed RNWLGQM\/1fae2c8385119444695aae2eb8f1c05e\" title=\"CodePen Embed RNWLGQM\/1fae2c8385119444695aae2eb8f1c05e\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Note that the <code>in<\/code> attribute and the offset ones (<code>dx<\/code> and <code>dy<\/code>) are greyed and crossed out when set to <code>SourceGraphic<\/code> and <code>0<\/code> respectively. It&#8217;s because these are the default values and if they are the values we want for them, then we don&#8217;t need to set them at all.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"blur-the-offset-map\">Blur the offset map<\/h3>\n\n\n\n<p>Next, we blur this offset result.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" 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><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'si'<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feOffset<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">dx<\/span>=<span class=\"hljs-string\">'9'<\/span> <span class=\"hljs-attr\">dy<\/span>=<span class=\"hljs-string\">'13'<\/span>\/&gt;<\/span>\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feGaussianBlur<\/span> <span class=\"hljs-attr\">stdDeviation<\/span>=<span class=\"hljs-string\">'5'<\/span>\/&gt;<\/span>\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><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>Adding this second primitive is equivalent to chaining <code>blur(5px)<\/code> after the <code>filter<\/code> we had at the previous step (with only the <code>feOffset<\/code> primitive).<\/p>\n\n\n\n<p>Note that this blur radius (and any SVG blur radius in general, whether it&#8217;s a <code>stdDeviation<\/code> attribute of an SVG <code>filter<\/code> primitive or a blur radius used by CSS equivalents like the <code>blur()<\/code> or <code>drop-shadow()<\/code> functions) needs to be half the one we&#8217;d use for a <code>box-shadow<\/code> <a href=\"https:\/\/codepen.io\/thebabydino\/pen\/WNPwzGg\">if we want the same result<\/a>. You can check out <a href=\"https:\/\/dbaron.org\/log\/20110225-blur-radius\">this article<\/a> by David Baron for a detailed explanation of the why behind.<\/p>\n\n\n\n<p>The interactive demo below lets us play with the <code>filter<\/code> we have so far (all primitive attributes can be changed) in order to get a better feel for how it works.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_PwPOLJZ\/983bfcbe8e69e72b24677143254dc223\" src=\"\/\/codepen.io\/anon\/embed\/PwPOLJZ\/983bfcbe8e69e72b24677143254dc223?height=680&amp;theme-id=1&amp;slug-hash=PwPOLJZ\/983bfcbe8e69e72b24677143254dc223&amp;default-tab=result\" height=\"680\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed PwPOLJZ\/983bfcbe8e69e72b24677143254dc223\" title=\"CodePen Embed PwPOLJZ\/983bfcbe8e69e72b24677143254dc223\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Note that these first two primitives can be in any order (we get the exact same result if we apply the offset <em>after<\/em> the blur). However, this is generally not the case \u2014 in most cases, the order of the primitives <em>does<\/em> matter.<\/p>\n\n\n\n<p>Also note that in some scenarios (for example if we increase the blur radius to the maximum allowed by the demo), the blur seems cut off from a certain point outside the input element&#8217;s boundary. This cutoff is where the <a href=\"https:\/\/drafts.fxtf.org\/filter-effects\/#FilterEffectsRegion\"><code>filter<\/code> region<\/a> ends.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1001\" height=\"964\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/cutoff.png?resize=1001%2C964&#038;ssl=1\" alt=\"Screenshot of the previous interactive demo where the interactive code panel can control the the visual result, in the case when the stdDeviation value of the feGaussianBlur primitive is bumped up to 32. In this situation, the blur of the black rectangle doesn't fade to full transparency. Instead, it gets abruptly cut off not far outside the initial boundary of the filter input image.\" class=\"wp-image-7286\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/cutoff.png?w=1001&amp;ssl=1 1001w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/cutoff.png?resize=300%2C289&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/cutoff.png?resize=768%2C740&amp;ssl=1 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>By default, the <code>filter<\/code> region extends <code>10%<\/code> of the <code>filter<\/code> input&#8217;s <a href=\"https:\/\/svgwg.org\/svg2-draft\/coords.html#TermObjectBoundingBox\">bounding box<\/a> size in every direction. In the case of a rectangular image, the bounding box is the image rectangle, the one whose boundary is marked by a dashed line in the interactive demos above.<\/p>\n\n\n\n<p>We can change this region by changing the <code>x<\/code>, <code>y<\/code>, <code>width<\/code> and <code>height<\/code> attributes of the <code>filter<\/code> element. By default, these are given relative to the width and height of the <code>filter<\/code> input&#8217;s bounding box, using either a percentage or decimal representation. We could change the value of the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/SVG\/Reference\/Attribute\/filterUnits\"><code>filterUnits<\/code><\/a> attribute to make them fixed pixel values, but I don&#8217;t think that&#8217;s something I&#8217;ve ever wanted to do and the default of them being relative to the <code>filter<\/code> input&#8217;s bounding box is what we want here, too.<\/p>\n\n\n\n<p>For example, <code>x='-.25'<\/code>and <code>x='-25%'<\/code> are both valid and produce the same result. In this case, the <code>filter<\/code> region starts from <code>25%<\/code> of the input bounding box width to the left (negative direction) of the left edge of this bounding box. The interactive demo below allows toying with the <code>filter<\/code> region too.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_MYaLNyQ\/c550fc4265c140451ce6a8128eb06c5c\" src=\"\/\/codepen.io\/anon\/embed\/MYaLNyQ\/c550fc4265c140451ce6a8128eb06c5c?height=670&amp;theme-id=1&amp;slug-hash=MYaLNyQ\/c550fc4265c140451ce6a8128eb06c5c&amp;default-tab=result\" height=\"670\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed MYaLNyQ\/c550fc4265c140451ce6a8128eb06c5c\" title=\"CodePen Embed MYaLNyQ\/c550fc4265c140451ce6a8128eb06c5c\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>However, since our desired effect, the basic inset shadow, is limited to the area of the <code>filter<\/code> input (that is, the area of the original image), we don&#8217;t care if anything outside it gets cut off by the <code>filter<\/code> region limit, so we won&#8217;t be touching these <code>filter<\/code> attributes. At least for now, as long as we&#8217;re talking just about the base inset shadow.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"subtract-offset--blurred-version-from-initial-one\">Subtract offset &amp; blurred version from initial one<\/h3>\n\n\n\n<p>The next step is to subtract the alpha of this offset and blurred result from the original alpha map (<code>SourceAlpha<\/code>) with no offset or blur applied:<\/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 shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'si'<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feOffset<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">dx<\/span>=<span class=\"hljs-string\">'9'<\/span> <span class=\"hljs-attr\">dy<\/span>=<span class=\"hljs-string\">'13'<\/span>\/&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feGaussianBlur<\/span> <span class=\"hljs-attr\">stdDeviation<\/span>=<span class=\"hljs-string\">'5'<\/span>\/&gt;<\/span>\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feComposite<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">operator<\/span>=<span class=\"hljs-string\">'out'<\/span>\/&gt;<\/span>\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span>\n<\/span><\/span><\/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><code>feComposite<\/code> is a primitive with two inputs (<code>in<\/code> and <code>in2<\/code>, both defaulting to the result of the previous primitive). When we use <code>feComposite<\/code> with the <code>operator<\/code> attribute set to <code>out<\/code>, we subtract the second input (<code>in2<\/code>, not set explicitly here as we want it to be the result of the previous primitive) out of the first one (<code>in<\/code>).<\/p>\n\n\n\n<p>This isn&#8217;t plain clamped subtraction. Instead, it&#8217;s similar to what <code>subtract<\/code> (<code>source-out<\/code> in the ancient, non-standard WebKit version) <a href=\"https:\/\/css-tricks.com\/mask-compositing-the-crash-course\/#aa-subtract\">does when compositing alpha <code>mask<\/code> layers<\/a>: the alpha of the first input <code>in<\/code> (<code>\u03b1<\/code>) is multiplied with the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Method_of_complements#Binary_method\">complement<\/a> of the second input <code>in2<\/code> alpha (<code>\u03b1\u2082<\/code>).<\/p>\n\n\n\n<p>This means that for every pair of corresponding pixels from the two inputs, the RGB channels of the result are those of the pixel from the first input (<code>in<\/code>), while the alpha of the result is given by the following formula:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\u03b1\u00b7(1 \u2013 \u03b1\u2082)<\/pre>\n\n\n\n<p>Where <code>\u03b1<\/code> is the alpha of the pixel from the first input <code>in<\/code>, and <code>\u03b1\u2082<\/code> is the alpha of the corresponding pixel from the second input <code>in2<\/code>, the input we subtract out of the first to get a black inset shadow.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_VYvrRgY\/4d0141f889c09c0b953f4f8794a13ca9\" src=\"\/\/codepen.io\/anon\/embed\/VYvrRgY\/4d0141f889c09c0b953f4f8794a13ca9?height=670&amp;theme-id=1&amp;slug-hash=VYvrRgY\/4d0141f889c09c0b953f4f8794a13ca9&amp;default-tab=result\" height=\"670\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed VYvrRgY\/4d0141f889c09c0b953f4f8794a13ca9\" title=\"CodePen Embed VYvrRgY\/4d0141f889c09c0b953f4f8794a13ca9\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Note that this latest interactive demo disables the option to switch between <code>SourceAlpha<\/code> and <code>SourceGraphic<\/code> inputs for the <code>feOffset<\/code> primitive. This is due to <a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1957693\">a Firefox bug<\/a> which we might hit in certain situations and which makes the result of the <code>feComposite<\/code> simply disappear if <code>feOffset<\/code> uses the default <code>SourceGraphic<\/code> input.<\/p>\n\n\n\n<p>Switching the <code>operator<\/code> also isn&#8217;t enabled here, as it would mean just too much to unpack and most is outside the scope of this article anyway. Just know that some of the <code>operator<\/code> values work exactly the same as their CSS <code>mask-composite<\/code> equivalents.<\/p>\n\n\n\n<p>For example, <code>over<\/code> is equivalent to <code>add<\/code> (<code>source-over<\/code> in the ancient, non-standard WebKit version), subtracting the alpha product from their sum (<code>\u03b1 + \u03b1\u2082 - \u03b1\u00b7\u03b1\u2082<\/code>).<\/p>\n\n\n\n<p>Then <code>in<\/code> is equivalent to <code>intersect<\/code> (<code>source-in<\/code>), multiplying the alphas of the two inputs (<code>\u03b1\u00b7\u03b1\u2082<\/code>).<\/p>\n\n\n\n<p>And <code>xor<\/code> is equivalent to <code>exclude<\/code>, where we add up the result of each of the two inputs being subtracted from the other (<code>\u03b1\u00b7(1 \u2013 \u03b1\u2082) + \u03b1\u2082\u00b7(1 \u2013 \u03b1)<\/code>).<\/p>\n\n\n\n<p>For more details and visual examples illustrating how these operators work, you can check out <a href=\"https:\/\/kimikage.github.io\/ColorBlendModes.jl\/stable\/composite-operations\/\">this page<\/a> (note that all <code>operator<\/code> values used for <code>feComposite<\/code> are <code>source-*<\/code> ones, for the effect given by the <code>destination-*<\/code> ones, we need to reverse the two inputs).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"place-the-initial-image-underneath\">Place the initial image underneath<\/h3>\n\n\n\n<p>Now that we have the shadow, all we still need to do is place the <code>filter<\/code> input (the image in our case) underneath it. I&#8217;ve often seen this done with <code>feMerge<\/code> or <code>feComposite<\/code>. I personally prefer to do it with <code>feBlend<\/code> as this primitive with the default <code>mode<\/code> of <code>normal<\/code> produces the exact same result as the other two. Plus, other modes may offer us even more visually interesting results.<\/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 shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'si'<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feOffset<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">dx<\/span>=<span class=\"hljs-string\">'9'<\/span> <span class=\"hljs-attr\">dy<\/span>=<span class=\"hljs-string\">'13'<\/span>\/&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feGaussianBlur<\/span> <span class=\"hljs-attr\">stdDeviation<\/span>=<span class=\"hljs-string\">'5'<\/span>\/&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feComposite<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">operator<\/span>=<span class=\"hljs-string\">'out'<\/span>\/&gt;<\/span>\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feBlend<\/span> <span class=\"hljs-attr\">in2<\/span>=<span class=\"hljs-string\">'SourceGraphic'<\/span>\/&gt;<\/span>\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span>\n<\/span><\/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>Just like <code>feComposite<\/code>, <code>feBlend<\/code> takes two inputs. <code>in<\/code> is the one on top and we don&#8217;t need to set it explicitly here, as it defaults to the result of the previous primitive, the inset shadow in our case. This is exactly the layer we want to have on top here. <code>in2<\/code> is the one at the bottom and we set it to the <code>filter<\/code> input (<code>SourceGraphic<\/code>), which is the image in our case.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_YPyEbyY\/49240dd20c0268eda81899bc8b7273e6\" src=\"\/\/codepen.io\/anon\/embed\/YPyEbyY\/49240dd20c0268eda81899bc8b7273e6?height=680&amp;theme-id=1&amp;slug-hash=YPyEbyY\/49240dd20c0268eda81899bc8b7273e6&amp;default-tab=result\" height=\"680\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed YPyEbyY\/49240dd20c0268eda81899bc8b7273e6\" title=\"CodePen Embed YPyEbyY\/49240dd20c0268eda81899bc8b7273e6\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"a-base-case-example\">A base case example<\/h3>\n\n\n\n<p>This is exactly the technique we used to create the inner shadows on these squircle-shaped images.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"354\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/477984912-9b03119c-5bf8-4856-9558-14927ff2b8fc.png?resize=1024%2C354&#038;ssl=1\" alt=\"A grid of squircle-shaped images with inner shadows.\" class=\"wp-image-7229\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/477984912-9b03119c-5bf8-4856-9558-14927ff2b8fc.png?resize=1024%2C354&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/477984912-9b03119c-5bf8-4856-9558-14927ff2b8fc.png?resize=300%2C104&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/477984912-9b03119c-5bf8-4856-9558-14927ff2b8fc.png?resize=768%2C265&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/477984912-9b03119c-5bf8-4856-9558-14927ff2b8fc.png?resize=1536%2C531&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/477984912-9b03119c-5bf8-4856-9558-14927ff2b8fc.png?w=1566&amp;ssl=1 1566w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">squircle-shaped images with inset shadows (<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/MYgNgBa\">live demo<\/a>)<\/figcaption><\/figure>\n\n\n\n<p>Note that the squircle shape seems to be incorrect in Safari (tested via Epiphany on Ubuntu), but the relevant part (the inset shadow) seems to work well everywhere. Also, nowadays, this is not the simplest way to create squircle shapes anymore with <a href=\"https:\/\/frontendmasters.com\/blog\/drawing-css-shapes-using-corner-shape\/\">the <code>corner-shape<\/code> property<\/a> as well as <a href=\"https:\/\/frontendmasters.com\/blog\/shape-a-new-powerful-drawing-syntax-in-css\/\">the <code>shape()<\/code> function<\/a> making their way into browsers, but it&#8217;s still a way to do it and, leaving aside bugs like the incorrect squircle Safari issue, a better supported one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"spread-it-out-how-to-get-a-spread-radius-this-way\">Spread it out: how to get a spread radius this way<\/h2>\n\n\n\n<p>The <code>box-shadow<\/code> property also allows us to control a fourth length value beside the offsets and the blur radius: the spread radius. To get the same effect with an SVG <code>filter<\/code>, we either <code>erode<\/code> or <code>dilate<\/code> the alpha map (<code>SourceAlpha<\/code>) using <code>feMorphology<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'si'<\/span>&gt;<\/span>\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feMorphology<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">operator<\/span>=<span class=\"hljs-string\">'dilate'<\/span> <span class=\"hljs-attr\">radius<\/span>=<span class=\"hljs-string\">'5'<\/span>\/&gt;<\/span>\n<\/span><\/mark><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>When using the <code>dilate<\/code> operator, what <code>feMorphology<\/code> does is the following: for every channel of every pixel, it takes the maximum of all the values of that channel for the pixels lying within the specified <code>radius<\/code> (from the current pixel) along both the <em>x<\/em> and the <em>y<\/em> axes in both the negative and positive direction.<\/p>\n\n\n\n<p>Below, you can see how this works for a channel whose values are either maxed out (<code>1<\/code>) or zeroed (<code>0<\/code>). For every pixel of the input (green outline around the current one), the corresponding output value for the same channel is the maximum of all the values for that channel within a <code>radius<\/code> of <code>1<\/code> from the current pixel (within the red square).<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player aligncenter wp-block-jetpack-videopress--has-max-width\" style=\"max-width: 610px;\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='603' src='https:\/\/videopress.com\/embed\/mVG9dK0S?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=1&amp;persistVolume=0&amp;playsinline=1&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1739540970'><\/script><\/div>\n\t\t\t<figcaption>how dilation works in the general case<\/figcaption>\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>In our case, things are made simpler by the fact that the input of this primitive is the alpha map of the <code>filter<\/code> input (<code>SourceAlpha<\/code>). Each and every single one of its pixels has the RGB values zeroed, so it basically doesn&#8217;t change anything on the RGB channels. The only thing that changes is the alpha channel at the edges, which is again made simpler by the fact that our input is a rectangular box (the alpha is <code>1<\/code> within the rectangle boundary and <code>0<\/code> outside), so for a <code>radius<\/code> of <code>1<\/code>, our black box grows by <code>1px<\/code> in every one of the four directions (top, right, bottom, left), for a <code>radius<\/code> of <code>2<\/code> it grows by <code>2px<\/code> in every direction and so on.<\/p>\n\n\n\n<p>When using the <code>erode<\/code> operator, <code>feMorphology<\/code> takes the minimum of all the values of each channel for the pixels lying within the specified <code>radius<\/code> (from the current pixel) along both the <em>x<\/em> and the <em>y<\/em> axes in both the negative and positive direction.<\/p>\n\n\n\n<p>Below, you can see a similar recording to the dilation one, only this time it&#8217;s for erosion. For every pixel of the input (green outline around the current one), the corresponding output value for the same channel is the minimum of all the values for that channel within a <code>radius<\/code> of <code>1<\/code> from the current pixel (within the red square).<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player aligncenter wp-block-jetpack-videopress--has-max-width\" style=\"max-width: 634px;\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='600' src='https:\/\/videopress.com\/embed\/0Yeg4bUB?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1739540970'><\/script><\/div>\n\t\t\t<figcaption>how erosion works in the general case<\/figcaption>\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>So in our particular case, erosion means that for a <code>radius<\/code> of <code>1<\/code>, our black box shrinks by <code>1px<\/code> in every one of the four directions (top, right, bottom, left), for a <code>radius<\/code> of <code>2<\/code> it shrinks by <code>2px<\/code> in every direction and so on.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bNVaGzW\/29a67931d6775e8cc5669a10f283e5d0\" src=\"\/\/codepen.io\/anon\/embed\/bNVaGzW\/29a67931d6775e8cc5669a10f283e5d0?height=650&amp;theme-id=1&amp;slug-hash=bNVaGzW\/29a67931d6775e8cc5669a10f283e5d0&amp;default-tab=result\" height=\"650\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bNVaGzW\/29a67931d6775e8cc5669a10f283e5d0\" title=\"CodePen Embed bNVaGzW\/29a67931d6775e8cc5669a10f283e5d0\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Just for fun, the interactive demo above allows switching between <code>SourceAlpha<\/code> and <code>SourceGraphic<\/code>. This is completely irrelevant in the context of this article, but it was a little low effort extra that allows seeing the effect of this primitive on the RGB channels too.<\/p>\n\n\n\n<p>Since <code>erode<\/code> is a <code>min()<\/code> result where the lowest channel value wins, this operation darkens our input. Since <code>dilate<\/code> is a <code>max()<\/code> result where the highest channel value wins, this operation brightens our input. Also, they both create squarish shapes, which makes sense given the how behind, illustrated in the videos above. Basically, in the <code>dilate<\/code> case, every pixel brighter than those around it expands into a bigger and bigger square the more the <code>radius<\/code> increases; and in the erode case, every pixel darker than those around it expands into a bigger and bigger square as the <code>radius<\/code> increases.<\/p>\n\n\n\n<p>So if we introduce this <code>feMorphology<\/code> primitive before all the others in our inset shadow <code>filter<\/code> (keep in mind this also means removing the <code>in='SourceAlpha<\/code>&#8216; attribute from <code>feOffset<\/code>, as we want the <code>feOffset<\/code> input to be the <code>feMorphology<\/code> result and, if we don&#8217;t explicitly set the <code>in<\/code> attribute, it defaults to the result of the previous primitive), it&#8217;s going to allow us to emulate the spread radius CSS provides for <code>box-shadow<\/code>.<\/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 shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">'0'<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">'true'<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">filter<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">'si'<\/span>&gt;<\/span>\n<\/span><\/span><mark class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feMorphology<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">operator<\/span>=<span class=\"hljs-string\">'dilate'<\/span> <span class=\"hljs-attr\">radius<\/span>=<span class=\"hljs-string\">'3'<\/span>\/&gt;<\/span>\n<\/span><\/mark><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feOffset<\/span> <span class=\"hljs-attr\">dx<\/span>=<span class=\"hljs-string\">'9'<\/span> <span class=\"hljs-attr\">dy<\/span>=<span class=\"hljs-string\">'13'<\/span>\/&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feGaussianBlur<\/span> <span class=\"hljs-attr\">stdDeviation<\/span>=<span class=\"hljs-string\">'5'<\/span>\/&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feComposite<\/span> <span class=\"hljs-attr\">in<\/span>=<span class=\"hljs-string\">'SourceAlpha'<\/span> <span class=\"hljs-attr\">operator<\/span>=<span class=\"hljs-string\">'out'<\/span>\/&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">feBlend<\/span> <span class=\"hljs-attr\">in2<\/span>=<span class=\"hljs-string\">'SourceGraphic'<\/span>\/&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">filter<\/span>&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span>\n<\/span><\/span><\/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<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_RNWBdmx\/a5b95926e9bba0f463fe5f80f275fc56\" src=\"\/\/codepen.io\/anon\/embed\/RNWBdmx\/a5b95926e9bba0f463fe5f80f275fc56?height=750&amp;theme-id=1&amp;slug-hash=RNWBdmx\/a5b95926e9bba0f463fe5f80f275fc56&amp;default-tab=result\" height=\"750\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed RNWBdmx\/a5b95926e9bba0f463fe5f80f275fc56\" title=\"CodePen Embed RNWBdmx\/a5b95926e9bba0f463fe5f80f275fc56\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Note that here we may also change the order of the <code>feMorphology<\/code> and <code>feOffset<\/code> primitives before the <code>feGaussianBlur<\/code> one and still get the same result, just like we may also change the order of the <code>feOffset<\/code> and <code>feGaussianBlur<\/code> primitives after the <code>feMorphology<\/code> one. However, the <code>feMorphology<\/code> primitive needs to be before the <code>feGaussianBlur<\/code> one, as having the <code>feGaussianBlur<\/code> primitive before the <code>feMorphology<\/code> one would give us a different result from what we want.<\/p>\n\n\n\n<p>Unlike the CSS spread radius used by <code>box-shadow<\/code>, the <code>radius<\/code> attribute can only be positive here, so the <code>operator<\/code> value makes the difference, each of the two giving us a result that&#8217;s equivalent to either a positive CSS spread radius or a negative one.<\/p>\n\n\n\n<p>Since the dilated\/eroded, offset and blurred alpha map is subtracted (minus sign) out of the initial one for an inset shadow, the <code>erode<\/code> case corresponds to a positive spread radius, while the <code>dilate<\/code> case corresponds to a negative one.<\/p>\n\n\n\n<p>If we were to use a similar technique for an outer shadow, where the dilated\/eroded, offset and blurred alpha map would be the shadow itself, wouldn&#8217;t be subtracted out of anything (so plus sign in this case), the <code>erode<\/code> case would correspond to a negative spread radius and the <code>dilate<\/code> case to a positive one.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A fancier example<\/h3>\n\n\n\n<p>We can take it one step further and not only have an inner shadow with a spread, but also add a little touch that isn&#8217;t possible on any element with CSS alone: noise! This is done by displacing the inset shadow using a noise map, similar to how we create&nbsp;<a href=\"https:\/\/frontendmasters.com\/blog\/grainy-gradients\/\" target=\"_blank\" rel=\"noreferrer noopener\">grainy gradients<\/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=\"339\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/dilate_case_example_squircle.png?resize=1024%2C339&#038;ssl=1\" alt=\"A grid of squircle-shaped images with grainy inner shadows that also have a spread.\" class=\"wp-image-7316\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/dilate_case_example_squircle.png?resize=1024%2C339&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/dilate_case_example_squircle.png?resize=300%2C99&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/dilate_case_example_squircle.png?resize=768%2C254&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/dilate_case_example_squircle.png?resize=1536%2C508&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/dilate_case_example_squircle.png?w=1596&amp;ssl=1 1596w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">squircle-shaped images with inset shadows with spread and grain (<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/LEGEwdp\" target=\"_blank\" rel=\"noreferrer noopener\">live demo<\/a>)<\/figcaption><\/figure>\n\n\n\n<p><a href=\"https:\/\/gist.github.com\/thebabydino\/dcd680fd330be34030487b52315c6ff7\/raw\/3696b3c79c4426ef170cf2985a905d17dfb00dc4\/inset-shadow-img-basics.md\"><\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Inset `box-shadow` doesn&#8217;t work directly on image elements. There are work-arounds, but this SVG filter can do it directly. <\/p>\n<p>Don&#8217;t run! There is powerful stuff to learn here through interactive demos. <\/p>\n","protected":false},"author":32,"featured_media":7303,"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":[220,7,262,133,91],"class_list":["post-7213","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-box-shadow","tag-css","tag-filter","tag-images","tag-svg"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/09\/Screenshot-2025-09-29-at-1.26.41-PM.png?fit=2248%2C1332&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7213","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=7213"}],"version-history":[{"count":25,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7213\/revisions"}],"predecessor-version":[{"id":7320,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7213\/revisions\/7320"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/7303"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=7213"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=7213"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=7213"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}