{"id":6737,"date":"2025-08-20T10:40:49","date_gmt":"2025-08-20T15:40:49","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=6737"},"modified":"2025-08-20T10:40:50","modified_gmt":"2025-08-20T15:40:50","slug":"obsessing-over-smooth-radial-gradient-disc-edges","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/obsessing-over-smooth-radial-gradient-disc-edges\/","title":{"rendered":"Obsessing Over Smooth radial-gradient() Disc Edges"},"content":{"rendered":"\n<p>(&#8230; and how that lead me to a very underused CSS feature, resolution media queries.)<\/p>\n\n\n\n<p>You may have come across this situation: you want to create a disc (oval) shape contained within your element&#8217;s boundaries, and you want it to have smooth edges. Not jagged; not blurry.<\/p>\n\n\n\n<p>If you want to avoid using a pseudo-element or, even worse, children just for decorative purposes, then <code>radial-gradient()<\/code> seems to be the best solution. Especially in the case where you might need a bunch of such discs, more than the two pseudos available on an element.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-jaggies-problem\">The jaggies problem<\/h2>\n\n\n\n<p>However, if we do something like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">radial-gradient<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--r<\/span>), <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--c<\/span>) 100%, <span class=\"hljs-selector-id\">#0000<\/span>)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><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>Where <code>r<\/code> is the gradient disc radius, then we get <a href=\"https:\/\/en.wikipedia.org\/wiki\/Jaggies\">jaggies<\/a>, a step-like effect along the <code>radial-gradient()<\/code> disc, whereas one created with a pseudo-element has smooth-looking edges!<\/p>\n\n\n\n<p>Note that we aren&#8217;t setting a stop position explicitly for the final stop because the stop position of the final stop defaults to <code>100%<\/code> (of the <code>radial-gradient()<\/code> radius, which is <code>r<\/code> here), which is what we want in this case anyway. If you need a refresher on <code>radial-gradient()<\/code>, check out this <a href=\"https:\/\/patrickbrosset.com\/articles\/2022-10-24-do-you-really-understand-CSS-radial-gradients\/\">detailed explainer<\/a> by Patrick Brosset.<\/p>\n\n\n\n<p>You can see the difference between a pseudo-element disc (smooth edges) and a <code>radial-gradient()<\/code> one (jaggies) in this live demo:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_wBKeYBK\" src=\"\/\/codepen.io\/anon\/embed\/wBKeYBK?height=750&amp;theme-id=1&amp;slug-hash=wBKeYBK&amp;default-tab=result\" height=\"750\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed wBKeYBK\" title=\"CodePen Embed wBKeYBK\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>The smooth-looking edges of the pseudo-element version are a result of anti-aliasing, as it can be seen from the screen recording below:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"646\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/antialiasing.gif?resize=800%2C646&#038;ssl=1\" alt=\"Animated GIF. Shows the pseudo disc at the top and the radial-gradient() one at the bottom. Zooming in at pixel level in the edge area for both shows us we have a sharp transition from our brick red to transparent in the radial-gradient() case. However, in the pseudo case, anti-aliasing means we have semi-transparent pixels smoothing the transition from brick red to transparent.\" class=\"wp-image-6738\" style=\"width:626px;height:auto\"\/><figcaption class=\"wp-element-caption\">recording of zooming in at the disc edges for the two cases<\/figcaption><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"a-popular-yet-too-imperfect-fix\">A popular, yet too imperfect fix<\/h2>\n\n\n\n<p>A solution I often see used to try to fix <code>radial-gradient()<\/code> discs is introducing a <code>1%<\/code> distance between the positions of the two stops, something like this.<\/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\">radial-gradient<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--r<\/span>), <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--c<\/span>) 99%, <span class=\"hljs-selector-id\">#0000<\/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>As I mentioned before, unless another value is explicitly specified, the final stop position defaults to <code>100%<\/code>, so there&#8217;s never any need to explicitly set it to that value since it&#8217;s the default.<\/p>\n\n\n\n<p>However, a <code>1%<\/code> distance means blurry edges for big discs&#8230;<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"960\" height=\"480\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_perc_big_blur_aspect_ratio_2.png?resize=960%2C480&#038;ssl=1\" alt=\"Screenshot. Shows a big reddish disc with slightly blurry edges.\" class=\"wp-image-6809\" style=\"object-fit:cover\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_perc_big_blur_aspect_ratio_2.png?w=960&amp;ssl=1 960w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_perc_big_blur_aspect_ratio_2.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_perc_big_blur_aspect_ratio_2.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 960px) 100vw, 960px\" \/><figcaption class=\"wp-element-caption\">a big disc with a 1% distance between the red and transparent stop positions has blurry edges<\/figcaption><\/figure>\n<\/div>\n\n\n<p>&#8230; while we still get jaggies for small discs!<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"966\" height=\"160\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_perc_small_jagged.png?resize=966%2C160&#038;ssl=1\" alt=\"Screenshot. Shows a small reddish disc with slightly jagged edges.\" class=\"wp-image-6740\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_perc_small_jagged.png?w=966&amp;ssl=1 966w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_perc_small_jagged.png?resize=300%2C50&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_perc_small_jagged.png?resize=768%2C127&amp;ssl=1 768w\" sizes=\"auto, (max-width: 966px) 100vw, 966px\" \/><figcaption class=\"wp-element-caption\">a small disc with a 1% distance between the red and transparent stop positions has jagged edges<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"a-solution-i-thought-was-bulletproof\">A solution I thought was bulletproof<\/h2>\n\n\n\n<p>So my solution, which, up until recently, I thought would never fail, was to have a <code>1px<\/code> distance between the positions of our two stops:<\/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\">radial-gradient<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--r<\/span>), <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--c<\/span>) <span class=\"hljs-selector-tag\">calc<\/span>(100% <span class=\"hljs-selector-tag\">-<\/span> 1<span class=\"hljs-selector-tag\">px<\/span>), <span class=\"hljs-selector-id\">#0000<\/span>)<\/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>This works well regardless of disc size&#8230; until it doesn&#8217;t!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"a-pixel-is-not-always-a-pixel\">A pixel is not always a pixel<\/h2>\n\n\n\n<p>So there are situations when my &#8220;bulletproof&#8221; solution fails. For example, in two cases I&#8217;ve never really considered before, since my main laptop is almost two decades old: with a hi-DPI display or with &#8220;those pesky users doing their nasty zooms&#8221; (<a href=\"https:\/\/x.com\/myfonj\/status\/1939739313903710299\">credit for this gem<\/a>).<\/p>\n\n\n\n<p>In this case, when we <em><strong>zoom in<\/strong><\/em> up to a zoom level of <code>500%<\/code>, we get again blurry edges&#8230;<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"960\" height=\"480\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_px_zoom_in_aspect_ratio_2.png?resize=960%2C480&#038;ssl=1\" alt=\"Screenshot. Shows a big red disc with slightly blurry edges. The zoom level of 500% is also shown in the top right corner.\" class=\"wp-image-6808\" style=\"object-fit:cover\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_px_zoom_in_aspect_ratio_2.png?w=960&amp;ssl=1 960w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_px_zoom_in_aspect_ratio_2.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_px_zoom_in_aspect_ratio_2.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 960px) 100vw, 960px\" \/><figcaption class=\"wp-element-caption\"><em>a zoomed in page with a fully contained disc with a <code>1px<\/code> distance between the red and transparent stop positions &#8211; this disc has blurry edges due to the zoom<\/em><\/figcaption><\/figure>\n<\/div>\n\n\n<p>&#8230; and when we <em><strong>zoom out<\/strong><\/em> up to a zoom level of <code>25%<\/code>, we get jagged edges!<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"960\" height=\"480\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_px_zoom_out_aspect_ratio_2.png?resize=960%2C480&#038;ssl=1\" alt=\"Screenshot. Shows a big red disc with slightly jagged edges. The zoom level of 25% is also shown in the top right corner.\" class=\"wp-image-6807\" style=\"object-fit:cover\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_px_zoom_out_aspect_ratio_2.png?w=960&amp;ssl=1 960w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_px_zoom_out_aspect_ratio_2.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/base_px_zoom_out_aspect_ratio_2.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 960px) 100vw, 960px\" \/><figcaption class=\"wp-element-caption\">a zoomed out page with a fully contained disc with a <code>1px<\/code> distance between the red and transparent stop positions &#8211; this disc has jagged edges due to the zoom<\/figcaption><\/figure>\n<\/div>\n\n\n<p>Boo!<\/p>\n\n\n\n<p>So what can we do in this case?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"underrated-css-feature-resolution\">Underrated CSS feature: resolution!<\/h2>\n\n\n\n<p>Up until this summer, when I got fixated on this zoom problem, I had no idea that CSS provides <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/@media\/resolution\">resolution<\/a> media queries! These allow us to style things differently based on the device pixel density or zoom level.<\/p>\n\n\n\n<p>I don&#8217;t think I have access to any device with a higher pixel ratio display, but I can certainly test zoom. For zoom, this thing really works! For example, if we&#8217;re zoomed in to <code>500%<\/code>, we&#8217;re in the <code>5x<\/code> case:<\/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-keyword\">@media<\/span> (<span class=\"hljs-attribute\">resolution:<\/span> <span class=\"hljs-number\">5<\/span>x) {}<\/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>This means we can divide that <code>1px<\/code> difference by a factor <code>f<\/code> which we set in the media query.<\/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\">div<\/span> {\n  <span class=\"hljs-attribute\">background<\/span>: \n    <span class=\"hljs-built_in\">radial-gradient<\/span>(var(--r), \n    <span class=\"hljs-built_in\">var<\/span>(--c) <span class=\"hljs-built_in\">calc<\/span>(<span class=\"hljs-number\">100%<\/span> - <span class=\"hljs-number\">1px<\/span>\/var(--f, <span class=\"hljs-number\">1<\/span>)), <span class=\"hljs-number\">#0000<\/span>)\n}\n\n<span class=\"hljs-keyword\">@media<\/span> (<span class=\"hljs-attribute\">resolution:<\/span> <span class=\"hljs-number\">5<\/span>x) { <span class=\"hljs-selector-tag\">div<\/span> { <span class=\"hljs-attribute\">--f<\/span>: <span class=\"hljs-number\">5<\/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>Note that the <code>x<\/code> unit is an alias for the <code>dppx<\/code> unit, an alias that was only added in <a href=\"https:\/\/drafts.csswg.org\/css-values-4\/#resolution-value\">Level 4<\/a> of the CSS Values and Units Module (<a href=\"https:\/\/drafts.csswg.org\/css-values-3\/#resolution-value\">Level 3<\/a> did not include x). However, at this point, I&#8217;d say it&#8217;s safe to use since all major current desktop and mobile browsers have been supporting it for over half a decade.<\/p>\n\n\n\n<p>I prefer using <code>x<\/code> as it&#8217;s shorter and it feels more intuitive and consistent with <code>picture<\/code> sources.<\/p>\n\n\n\n<p>We can do the same for all other zoom levels Chromium browsers provide using Sass looping:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"SCSS\" data-shcb-language-slug=\"scss\"><span><code class=\"hljs language-scss\"><span class=\"hljs-variable\">$f<\/span>: .<span class=\"hljs-number\">25<\/span> .<span class=\"hljs-number\">33<\/span> .<span class=\"hljs-number\">5<\/span> .<span class=\"hljs-number\">67<\/span> .<span class=\"hljs-number\">75<\/span> .<span class=\"hljs-number\">8<\/span> .<span class=\"hljs-number\">9<\/span> <span class=\"hljs-number\">1.1<\/span> <span class=\"hljs-number\">1.25<\/span> <span class=\"hljs-number\">1.33<\/span> <span class=\"hljs-number\">1.4<\/span> <span class=\"hljs-number\">1.5<\/span> <span class=\"hljs-number\">1.75<\/span> <span class=\"hljs-number\">2<\/span> <span class=\"hljs-number\">2.5<\/span> <span class=\"hljs-number\">3<\/span> <span class=\"hljs-number\">4<\/span> <span class=\"hljs-number\">5<\/span>;\n\n<span class=\"hljs-variable\">$n<\/span>: length(<span class=\"hljs-variable\">$f<\/span>);\n\n<span class=\"hljs-keyword\">@for<\/span> <span class=\"hljs-variable\">$i<\/span> from <span class=\"hljs-number\">0<\/span> to <span class=\"hljs-variable\">$n<\/span> {\n  <span class=\"hljs-keyword\">@media<\/span> (resolution: nth(<span class=\"hljs-variable\">$f<\/span>, <span class=\"hljs-variable\">$i<\/span> + <span class=\"hljs-number\">1<\/span>)*<span class=\"hljs-number\">1<\/span>x) {\n    <span class=\"hljs-selector-tag\">div<\/span> { --f: #{nth(<span class=\"hljs-variable\">$f<\/span>, <span class=\"hljs-variable\">$i<\/span> + <span class=\"hljs-number\">1<\/span>)} }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">SCSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">scss<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This gives us a nice pure CSS way of ensuring we have smooth disc edges, not jagged, not blurry, regardless of display resolution or zoom level.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"800\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/smoo_zoom_test.gif?resize=800%2C800&#038;ssl=1\" alt=\"Animated GIF. Shows how zooming from 25% to 500% doesn't affect the radial-gradient() disc edges anymore.\" class=\"wp-image-6743\"\/><figcaption class=\"wp-element-caption\">zooming doesn&#8217;t mess up the edges of our <code>radial-gradient()<\/code> disc anymore<\/figcaption><\/figure>\n\n\n\n<div class=\"wp-block-group learn-more\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<p>Side note: for anyone wondering why the disc starts getting smaller once we&#8217;ve increased the zoom above a certain level, this is due to the way we&#8217;ve defined the disc radius:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">--r<\/span>: <span class=\"hljs-selector-tag\">min<\/span>(50<span class=\"hljs-selector-tag\">vmin<\/span> <span class=\"hljs-selector-tag\">-<\/span> 2<span class=\"hljs-selector-tag\">em<\/span>, 9<span class=\"hljs-selector-tag\">em<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>For large screens\/ low zoom levels, the second value in the <code>min()<\/code> (<code>9em<\/code>) is the one that&#8217;s used, as it&#8217;s smaller. Since the default <code>font-size<\/code> and, consequently, any <code>em<\/code> value always increases with zoom, the second <code>min()<\/code> value becomes bigger than the first after a certain level of zoom, so then it&#8217;s the first value that gets used. For <code>50vmin - 2em<\/code>, <code>50vmin<\/code> is always constant, doesn&#8217;t depend on the zoom level, but <code>2em<\/code> increases with zoom. This means our difference <code>50vmin - 2em<\/code> decreases with zoom.<\/p>\n<\/div><\/div>\n\n\n\n<p>Cool, but that&#8217;s quite a lot of media queries and what do we do when other browsers have other zoom levels available instead of the ones in our list above, which is Chromium specific?<\/p>\n\n\n\n<p>For example, Firefox goes from a <code>50%<\/code> zoom level to a <code>30%<\/code> one, which is the smallest value. It also uses <code>120%<\/code>, <code>170%<\/code> and <code>240%<\/code> zoom values instead of <code>125%<\/code>, <code>175%<\/code> and <code>250%<\/code> respectively in Chrome.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"800\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/zoom_levels_firefox.gif?resize=800%2C800&#038;ssl=1\" alt=\"Animated GIF. Same as before, only in Firefox this time, where zoom levels are different.\" class=\"wp-image-6744\"\/><figcaption class=\"wp-element-caption\">zoom levels in Firefox are different<\/figcaption><\/figure>\n\n\n\n<p>This means that since we have no match for a zoom level of <code>30%<\/code>, <code>--f<\/code> remains <code>1<\/code> there, just like in the default case, which means the zoomed out <code>1px<\/code> difference is seen as less than a third of that, resulting in jaggies at this smallest Firefox zoom level.<\/p>\n\n\n\n<p>When zooming in, the blur problem is pretty much undetectable for the <code>120%<\/code> zoom level (which again has no match among our resolution media queries), but it starts being noticeable for the bigger no match zoom levels at <code>170%<\/code> and <code>240%<\/code>.<\/p>\n\n\n\n<p>We could add those Firefox zoom levels to the list&#8230; or we could do something better! That is, use max and min resolution depending on whether we&#8217;re in the subunitary case or not, and also reverse the order of the subunitary zooms. The second part is because if we were to have the same order, with <code>.9<\/code> being after <code>.8<\/code>, then the <code>(max-resolution: .9x)<\/code> case would override the <code>(max-resolution: .8x)<\/code> one.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"SCSS\" data-shcb-language-slug=\"scss\"><span><code class=\"hljs language-scss\"><span class=\"hljs-variable\">$f<\/span>: .<span class=\"hljs-number\">9<\/span> .<span class=\"hljs-number\">8<\/span> .<span class=\"hljs-number\">75<\/span> .<span class=\"hljs-number\">67<\/span> .<span class=\"hljs-number\">5<\/span> .<span class=\"hljs-number\">33<\/span> .<span class=\"hljs-number\">25<\/span> \n    <span class=\"hljs-number\">1.1<\/span> <span class=\"hljs-number\">1.2<\/span> <span class=\"hljs-number\">1.33<\/span> <span class=\"hljs-number\">1.4<\/span> <span class=\"hljs-number\">1.5<\/span> <span class=\"hljs-number\">1.7<\/span> <span class=\"hljs-number\">2<\/span> <span class=\"hljs-number\">2.4<\/span> <span class=\"hljs-number\">3<\/span> <span class=\"hljs-number\">4<\/span> <span class=\"hljs-number\">5<\/span>;\n\n<span class=\"hljs-variable\">$n<\/span>: length(<span class=\"hljs-variable\">$f<\/span>);\n\n<span class=\"hljs-keyword\">@for<\/span> <span class=\"hljs-variable\">$i<\/span> from <span class=\"hljs-number\">0<\/span> to <span class=\"hljs-variable\">$n<\/span> {\n  <span class=\"hljs-variable\">$c<\/span>: nth(<span class=\"hljs-variable\">$f<\/span>, <span class=\"hljs-variable\">$i<\/span> + <span class=\"hljs-number\">1<\/span>);\n\n  <span class=\"hljs-keyword\">@media<\/span> (#{if(<span class=\"hljs-variable\">$c<\/span> &lt; 1, 'max', 'min')}-resolution: <span class=\"hljs-variable\">$c<\/span>*<span class=\"hljs-number\">1<\/span>x) {\n    div { --f: #{<span class=\"hljs-variable\">$c<\/span>} }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">SCSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">scss<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>A more subtle change from before is that, when the zoom levels are above <code>1<\/code>, we are using the slightly smaller of two zoom values that are close enough in Chrome and Firefox, but not quite the same. For example, between <code>1.25<\/code> in Chrome and <code>1.2<\/code> in Firefox we use <code>1.2<\/code>, between <code>2.5<\/code> in Chrome and <code>2.4<\/code> in Firefox, we use <code>2.4<\/code>. This is because the <code>(min-resolution: 1.2x)<\/code> case also catches the entire <code>(min-resolution: 1.25x)<\/code> case, but not the other way around. And the same thing goes for the other close, but not quite the same zoom level pairs from the two browsers.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bNVgrdp\" src=\"\/\/codepen.io\/anon\/embed\/bNVgrdp?height=450&amp;theme-id=1&amp;slug-hash=bNVgrdp&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bNVgrdp\" title=\"CodePen Embed bNVgrdp\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Much better! But what if we really hate having so many media queries?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-less-code-and-more-flexible-js-solution\">The less code and more flexible JS solution<\/h2>\n\n\n\n<p>In this case, we&#8217;d set <code>f<\/code> from the JS as follows:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">zoom<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-built_in\">document<\/span>.body.style.setProperty(<span class=\"hljs-string\">'--f'<\/span>, <span class=\"hljs-built_in\">window<\/span>.devicePixelRatio);\n  matchMedia(<span class=\"hljs-string\">`(resolution: <span class=\"hljs-subst\">${<span class=\"hljs-built_in\">window<\/span>.devicePixelRatio}<\/span>x)`<\/span>)\n    .addEventListener(<span class=\"hljs-string\">'change'<\/span>, zoom, { <span class=\"hljs-attr\">once<\/span>: <span class=\"hljs-literal\">true<\/span> });\n}\n\nzoom();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This works for any place where we may want to have <code>radial-gradient()<\/code> created discs &#8211; not just for <code>background<\/code> values, but also for <code>mask<\/code> or <code>border-image<\/code> values.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bNVrjdR\" src=\"\/\/codepen.io\/anon\/embed\/bNVrjdR?height=450&amp;theme-id=1&amp;slug-hash=bNVrjdR&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bNVrjdR\" title=\"CodePen Embed bNVrjdR\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"conclusion\">Conclusion<\/h2>\n\n\n\n<p>Is this overkill? Something only a psycho would do? It depends.<\/p>\n\n\n\n<p>In some cases, having smooth edges may be worth obsessing about. For example, if we use a <code>mask<\/code> as a fallback for <code>shape()<\/code> in the case of a component (like a <code>header<\/code>) with both convex and concave roundings.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"303\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/header.png?resize=1024%2C303&#038;ssl=1\" alt=\"Screenshot. Shows a header with both convex and concave roundings.\" class=\"wp-image-6745\" style=\"object-fit:cover\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/header.png?resize=1024%2C303&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/header.png?resize=300%2C89&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/header.png?resize=768%2C228&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/header.png?w=1080&amp;ssl=1 1080w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">screenshot of a header component with both convex and concave roundings (<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/azOgOKE\/09e91c7726a4d5340e38735298ef7bd0\">live demo<\/a>)<\/figcaption><\/figure>\n<\/div>\n\n\n<p>While newer Chrome and Safari versions have supported <code>shape()<\/code> for a few months now, Firefox support isn&#8217;t there yet. We could set the <code>layout.css.basic-shape-shape.enabled<\/code> flag to true in <code>about:config<\/code> to play with it there too, but remember, most people won&#8217;t have it enabled, and there is a reason why it&#8217;s still behind the flag in Firefox: not all commands work. We can use the lines and arcs we need for this particular shape, but B\u00e9zier curves don&#8217;t work yet. Furthermore, some people may be stuck on older hardware\/ operating systems and may be unable to update Chrome or Safari to the latest version. So having a fallback for <code>shape()<\/code> is very much necessary.<\/p>\n\n\n\n<p>Without the zoom\/device pixel ratio factor, we get ugly blurry edges for the concave rounding (the convex one is created via <code>border-radius<\/code>, so it doesn&#8217;t have this problem) at a zoom level of <code>500%<\/code> when <code>shape()<\/code> isn&#8217;t supported and the <code>mask<\/code> fallback is used (for example, in Firefox without the flag enabled).<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"318\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/zoom_mask_problem_no_fix_strip.png?resize=1024%2C318&#038;ssl=1\" alt=\"Screenshot. Shows the same header with both convex and concave roundings. We can see that the page has been zoomed in up to a level of 500% and that the edges of the concave roundings (obtained using radial-gradient() mask layers) are blurry.\" class=\"wp-image-6805\" style=\"object-fit:cover\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/zoom_mask_problem_no_fix_strip.png?resize=1024%2C318&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/zoom_mask_problem_no_fix_strip.png?resize=300%2C93&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/zoom_mask_problem_no_fix_strip.png?resize=768%2C239&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/zoom_mask_problem_no_fix_strip.png?w=1352&amp;ssl=1 1352w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">the problem when using the fallback without the zoom\/ device pixel ratio factor correction<\/figcaption><\/figure>\n<\/div>\n\n\n<p>There are, however, other cases where we could embrace (and maybe even enhance) the blurry edges instead of doing anything about them. For example, when the discs are a part of a faded background.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_zxvExxx\" src=\"\/\/codepen.io\/anon\/embed\/zxvExxx?height=350&amp;theme-id=1&amp;slug-hash=zxvExxx&amp;default-tab=css,result\" height=\"350\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed zxvExxx\" title=\"CodePen Embed zxvExxx\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n","protected":false},"excerpt":{"rendered":"<p>An underdog media query, resolution queries, comes to the rescue here in defining radial gradients that don&#8217;t blur or get the jaggies.<\/p>\n","protected":false},"author":32,"featured_media":6823,"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-6737","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\/08\/jaggies-1.jpg?fit=1600%2C800&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6737","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=6737"}],"version-history":[{"count":11,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6737\/revisions"}],"predecessor-version":[{"id":6824,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6737\/revisions\/6824"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/6823"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=6737"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=6737"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=6737"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}