{"id":3782,"date":"2024-09-11T14:18:31","date_gmt":"2024-09-11T19:18:31","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=3782"},"modified":"2024-09-13T10:47:35","modified_gmt":"2024-09-13T15:47:35","slug":"split-effects-with-no-content-duplication","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/split-effects-with-no-content-duplication\/","title":{"rendered":"Split Effects with no Content Duplication"},"content":{"rendered":"\n<p>A&nbsp;<a href=\"https:\/\/frontendmasters.com\/blog\/clip-pathing-color-changes\/\">recent post<\/a>&nbsp;here lead me to another called&nbsp;<a href=\"https:\/\/emilkowal.ski\/ui\/the-magic-of-clip-path\">The Magic of Clip Path<\/a>&nbsp;by Emil Kowalski, which focuses on the&nbsp;<code>inset()<\/code>&nbsp;basic shape in particular. While I agree that&nbsp;<code>clip-path<\/code>&nbsp;is a very useful property and the&nbsp;<code>inset()<\/code>&nbsp;basic shape is underrated and underused on the web, most of the use case examples in the article are far from ideal as they rely on content duplication, which can come at a maintenance, performance, and accessibility cost, not to mention that some of them break in some scenarios.<\/p>\n\n\n\n<p>In this article, I&#8217;ll be showing how to get the same effects with no content duplication.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"comparison-sliders\">Comparison Sliders<\/h2>\n\n\n\n<p>Emil creates this with two different images (the before and the after) stacked one on top of the other, the top one being clipped, plus a&nbsp;<code>button<\/code>&nbsp;for the draggable line control.<\/p>\n\n\n\n<p>Using a&nbsp;<code>button<\/code>&nbsp;for the draggable line somehow doesn&#8217;t feel right to me, but I&#8217;m no expert when it comes to accessibility, so we&#8217;ll be focusing on how to do this with a single image, though this is the one example where I can see some advantages to using two images instead of one.<\/p>\n\n\n\n<p>This kind of comparison slider is something I once explained in detail in&nbsp;<a href=\"https:\/\/css-tricks.com\/taming-blend-modes-difference-and-exclusion\/#aa-invert-just-an-area-of-an-element-or-a-background\">another article<\/a>&nbsp;some years back. The basic idea used there is the following: the original image (the &#8220;before&#8221;) is a&nbsp;<code>background<\/code>&nbsp;layer of a slider whose thumb is the draggable split line. The original image&nbsp;<code>background<\/code>&nbsp;layer is blended with another which only covers the progress area between the lateral edge of the track and the current thumb position. The result of the blending operation is the &#8220;after&#8221;.<\/p>\n\n\n\n<p>In my old article, the result of the blending operation was the negative of the image:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_qBqqoXa\" src=\"\/\/codepen.io\/anon\/embed\/qBqqoXa?height=450&amp;theme-id=47434&amp;slug-hash=qBqqoXa&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed qBqqoXa\" title=\"CodePen Embed qBqqoXa\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Here&#8217;s a fancier version of it:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_OJbmxPq\" src=\"\/\/codepen.io\/anon\/embed\/OJbmxPq?height=450&amp;theme-id=47434&amp;slug-hash=OJbmxPq&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed OJbmxPq\" title=\"CodePen Embed OJbmxPq\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>But we can also have other effects, for example desaturating an image (black and white photography effect).<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_MWMvxxX\" src=\"\/\/codepen.io\/anon\/embed\/MWMvxxX?height=450&amp;theme-id=47434&amp;slug-hash=MWMvxxX&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed MWMvxxX\" title=\"CodePen Embed MWMvxxX\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>This demo needs a single HTML element (<code>input[type=range]<\/code>), less than 20 CSS declarations (and that&#8217;s with having to duplicate a bunch of them for the&nbsp;<code>-webkit-<\/code>&nbsp;and&nbsp;<code>-moz-<\/code>&nbsp;cases) and under&nbsp;<code>100<\/code>&nbsp;bytes of JS (without even bothering to minify it).<\/p>\n\n\n\n<p>The trick here is to use a&nbsp;<code>color<\/code>&nbsp;blend mode, which takes the the hue and the saturation of the top layer (transparent before the thumb and&nbsp;<em>any<\/em>&nbsp;grey after) and the luminosity (which is not&nbsp;<a href=\"https:\/\/codepen.io\/thebabydino\/full\/RwoOMOZ\">the &#8216;L&#8217; in HSL<\/a>, that one stands for lightness, but still close enough in a lot of cases) of the bottom layer (the image).<\/p>\n\n\n\n<p>The saturation of&nbsp;<em>any<\/em>&nbsp;grey is&nbsp;<code>0<\/code>&nbsp;and zero saturation makes the hue irrelevant (if you think about&nbsp;<a href=\"https:\/\/codepen.io\/thebabydino\/full\/NvKEpd\">the HSL bicone<\/a>, the saturation is the horizontal distance from the vertical axis, so the greys are on the vertical axis, where the rotation around it, which gives us the hue,&nbsp;<a href=\"https:\/\/codepen.io\/thebabydino\/full\/ZELeqVN\">doesn&#8217;t matter anymore<\/a>).<\/p>\n\n\n\n<p>That is, the&nbsp;<code>color<\/code>&nbsp;blend mode with any grey top layer zeroes the saturation of the result, giving us a fully desaturated image (as if we applied&nbsp;<code>filter: grayscale(1)<\/code>&nbsp;on it).<\/p>\n\n\n\n<p>We can also make a fully desaturated image monochrome. This only requires a couple of tiny changes from the previous demo: replacing the grey with a dark blue (or anything with non-zero saturation, really) and using a black and white image.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ExBvJjz\" src=\"\/\/codepen.io\/anon\/embed\/ExBvJjz?height=450&amp;theme-id=47434&amp;slug-hash=ExBvJjz&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ExBvJjz\" title=\"CodePen Embed ExBvJjz\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Or why just monochrome it when we can duotone it?<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_jOjLRyp\" src=\"\/\/codepen.io\/anon\/embed\/jOjLRyp?height=450&amp;theme-id=47434&amp;slug-hash=jOjLRyp&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed jOjLRyp\" title=\"CodePen Embed jOjLRyp\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Here, we just switched to an\u00a0<code>exclusion<\/code>\u00a0blend mode. How this works in the back is something I explained in a lot of detail in <a href=\"https:\/\/css-tricks.com\/taming-blend-modes-difference-and-exclusion\/#aa-invert-just-an-area-of-an-element-or-a-background\">the blend modes article<\/a>.<\/p>\n\n\n\n<p>This technique also has the advantage of only needing a minimal amount of JS. All the JS does here is to update one custom property&nbsp;<code>--val<\/code>&nbsp;when the slider thumb gets dragged. That&#8217;s it!<\/p>\n\n\n\n<p>However, the amount of effects we can achieve by blending is limited and, since I wrote that article, I&#8217;ve changed my mind about the image as a&nbsp;<code>background<\/code>&nbsp;approach and I don&#8217;t like it as much anymore nowadays. I&#8217;d rather have an actual&nbsp;<code>img<\/code>&nbsp;element there, which can get a proper right click menu and a proper&nbsp;<code>alt<\/code>&nbsp;text (even though the sliders in the previous examples have a label for screen readers which explains the changing image effect on dragging the thumb).<\/p>\n\n\n\n<p>So the more flexible and overall better approach I\u2019d go for nowadays involves an <code>img<\/code> element and an <code>input[type=range]<\/code> inside a wrapper kind of structure. On the CSS side, I&#8217;d use a<code>\u00a0backdrop-filter<\/code> (which opens the door to endless possibilities) instead of blending. The JS remains the same, we only need it to update the same custom property as before.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_XWLawxg\" src=\"\/\/codepen.io\/anon\/embed\/XWLawxg?height=450&amp;theme-id=47434&amp;slug-hash=XWLawxg&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed XWLawxg\" title=\"CodePen Embed XWLawxg\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>The trick is to make the wrapper a&nbsp;<code>grid<\/code>&nbsp;container with a single grid cell, stack and stretch inside this cell the&nbsp;<code>img<\/code>, then a wrapper pseudo-element and finally the&nbsp;<code>input[type=range]<\/code>&nbsp;on top of both.<\/p>\n\n\n\n<p>Both the pseudo-element and the slider get&nbsp;<code>pointer-events: none<\/code>, so that right click brings up a menu that allows us to open the image in a new tab, save it, copy it and so on. Note that this needs to get reverted on the slider&#8217;s&nbsp;<code>thumb<\/code>&nbsp;(by setting&nbsp;<code>pointer-events: auto<\/code>) so we can drag it. The pseudo-element gets clipped to just the area between the slider&#8217;s edge and the thumb&#8217;s vertical midline. It also has a&nbsp;<code>backdrop-filter<\/code>. This creates the desired effect on the part of the image underneath the area this pseudo-element is clipped to.<\/p>\n\n\n\n<p>The demo above shows a blur effect, but we have unlimited possibilities here, as we can also chain CSS filters or even use SVG ones. We can make our image grainy, introduce&nbsp;<a href=\"https:\/\/mastodon.social\/@anatudor\/112444748312431903\">chromatic aberration<\/a>&nbsp;or&nbsp;<a href=\"https:\/\/mastodon.social\/@anatudor\/112242678457752295\">swap two channels<\/a>&#8230; the sky&#8217;s the limit!<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_qBzXGzK\" src=\"\/\/codepen.io\/anon\/embed\/qBzXGzK?height=450&amp;theme-id=47434&amp;slug-hash=qBzXGzK&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed qBzXGzK\" title=\"CodePen Embed qBzXGzK\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>One caveat though: as cool as in the browser image&nbsp;<code>filter<\/code>&nbsp;effects are, we only have access to the original image via the right click menu. We cannot save or copy the result we get after applying the&nbsp;<code>filter<\/code>&nbsp;this way. For the situation when we want that, stacking the filtered and clipped version of the image on top of the original is probably the better idea, even if we have to load two images.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"split-text\">Split Text<\/h2>\n\n\n\n<p>Emil showcases an example similar to the one <a href=\"https:\/\/twitter.com\/raunofreiberg\/status\/1785738080520925627\">used by Vercel here<\/a>. This duplicates the text and clips the version on top. It also breaks on small viewports.<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='350' src='https:\/\/videopress.com\/embed\/XjlqtLd8?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=1725245713'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>But we can do something like this with no text duplication whatsoever, which also allows us to avoid such problems, regardless of the viewport size.<\/p>\n\n\n\n<p>This can be seen in the demo below where you can drag the separator line:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_oNOrXQj\" src=\"\/\/codepen.io\/anon\/embed\/oNOrXQj?height=450&amp;theme-id=47434&amp;slug-hash=oNOrXQj&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed oNOrXQj\" title=\"CodePen Embed oNOrXQj\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>The trick here is to put the text fill, the text stroke and the progress area each on one RGB channel. In my demo, the text fill uses the blue channel, the text stroke uses the red channel and the progress area uses the green channel. Note that the progress area is created using a&nbsp;<a href=\"https:\/\/x.com\/anatudor\/status\/1478412237295566850\">full coverage pseudo<\/a>&nbsp;on the element containing the text and that this pseudo is blended with its parent.<\/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\">p<\/span> {\n  <span class=\"hljs-attribute\">position<\/span>: relative;\n  <span class=\"hljs-attribute\">color<\/span>: <span class=\"hljs-number\">#00f<\/span>;\n  <span class=\"hljs-attribute\">-webkit-text-stroke<\/span>: <span class=\"hljs-number\">#f00<\/span> <span class=\"hljs-number\">4px<\/span>;\n  <span class=\"hljs-attribute\">isolation<\/span>: isolate;\n\n  &amp;::after {\n    <span class=\"hljs-attribute\">position<\/span>: absolute;\n    <span class=\"hljs-attribute\">inset<\/span>: <span class=\"hljs-number\">0<\/span>;\n    <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">linear-gradient<\/span>(#<span class=\"hljs-number\">0<\/span>f0 var(--prc), <span class=\"hljs-number\">#000<\/span> <span class=\"hljs-number\">0<\/span>);\n    <span class=\"hljs-attribute\">mix-blend-mode<\/span>: lighten;\n    <span class=\"hljs-attribute\">pointer-events<\/span>: none;\n  }\n}<\/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>The&nbsp;<code>--prc<\/code>&nbsp;stop position is the progress value of the slider in&nbsp;<code>%<\/code>. The higher up we pull it, the lower the value and the other way around.<\/p>\n\n\n\n<p>The&nbsp;<code>isolation<\/code>&nbsp;property ensures the pseudo is only blended with its parent, but not with whatever backdrop may be behind its parent as well.&nbsp;<code>pointer-events: none<\/code>&nbsp;on the pseudo ensures we can click and select the text underneath.<\/p>\n\n\n\n<p>The result so far looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"419\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/a0c487c75678b468-1024x419-1.png?resize=1024%2C419&#038;ssl=1\" alt=\"\" class=\"wp-image-3790\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/a0c487c75678b468-1024x419-1.png?w=1024&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/a0c487c75678b468-1024x419-1.png?resize=300%2C123&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/a0c487c75678b468-1024x419-1.png?resize=768%2C314&amp;ssl=1 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">Intermediate result.<\/figcaption><\/figure>\n\n\n\n<p>You can see that here, each of the three RGB channels are either zeroed or maxed out (0 or&nbsp;1). The values for the red channel (text stroke) and the blue channel (text fill) are mutually exclusive, it&#8217;s just the green channel (progress area) that can mix with the other two.<\/p>\n\n\n\n<p>We then apply an SVG&nbsp;<code>filter<\/code>. Here, we combine two concepts I&#8217;ve talked about this year before: using&nbsp;<a href=\"https:\/\/mastodon.social\/@anatudor\/112286525196818095\">RGB channels as alpha masks<\/a>&nbsp;and painting the graphic we extract using&nbsp;<a href=\"https:\/\/mastodon.social\/@anatudor\/112157559510002242\">an RGB value<\/a>&nbsp;(using one of the two ways I did for&nbsp;<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/YzgwErb\">these monojicons<\/a>).<\/p>\n\n\n\n<p>Our&nbsp;<code>filter<\/code>&nbsp;extracts the intersection between the blue channel (the text fill) and the green channel (the progress area) &#8211; that is, the text fill within the limits of the progress area &#8211; and paints it&nbsp;<code>white<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"737\" height=\"313\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/4c8d4dd8abb9103b.png?resize=737%2C313&#038;ssl=1\" alt=\"\" class=\"wp-image-3789\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/4c8d4dd8abb9103b.png?w=737&amp;ssl=1 737w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/4c8d4dd8abb9103b.png?resize=300%2C127&amp;ssl=1 300w\" sizes=\"auto, (max-width: 737px) 100vw, 737px\" \/><figcaption class=\"wp-element-caption\">The intersection between the text fill and the progress area, painted in white.<\/figcaption><\/figure>\n\n\n\n<p>This is pretty much like creating an alpha mask that makes opaque the area where both the blue channel and the green channel are maxed out. And at the same time, makes transparent the area where at least one of the two is&nbsp;<code>0<\/code>.<\/p>\n\n\n\n<p>The&nbsp;<code>filter<\/code>&nbsp;also extracts the difference between the red channel (the text stroke) and the green channel (the progress area) &#8211; that is, the text stroke outside the progress area, then paints it using a variable (which can be either&nbsp;<code>currentColor<\/code>&nbsp;or a custom property,&nbsp;<code>var(--c-neon)<\/code>, for example).<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"437\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/89629fa5c262b96d.png?resize=1024%2C437&#038;ssl=1\" alt=\"\" class=\"wp-image-3791\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/89629fa5c262b96d.png?resize=1024%2C437&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/89629fa5c262b96d.png?resize=300%2C128&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/89629fa5c262b96d.png?resize=768%2C328&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/89629fa5c262b96d.png?w=1139&amp;ssl=1 1139w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">The difference between the text stroke and the progress area, painted in neon blue.<\/figcaption><\/figure>\n\n\n\n<p>Finally, we put these two together and we have the result!<\/p>\n\n\n\n<p>There are some other minor polishing tweaks in the demo, but this is the main idea. It&#8217;s very similar to other text split demos with no duplication I&#8217;ve made (as seen in&nbsp;<a href=\"https:\/\/codepen.io\/collection\/gYNRKP?sort_by=id\">this CodePen collection<\/a>), the only difference being that in this case the split line isn&#8217;t fixed, but depends on a value that changes when dragging the slider.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"639\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/4ac81ff1e5864e8e.png?resize=1024%2C639&#038;ssl=1\" alt=\"\" class=\"wp-image-3792\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/4ac81ff1e5864e8e.png?resize=1024%2C639&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/4ac81ff1e5864e8e.png?resize=300%2C187&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/4ac81ff1e5864e8e.png?resize=768%2C479&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/4ac81ff1e5864e8e.png?w=1415&amp;ssl=1 1415w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">The CodePen collection.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"tabs-transition\">Tabs Transition<\/h2>\n\n\n\n<p>This is another effect that Emil achieves by duplicating the whole navigation content.<\/p>\n\n\n\n<p>As you might suspect, we can do without duplication!<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_RwzoqWj\" src=\"\/\/codepen.io\/anon\/embed\/RwzoqWj?height=450&amp;theme-id=47434&amp;slug-hash=RwzoqWj&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed RwzoqWj\" title=\"CodePen Embed RwzoqWj\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>What is going on here?<\/p>\n\n\n\n<p>First off, each&nbsp;<code>nav<\/code>&nbsp;item has an index&nbsp;<code>--i<\/code>, whereas the&nbsp;<code>nav<\/code>&nbsp;itself has a current index&nbsp;<code>--k<\/code>, equal to the index&nbsp;<code>--i<\/code>&nbsp;of the currently selected item. The only JS necessary here is to update the value of&nbsp;<code>--k<\/code>&nbsp;on the&nbsp;<code>nav<\/code>&nbsp;to match the value&nbsp;<code>--i<\/code>&nbsp;of the item we&#8217;ve just clicked\/selected (works with&nbsp;<kbd>Tab + Enter<\/kbd>&nbsp;too).<\/p>\n\n\n\n<p>We want the currently selected item to have a highlight \u2014 that is, we want to have a highlight over the item where the difference between&nbsp;<code>--i<\/code>&nbsp;and&nbsp;<code>--k<\/code>&nbsp;is&nbsp;<code>0<\/code>. In order to know from which direction this highlight needs to move when we change the selected item and&nbsp;<code>--k<\/code>&nbsp;changes value, we need to also get the sign of this difference between&nbsp;<code>--i<\/code>&nbsp;and&nbsp;<code>--k<\/code>. Since no matter which item is selected, both&nbsp;<code>--i<\/code>&nbsp;and&nbsp;<code>--k<\/code>&nbsp;<em>are set<\/em>&nbsp;to integer values, we use this formula that I explained in detail in&nbsp;<a href=\"https:\/\/css-tricks.com\/using-absolute-value-sign-rounding-and-modulo-in-css-today\/\">an older article<\/a>&nbsp;to compute the sign:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">--sgn: clamp(-1, var(--i) - var(--k), 1)<\/pre>\n\n\n\n<p>Now, you may be wondering why in the world still use this when we now finally have the&nbsp;<code>sign()<\/code>&nbsp;function supported cross-browser (almost, it&#8217;s still behind a flag in Chrome) and the answer is that&#8230; well, we&#8217;re calling it &#8220;sign&#8221;, but that&#8217;s not exactly what we want. I said above that both&nbsp;<code>--i<\/code>&nbsp;and&nbsp;<code>--k<\/code>&nbsp;<em>are set<\/em>&nbsp;to integer values and, while that is true,&nbsp;<code>--k<\/code>&nbsp;also takes non-integer values when it smoothly transitions from the integer value it previously had to the one it\u2019s currently set to.<\/p>\n\n\n\n<p>The&nbsp;<code>sign()<\/code> function jumps from&nbsp;-1 to&nbsp;0, then to&nbsp;1 and we don\u2019t want that jump.&nbsp;We don&#8217;t want this:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/files.mastodon.social\/media_attachments\/files\/112\/943\/516\/423\/807\/249\/original\/e82954ff98549d7b.png?ssl=1\" alt=\"The graph of sign(x) has discontinuity at 0.\"\/><figcaption class=\"wp-element-caption\">sign(x) graph<\/figcaption><\/figure>\n\n\n\n<p>We want this:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/files.mastodon.social\/media_attachments\/files\/112\/943\/516\/995\/110\/082\/original\/04720938adf74ac5.png?ssl=1\" alt=\"The graph of clamp(-1, x, 1) is continuous.\"\/><figcaption class=\"wp-element-caption\">clamp(-1, x, 1) graph<\/figcaption><\/figure>\n\n\n\n<p>Next we want to know if the highlight is outside of an item or over that item, meaning that item is selected. The highlight is outside of an item if&nbsp;<code>--sgn<\/code>&nbsp;is non-zero:<\/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\">--out<\/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-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>That is, if&nbsp;<code>--sgn<\/code>&nbsp;is either&nbsp;<code>-1<\/code>&nbsp;or&nbsp;<code>1<\/code>, then&nbsp;<code>--out<\/code>&nbsp;is&nbsp;<code>1<\/code>, the highlight is outside the item, meaning the item is not selected. If&nbsp;<code>--sgn<\/code>&nbsp;is&nbsp;<code>0<\/code>, then&nbsp;<code>--out<\/code>&nbsp;is&nbsp;<code>1<\/code>, the highlight is not outside the item, but instead is over it, the item is selected.<\/p>\n\n\n\n<p>Note that with&nbsp;<code>sign()<\/code>&nbsp;and&nbsp;<code>abs()<\/code>&nbsp;being the final two mathematical functions&nbsp;<strong>still behind the Experimental Web Platform features flag<\/strong>&nbsp;in Chrome, we need to wrap the above in a <code>@supports<\/code> and also use a fallback:<\/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\">--out<\/span>: <span class=\"hljs-selector-tag\">max<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--sgn<\/span>), <span class=\"hljs-selector-tag\">-1<\/span>*<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--sgn<\/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>There&#8217;s still one more thing we need to compute here and that&#8217;s whether we need to move the highlight towards the positive direction of the&nbsp;<var>x<\/var>&nbsp;axis if we were to select an item of index&nbsp;<code>--i<\/code>&nbsp;that&#8217;s currently not selected:<\/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\">--bit<\/span>: <span class=\"hljs-selector-tag\">round<\/span>(<span class=\"hljs-selector-class\">.5<\/span>*(1 + <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--sgn<\/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>We&#8217;d need to move in the positive direction of the&nbsp;<var>x<\/var>&nbsp;axis if we were to select an item of an index&nbsp;<code>--i<\/code>&nbsp;bigger than&nbsp;<code>--k<\/code>, that is, if&nbsp;<code>--sgn<\/code>&nbsp;is&nbsp;<code>1<\/code>. In this case,&nbsp;<code>--bit<\/code>&nbsp;computes to&nbsp;<code>1<\/code>. Othwerwise, if we wouldn&#8217;t need to move in the positive direction of the&nbsp;<var>x<\/var>&nbsp;axis but in the negative one, then that means&nbsp;<code>--i<\/code>&nbsp;is smaller than&nbsp;<code>--k<\/code>, which means&nbsp;<code>--sgn<\/code>&nbsp;is&nbsp;<code>-1<\/code>, so&nbsp;<code>--bit<\/code>&nbsp;computes to&nbsp;<code>0<\/code>.<\/p>\n\n\n\n<p>We create the highlight with a pseudo-element, which is absolutely positioned to&nbsp;<a href=\"https:\/\/mastodon.social\/@anatudor\/109389255373912182\">cover its entire parent<\/a>. Okay, but we want this highlight only for the currently selected item, so we clip it to nothing for all the others, that is, for all those where&nbsp;<code>--out<\/code>&nbsp;computes to&nbsp;<code>1<\/code>.<\/p>\n\n\n\n<p>And we know whether to clip it from the right or from the left based on the&nbsp;<code>--bit<\/code>&nbsp;value. If&nbsp;<code>--bit<\/code>&nbsp;is&nbsp;<code>1<\/code>, we clip from the right (the positive direction of the&nbsp;<var>x<\/var>&nbsp;axis). Otherwise, we clip from the left. This is the&nbsp;<code>clip-path<\/code>&nbsp;and the lateral offset values:<\/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\">--r<\/span>: <span class=\"hljs-selector-tag\">calc<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--out<\/span>)*<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--bit<\/span>)*100%);\n<span class=\"hljs-selector-tag\">--l<\/span>: <span class=\"hljs-selector-tag\">calc<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--out<\/span>)*(1 <span class=\"hljs-selector-tag\">-<\/span> <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--bit<\/span>))*100%);\n<span class=\"hljs-selector-tag\">clip-path<\/span>: <span class=\"hljs-selector-tag\">inset<\/span>(0 <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--r<\/span>) 0 <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--l<\/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>The final ingredient is to&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/@property\">register<\/a>&nbsp;(as&nbsp;<code>'&lt;number&gt;'<\/code>) and transition&nbsp;<code>--k<\/code>.<\/p>\n\n\n\n<p>You can see a basic version of this below:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_poXrXqa\" src=\"\/\/codepen.io\/anon\/embed\/poXrXqa?height=450&amp;theme-id=47434&amp;slug-hash=poXrXqa&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed poXrXqa\" title=\"CodePen Embed poXrXqa\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Now you may see a teeny tiny gap in the highlight during the&nbsp;<code>transition<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/files.mastodon.social\/media_attachments\/files\/112\/944\/804\/212\/393\/307\/original\/e1c14c04ead5b5bf.png?ssl=1\" alt=\"Screenshot. Shows a tiny vertical gap during the transition.\"\/><figcaption class=\"wp-element-caption\">The problem.<\/figcaption><\/figure>\n\n\n\n<p>This is due to pixel rounding and the way to fix it is to ensure the pseudo-element highlight doesn&#8217;t have a width that might get rounded&nbsp;<em>down<\/em>&nbsp;to an integer number of pixels.<\/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\">inset<\/span>: 0 <span class=\"hljs-selector-tag\">-<\/span><span class=\"hljs-selector-class\">.5px<\/span><\/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>That&#8217;s it!<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_WNqZyeY\" src=\"\/\/codepen.io\/anon\/embed\/WNqZyeY?height=450&amp;theme-id=47434&amp;slug-hash=WNqZyeY&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed WNqZyeY\" title=\"CodePen Embed WNqZyeY\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Okay, but what about the rounded corners and the text&nbsp;<code>color<\/code>&nbsp;change on intersecting the highlight? For that, we need an SVG&nbsp;<code>filter<\/code>&nbsp;that achieves two things. One,&nbsp;<a href=\"https:\/\/mastodon.social\/@anatudor\/112523336154596358\">the blobby look<\/a>&nbsp;and two, something very similar to the one in the earlier text split example, a different look for the text where it intersects the highlight blob versus where it doesn&#8217;t. We also want to have a different look for the&nbsp;<code>:hover<\/code>\/&nbsp;<code>:focus<\/code>&nbsp;state outside the blob.<\/p>\n\n\n\n<p>Just like in the text split case, we use a separate RGB channel for each component. The red channel is used for the highlight, the blue channel for the regular text and the green channel for the text in the <code>:hover<\/code>\/<code>:focus<\/code> case.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/files.mastodon.social\/media_attachments\/files\/113\/090\/302\/854\/648\/018\/small\/fb5760a2b24ae32e.png?ssl=1\" alt=\"Screenshot. Shows the nav with different parts on different channels. Normal (not focused\/ hovered) text is on the blue channel. Hovered\/ focused text is on the blue channel. The highlight is on the red channel. Since the layers are blended using the lighten blend mode, the text intersecting the highlight looks fuchsia\/ magenta (has both the red and bluue channels maxed out).\"\/><figcaption class=\"wp-element-caption\">Intermediate\/pre-filter result.<\/figcaption><\/figure>\n\n\n\n<p>Then in the SVG&nbsp;<code>filter<\/code>, we first extract the highlight out of the red channel, paint it blue and <a href=\"https:\/\/mastodon.social\/@anatudor\/112523336154596358\">turn its shape into a blob<\/a>. Then we extract the text out of the blue and green channels and paint it either grey or blue depending on what channel it&#8217;s on. We place the blob on top of it and on top of that, we put the intersection between the text and the blob, painted in white.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"theme-switch\">Theme Switch Transition<\/h2>\n\n\n\n<p>Emil shared a version that duplicates the entire page, one version being in light mode and the other in the dark mode, with the top one having a <code>clip-path<\/code> on it.<\/p>\n\n\n\n<p>Back when&nbsp;<code>:has()<\/code>&nbsp;was still a new feature in late 2022, I started toying with a bunch theme switch effects using it and one of them produced a result very similar to this, but without duplicating any content.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_poKjWgW\" src=\"\/\/codepen.io\/anon\/embed\/poKjWgW?height=450&amp;theme-id=47434&amp;slug-hash=poKjWgW&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed poKjWgW\" title=\"CodePen Embed poKjWgW\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Let&#8217;s take a quick look at the idea behind!<\/p>\n\n\n\n<p>It&#8217;s not very far from what I&#8217;m doing in this bubble theme switch (which I&#8217;ve explained in detail in the Pen description), but it allows for more control than simply inverting what&#8217;s on the page.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_mdgjMBv\" src=\"\/\/codepen.io\/anon\/embed\/mdgjMBv?height=450&amp;theme-id=47434&amp;slug-hash=mdgjMBv&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed mdgjMBv\" title=\"CodePen Embed mdgjMBv\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>We have a custom property&nbsp;<code>--dark<\/code>&nbsp;that&#8217;s&nbsp;<code>0<\/code>&nbsp;in the light theme case and&nbsp;<code>1<\/code>&nbsp;in the dark theme case.<\/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\">body<\/span> {\n  <span class=\"hljs-attribute\">--dark<\/span>: <span class=\"hljs-number\">0<\/span>;\n \n  &amp;:has(#<span class=\"hljs-attribute\">dark<\/span>:checked) { --dark: <span class=\"hljs-number\">1<\/span> }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We make the page&nbsp;<code>background<\/code>&nbsp;a CSS gradient with&nbsp;<code>background-attachment: fixed<\/code>&nbsp;and depending on the&nbsp;<code>--dark<\/code>&nbsp;custom property via a percentage&nbsp;<code>--perc<\/code>, which we register as&nbsp;<code>'&lt;length-percentage&gt;'<\/code>&nbsp;so it can be transitioned when we switch the theme.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">body<\/span> {\n  <span class=\"hljs-comment\">\/* same as before *\/<\/span>\n  <span class=\"hljs-attribute\">--perc<\/span>: <span class=\"hljs-built_in\">calc<\/span>(var(--dark)*<span class=\"hljs-number\">100%<\/span>);\n  <span class=\"hljs-attribute\">background<\/span>: \n    <span class=\"hljs-built_in\">linear-gradient<\/span>(<span class=\"hljs-number\">90deg<\/span>, #<span class=\"hljs-number\">333<\/span> var(--perc), <span class=\"hljs-number\">#ddd<\/span> <span class=\"hljs-number\">0<\/span>) fixed;\n  <span class=\"hljs-attribute\">transition<\/span>: --perc .<span class=\"hljs-number\">65s<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We do something similar for the text itself. In this case, we also need to set the&nbsp;<code>color<\/code>&nbsp;property to&nbsp;<code>transparent<\/code>&nbsp;and clip its&nbsp;<code>background<\/code>&nbsp;to&nbsp;<code>text<\/code>. This isn&#8217;t super ideal as we can end up having to set such a&nbsp;<code>background<\/code>&nbsp;clipped to&nbsp;<code>text<\/code>&nbsp;to a lot of elements on the page, but oh, well&#8230;<\/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\">p<\/span>, <span class=\"hljs-selector-tag\">label<\/span> {\n  <span class=\"hljs-attribute\">background<\/span>: \n    <span class=\"hljs-built_in\">linear-gradient<\/span>(<span class=\"hljs-number\">90deg<\/span>, #ddd var(--perc), <span class=\"hljs-number\">#333<\/span> <span class=\"hljs-number\">0<\/span>) text fixed\n}<\/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>Click on either &#8220;dark&#8221; or &#8220;light&#8221; below:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_wvLrQpM\" src=\"\/\/codepen.io\/anon\/embed\/wvLrQpM?height=450&amp;theme-id=47434&amp;slug-hash=wvLrQpM&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed wvLrQpM\" title=\"CodePen Embed wvLrQpM\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>This is the basic idea behind. There are two big issues with it.<\/p>\n\n\n\n<p>One, the swipe direction changes (it goes from right to left) when we switch back from the dark theme to the light one. We want the swipe transition to always go from left to right. We can fix this by using an angle&nbsp;<code>--ang<\/code>&nbsp;that depends on the value of the&nbsp;<code>--dark<\/code>&nbsp;switch. This isn&#8217;t the best solution as it limits us to a linear swipe effect, but we&#8217;ll stick to it for now and come back to this problem later.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">--sign<\/span>: <span class=\"hljs-selector-tag\">sign<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--dark<\/span>) <span class=\"hljs-selector-tag\">-<\/span> <span class=\"hljs-selector-class\">.5<\/span>);\n<span class=\"hljs-selector-tag\">--ang<\/span>: <span class=\"hljs-selector-tag\">calc<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--sign<\/span>)*90<span class=\"hljs-selector-tag\">deg<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><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>To this angle <code>--ang<\/code>, we may add another one that gives us the direction of the swipe.<\/p>\n\n\n\n<p>Two, it doesn&#8217;t give any indication about whether any of these controls is focused or hovered (for example an outline) and there are no special styles for the currently selected one, but we can fix that using the&nbsp;<a href=\"https:\/\/css-tricks.com\/dry-switching-with-css-variables-the-difference-of-one-declaration\/\">DRY switching technique<\/a>&nbsp;(with&nbsp;<code>--hov<\/code>&nbsp;and&nbsp;<code>--sel<\/code>&nbsp;switches) plus&nbsp;<code>color-mix()<\/code>&nbsp;to further simplify things.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_zYVEMaa\" src=\"\/\/codepen.io\/anon\/embed\/zYVEMaa?height=450&amp;theme-id=47434&amp;slug-hash=zYVEMaa&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed zYVEMaa\" title=\"CodePen Embed zYVEMaa\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>The same idea applies to all text and links.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_XWLeOqj\" src=\"\/\/codepen.io\/anon\/embed\/XWLeOqj?height=450&amp;theme-id=47434&amp;slug-hash=XWLeOqj&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed XWLeOqj\" title=\"CodePen Embed XWLeOqj\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>There are a couple of issues here.<\/p>\n\n\n\n<p>The first is that we have these ugly edges around the link letters due to the fact that the link&nbsp;<code>background<\/code>&nbsp;clipped to&nbsp;<code>text<\/code>&nbsp;is placed on top of the paragraph&nbsp;<code>background<\/code>&nbsp;clipped to the same&nbsp;<code>text<\/code>&nbsp;and the latter contrasts with the page&nbsp;<code>background<\/code>&nbsp;even more.<\/p>\n\n\n\n<p>There are a couple of pretty straightforward fixes here. One by isolating the paragraph and then applying a&nbsp;<code>hard-light<\/code>&nbsp;blend mode on the link and another by using slightly thicker text for the link. For example, with a font like&nbsp;<a href=\"https:\/\/fonts.google.com\/specimen\/REM\">REM<\/a>, we can give the normal paragraph text a&nbsp;<code>font-weight<\/code>&nbsp;of&nbsp;<code>300<\/code>&nbsp;and the links a&nbsp;<code>font-weight<\/code>&nbsp;of&nbsp;<code>400<\/code>.<\/p>\n\n\n\n<p>The second is that when the&nbsp;<code>background<\/code>&nbsp;gets clipped to&nbsp;<code>text<\/code>, that doesn&#8217;t also include the&nbsp;<code>text-decoration<\/code>&nbsp;(underline, for example).<\/p>\n\n\n\n<p>An easy solution for this would normally be to add another&nbsp;<code>background<\/code>&nbsp;of limited vertical size at the&nbsp;<code>bottom<\/code>, but this doesn&#8217;t work here due to the&nbsp;<code>fixed<\/code>&nbsp;nature of the&nbsp;<code>background<\/code>.<\/p>\n\n\n\n<p>So we&#8217;re forced to use a pseudo and make links&nbsp;<code>inline-block<\/code>&nbsp;or wrap each link&#8217;s text content in a&nbsp;<code>span<\/code>. Each of these comes with some complications of its own, but oh, well&#8230;<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bGPoJPj\" src=\"\/\/codepen.io\/anon\/embed\/bGPoJPj?height=450&amp;theme-id=47434&amp;slug-hash=bGPoJPj&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bGPoJPj\" title=\"CodePen Embed bGPoJPj\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>In the future, being able to clip the&nbsp;<code>fixed<\/code>&nbsp;background to text&nbsp;<em>and<\/em>&nbsp;to a bottom border should do the trick without the need for the extra pseudo-element hack (see this&nbsp;<a href=\"https:\/\/verou.me\/specs\/#continuous-image-borders\">proposal<\/a>&nbsp;by Lea Verou).<\/p>\n\n\n\n<p>Also, for every element that needs to have both text content and a&nbsp;<code>background<\/code>&nbsp;(like a&nbsp;<code>button<\/code>, for example!)&#8230; some bad news! Because of a&nbsp;<a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1481498\">Firefox bug<\/a>&nbsp;old enough to go to school, we need to either make that element&nbsp;<code>inline-block<\/code>&nbsp;like we did in the links scenario and use a pseudo or wrap that text content in an inner&nbsp;<code>span<\/code>. Or, in order to avoid the problems that these two methods come with (and maybe introduce some performance ones instead), we could use an SVG&nbsp;<code>filter<\/code>. That&#8217;s pretty much what we have to do for a lot of&nbsp;<code>input<\/code>&nbsp;elements (like&nbsp;<code>input[type=button]<\/code>) anyway.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_YzormZw\" src=\"\/\/codepen.io\/anon\/embed\/YzormZw?height=450&amp;theme-id=47434&amp;slug-hash=YzormZw&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed YzormZw\" title=\"CodePen Embed YzormZw\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Okay, but what if we want to have a patterned&nbsp;<code>background<\/code>? Or what if we want a more interesting link hover effect, for example a&nbsp;<a href=\"https:\/\/codepen.io\/thebabydino\/pen\/JjGMZyM\">XOR one<\/a>? Or other kinds of XOR effects, for example one on a header? Blending (the&nbsp;<code>difference<\/code>&nbsp;blend mode in particular) to the rescue!<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ZEdXgNN\" src=\"\/\/codepen.io\/anon\/embed\/ZEdXgNN?height=450&amp;theme-id=47434&amp;slug-hash=ZEdXgNN&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ZEdXgNN\" title=\"CodePen Embed ZEdXgNN\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>What about having some gradients on the page? For example, in the case of gradient buttons. That can be done using the same trick of putting the text, the gradient of the&nbsp;<code>button<\/code>&nbsp;and the gradient determining the progress of the theme swipe transition each on a different RGB channel. Then we use an SVG&nbsp;<code>filter<\/code>&nbsp;to extract the gradients and text for each of the two themes, paint them as desired and resolve how much of each is shown based on the progress of the theme swipe transition.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_LYKOXmJ\" src=\"\/\/codepen.io\/anon\/embed\/LYKOXmJ?height=450&amp;theme-id=47434&amp;slug-hash=LYKOXmJ&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed LYKOXmJ\" title=\"CodePen Embed LYKOXmJ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>You may remember I said something about being tied to linear swipes here. But we can fix that in order to also have radial or conic ones!<\/p>\n\n\n\n<p>First, we need to have two swipe percentage values: one that changes instantly when selecting another theme and another one that transitions smoothly. We only register the second one (<code>--perc-ani<\/code>).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">--dark<\/span>: 0;\n<span class=\"hljs-selector-tag\">--perc-fix<\/span>: <span class=\"hljs-selector-tag\">calc<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--dark<\/span>)*100%);\n<span class=\"hljs-selector-tag\">--perc-ani<\/span>: <span class=\"hljs-selector-tag\">calc<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--dark<\/span>)*100%);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><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 also compute a direction or sign value, whatever you want to call it. This is&nbsp;<code>1<\/code>when we&#8217;ve switched from the light theme to the dark one (<code>--dark<\/code>&nbsp;is&nbsp;<code>1<\/code>) and&nbsp;<code>-1<\/code>&nbsp;otherwise (<code>--dark<\/code>&nbsp;got switched to&nbsp;<code>0<\/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\"><span class=\"hljs-selector-tag\">--sign<\/span>: <span class=\"hljs-selector-tag\">sign<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--dark<\/span>) <span class=\"hljs-selector-tag\">-<\/span> <span class=\"hljs-selector-class\">.5<\/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<p>Then we compute the progress of the swipe. This always smoothly goes from&nbsp;<code>0%<\/code>&nbsp;to&nbsp;<code>100%<\/code>&nbsp;over the course of the swipe that changes the theme, regardless of whether we&#8217;re switching from the light to the dark theme (<code>--sign<\/code>&nbsp;is&nbsp;<code>1<\/code>,&nbsp;<code>--perc-fix<\/code>&nbsp;has switched to&nbsp;<code>100%<\/code>&nbsp;and&nbsp;<code>--perc-ani<\/code>&nbsp;transitions from&nbsp;<code>0%<\/code>&nbsp;to&nbsp;<code>100%<\/code>) or from the dark to the light theme (<code>--sign<\/code>&nbsp;is&nbsp;<code>-1<\/code>,&nbsp;<code>--perc-fix<\/code>&nbsp;has switched to&nbsp;<code>0%<\/code>&nbsp;and&nbsp;<code>--perc-ani<\/code>&nbsp;transitions from&nbsp;<code>100%<\/code>&nbsp;to&nbsp;<code>0%<\/code>).<\/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\">--perc<\/span>: <span class=\"hljs-selector-tag\">calc<\/span>(100% <span class=\"hljs-selector-tag\">-<\/span> <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--perc-fix<\/span>) + <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--sign<\/span>)*<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--perc-ani<\/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>We then use this in the stop list for the gradient (which now can be a radial or a conic one too) transitioned to create the swipe effect:<\/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\">--list<\/span>: <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--c1<\/span>) <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--perc<\/span>), <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--c0<\/span>) 0%<\/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>The&nbsp;<code>--c0<\/code>&nbsp;and&nbsp;<code>--c1<\/code>&nbsp;values depend on whether we&#8217;re going from the light theme to the dark one (<code>--perc-fix<\/code>&nbsp;has switched to&nbsp;<code>100%<\/code>) or the other way (<code>--perc-fix<\/code>&nbsp;has switched to&nbsp;<code>0%<\/code>):<\/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\"><span class=\"hljs-selector-tag\">--c0<\/span>: <span class=\"hljs-selector-tag\">color-mix<\/span>(<span class=\"hljs-selector-tag\">in<\/span> <span class=\"hljs-selector-tag\">srgb<\/span>, <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--dark<\/span>) <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--perc-fix<\/span>), <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--light<\/span>));\n<span class=\"hljs-selector-tag\">--c1<\/span>: <span class=\"hljs-selector-tag\">color-mix<\/span>(<span class=\"hljs-selector-tag\">in<\/span> <span class=\"hljs-selector-tag\">srgb<\/span>, <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--dark<\/span>), <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--light<\/span>) <span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--perc-fix<\/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<p>And that&#8217;s it! This is the technique from my initial demo in this section which allows us to use any kind of gradient for our swipe.<\/p>\n\n\n\n<p>I hope you&#8217;ve enjoyed this little ride through the land of fun effects without content duplication!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The `clip-path` property with the `inset()` shape makes for some cool design opportunities. Here we&#8217;ll expand on some existing ideas, improving them by not requiring any content duplication.<\/p>\n","protected":false},"author":32,"featured_media":3855,"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":[216,7,226,91],"class_list":["post-3782","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-clip-path","tag-css","tag-range","tag-svg"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/09\/oNOrXQj.webp?fit=896%2C504&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/3782","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=3782"}],"version-history":[{"count":12,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/3782\/revisions"}],"predecessor-version":[{"id":3811,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/3782\/revisions\/3811"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/3855"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=3782"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=3782"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=3782"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}