<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>CSS-Tricks</title>
	<atom:link href="https://css-tricks.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://css-tricks.com</link>
	<description>Tips, Tricks, and Techniques on using Cascading Style Sheets.</description>
	<lastBuildDate>Thu, 23 Apr 2026 13:24:01 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&#038;ssl=1</url>
	<title>CSS-Tricks</title>
	<link>https://css-tricks.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">45537868</site>	<item>
		<title>Recreating Apple’s Vision Pro Animation in CSS</title>
		<link>https://css-tricks.com/recreating-apples-vision-pro-animation-in-css/</link>
					<comments>https://css-tricks.com/recreating-apples-vision-pro-animation-in-css/#respond</comments>
		
		<dc:creator><![CDATA[John Rhea]]></dc:creator>
		<pubDate>Thu, 23 Apr 2026 13:22:57 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[animation]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392970</guid>

					<description><![CDATA[<p>Putting CSS’s more recent scrolling animation capabilities to the test to recreate a complex animation of the Apple Vision Pro headset from Apple's website.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/recreating-apples-vision-pro-animation-in-css/">Recreating Apple’s Vision Pro Animation in CSS</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Apple’s product animations, particularly the scrolly teardowns (technical term), have always been inspiring. But these bleeding-edge animations have always used JavaScript and other technologies. Plus, they aren’t always responsive (or, at least, Apple switches to a static image at a certain width).</p>



<span id="more-392970"></span>



<p>I’ve been wowed by CSS’s more recent scrolling animation capabilities and wondered if I could rebuild one of these animations in just CSS <em>and</em> make it responsive. (In fact, CSS sure has come a long way since <a href="https://css-tricks.com/lets-make-one-of-those-fancy-scrolling-animations-used-on-apple-product-pages/">the last attempt in this publication</a>.) The one I’ll be attempting is from the <a href="https://www.apple.com/apple-vision-pro/" rel="noopener">Vision Pro site</a> and to see it you’ll need to scroll down until you hit a black background, a little more than halfway down the page. If you’re too <del>lazy</del> errr… efficient to go look yourself, and/or they decide to change the animation after this article goes live, you can watch this video:</p>



<figure class="wp-block-video"><video height="1836" style="aspect-ratio: 2400 / 1836;" width="2400" controls src="https://css-tricks.com/wp-content/uploads/2026/03/AppleSite.mov" playsinline></video></figure>



<p class="is-style-explanation"><strong>Note:</strong> While Apple’s version works in all major browsers, the CSS-only version, at the time of this writing, will not work in Firefox.</p>


<h3 class="wp-block-heading" id="apple-s-animation">Apple’s Animation</h3>


<p>The first thing we have to do is identify what’s going on in the original animation. There are two major stages.</p>


<h4 class="wp-block-heading" id="stage-1-exploding-hardware">Stage 1: “Exploding” Hardware</h4>


<p>Three electronic components rise in sequence from the Vision Pro device at the bottom of the page. Each of the three components is a set of two images that go both in front of and behind other components like a sub roll around a hot dog bun around a bread stick. (Yes, that’s a weird analogy, but you get it, don’t you?)</p>



<p>The first, outermost component (the sub roll) comprises the frontmost and the hindmost images allowing it to appear as if it’s both in front of and behind the other components.</p>



<p>The next component (the hot dog bun) wraps the third component (the bread stick) similarly. This provides depth, visual interest, and a 3D effect, as transparent areas in each image allow the images behind it to show through.</p>


<h4 class="wp-block-heading" id="stage-2-flip-up-to-eyepieces">Stage 2: Flip-Up to Eyepieces</h4>


<p>The final piece of the Vision Pro animation flips the device up in a smooth motion to show the eyepieces. Apple does this portion with a video, using JavaScript to advance the video as the user scrolls.</p>



<p>Let’s recreate these, one stage at a time.</p>


<h3 class="wp-block-heading" id="-exploding-hardware">“Exploding” Hardware</h3>


<p>Since Apple already created the six images for the components, we can borrow them. Initially, I started with a stack of <code>img</code> tags in a <code>div</code> and used <code>position: fixed</code> to keep the images at the bottom of the page and <code>position: absolute</code> to have them overlap each other. However, when I did this, I ran into two issues: (1) It wasn’t responsive — shrinking the width of the viewport made the images go off screen, and (2) the Vision Pro couldn’t scroll into view or scroll out of view as it does on the Apple site.</p>



<p>After banging my head against this for a bit, I went back and looked at how Apple constructed it. They had made each image a background image that was at <code>background-position: bottom center</code>, and used <code>background-size: cover</code> to keep it a consistent aspect ratio. I still needed them to be able to overlap though, but I didn’t want to pull them out of flow the way <code>position: absolute</code> does so I set <code>display:</code> <code>grid</code> on their parent element and assigned them all to the same grid area.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.visionpro { /* the overarching div that holds all the images */
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
}
.part { /* each of the images has a part class */
  grid-area: 1 / 1 / 2 / 2;
}</code></pre>



<p>As my logic professor used to say in the early aughts, <em>“Now we’re cooking with gas!”</em> (I don’t really know how that applies here, but it seemed appropriate. Somewhat illogical, I know.)</p>



<p>I then began animating the components. I started with a <a href="https://css-tricks.com/almanac/properties/s/scroll-timeline-name">scroll timeline</a> that would have allowed me to pin the animation timeline to scrolling the entire <code>html</code> element, but realized that if the Vision Pro (meaning the elements holding all of the images) was going to scroll both into and out of the viewport, then I should switch to a <a href="https://css-tricks.com/almanac/functions/v/view/">view timeline</a> so that scrolling the element into view would start the animation rather than trying to estimate a keyframe percentage to start on where the elements would be in view (a rather brittle and non-responsive way to handle it).</p>



<p>Scrolling the Vision Pro into view, pausing while it’s animating, and then scrolling it out of view is a textbook use of <code>position: sticky</code>. So I created a container <code>div</code> that fully encapsulated the Vision Pro <code>div</code> and set it to <code>position: relative</code>. I pushed the container <code>div</code> down past the viewport with a top margin, and set <code>top</code> on the vision pro <code>div</code> to 0. You could then scroll up till the <code>position: sticky</code> held the vision pro in place, the animation executed and then, when the container had been entirely scrolled through, it would carry the Vision Pro <code>div</code> up and out of the viewport.</p>



<p>Now, to tackle the component moves. When I first used a <code>translate</code> to move the images up, I had hoped to use the natural order of the elements to keep everything nicely stacked in my bread-based turducken. Alas, the browser’s sneaky optimization engine placed my sub roll entirely on top of my hot dog bun, which was entirely on top of my breadstick. Luckily, using <code>z-index</code> allowed me to separate the layers and get the overlap that is part of why Apple’s version looks so awesome.</p>



<p>Another problem I ran into was that, at sizes smaller than the 960-pixel width of the images, I couldn’t reliably and responsively move the components up. They needed to be far enough away that they didn’t interfere with Stage 2, but not so far away that they went fully out of the viewport. (Where’s a bear family and a blonde girl when you need them?) Thankfully, as it so often does, algebra saved my tuchus. Since I have the dimensions of the full-size image (960px by 608px), and the full width of the image is equal to the width of the viewport, I could write an equation like below to get the height and use that in my translation calculations for how far to move each component.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">--stage2-height: calc(min(100vw, 960px) * 608 / 960);</code></pre>



<p>However, this calculation breaks down when the viewport is shorter than <code>608px</code> and wider than <code>960px</code> because the width of the image is no longer equal to <code>100vw</code>. I initially wrote a similar equation to calculate the width:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">--stage2-width: calc(min(100vh, 608px) * 960 / 608);</code></pre>



<p>But it also only works if the height is <code>608px</code> or less, and they both won’t work while the other one applies. This would be a simple fix using an “if” statement. <a href="https://css-tricks.com/if-css-gets-inline-conditionals/">While CSS does have an <code>if()</code> function</a> as I’m writing this, it doesn’t work in Safari. While I know this whole thing won’t work in Firefox, I didn’t want to knock out a whole other browser if I could help it. So, I fixed it with a media query:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  --stage2-height: calc(min(100vw, 960px) * 608 / 960);
  --stage2-width: calc(min(100vh, 608px) * 960 / 608);
}

@media screen and (max-height: 608px) {
  :root {
    --stage2-height: calc(var(--vid-width) * 608 / 960);
  }
}</code></pre>



<p>I patted myself on the back for my mathematical genius and problem-solving skills until I realized (as you smarty pants people have probably already figured out) that if the height is less than <code>608px</code>, then it’s equal to <code>100vh</code>. (Yes, <a href="https://css-tricks.com/the-large-small-and-dynamic-viewports/"><code>vh</code> is a complicated unit</a>, particularly on iOS, but for this proof of concept I’m ignoring its downsides).</p>



<p>So, really all I needed was:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  --stage2-height: calc(min(100vw, 960px) * 608 / 960);
}

@media screen and (max-height: 608px) {
  :root {
    --stage2-height: 100vh;
  }
}</code></pre>



<p>But whatever my mathematical tangents (Ha! Terrible math pun!), this allowed me to base my vertical translations on the height of the Stage 2 graphics, e.g.:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">translate: 0 calc(var(--stage2-height) * -1 - 25vh);</code></pre>



<p>&#8230;and thus get them out of the way for the Stage 2 animation. That said, it wasn’t perfect, and at viewports narrower than <code>410px</code>, I still had to make an adjustment to the heights using a media query.</p>


<h3 class="wp-block-heading" id="flip-up-to-eyepieces">Flip-Up to Eyepieces</h3>


<p>Unfortunately, there’s no way to either start a video with just CSS or modify the frame rate with just CSS. However, we can create a set of keyframes that changes the background image over time, such as:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* ... */

50% {
  background-image: url(imgs/video/00037.jpg);
  z-index: -1;
}

51% {
  background-image: url(imgs/video/00039.jpg);
  z-index: -1;
}

52% {
  background-image: url(imgs/video/00041.jpg);
  z-index: -1;
}

/* ... */</code></pre>



<p>(Since there’s, like, 60-some images involved in this one, I’m not giving you the full set of keyframes, but you can go look at the <code>cssvideo</code> keyframes in the complete <a href="https://codepen.io/undeadinstitute/pen/bNweEOB?editors=1100" rel="noopener">CodePen</a> for the full Monty.)</p>



<p>The downside of this, however, is that instead of one video file, we’re downloading 60+ files for the same effect. You’ll notice that the file numbers skip a number between each iteration. This was me halving the number of frames so that we didn’t have 120+ images to download. (You might be able to speed things up with a sprite, but since this is more proof-of-concept than a production-ready solution, I didn’t have the patience to stitch 60+ images together).</p>



<p>The animation was a bit choppy on the initial scroll, even when running the demo locally.</p>



<figure class="wp-block-video"><video height="1806" style="aspect-ratio: 2342 / 1806;" width="2342" controls src="https://css-tricks.com/wp-content/uploads/2026/03/stutter.mov" playsinline></video></figure>



<p>So I added:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;link rel="preload" as="image" href="imgs/video/00011.jpg"></code></pre>



<p>&#8230;for every image, including the component images. That helped a lot because the server didn’t have to parse the CSS before downloading all the images.</p>



<p>Using the same view timeline as we do for Stage 1, we run an animation moving it into place and the <code>cssvideo</code> animation and the eyepieces appear to “flip up.”</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">animation: vpsf-move forwards, cssvideo forwards;
animation-timeline: --apple-vp, --apple-vp;</code></pre>


<h3 class="wp-block-heading" id="fine-tuning">Fine Tuning</h3>


<p>While a view timeline was great, the animation didn’t always begin or end exactly when I wanted it to. Enter <a href="https://css-tricks.com/almanac/properties/a/animation-range/"><code>animation-range</code></a>. While there’s a lot of options what I used on all of the <code>.part</code>s was</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">animation-range: contain cover;</code></pre>



<p>This made sure that the Vision Pro element was inside the viewport before it started (<code>contain</code>) and that it didn’t fully finish the animation until it was out of view (<code>cover</code>). This worked well for the parts because I wanted them fully in view before the components started rising and since their endpoint isn’t important they can keep moving until they’re off screen.</p>



<p>However, for Stage 2, I wanted to be sure the flip up animation had ended before it went off screen so for this one I used:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">animation-range: cover 10% contain;</code></pre>



<p>Both <code>cover</code> and <code>10%</code> refer to the start of the animation, using the <code>cover</code> keyword, but pushing its start 10% later. The <code>contain</code> ensures that the animation ends before it starts going off screen.</p>



<p>Here’s everything together:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_bNweEOB" src="//codepen.io/anon/embed/preview/bNweEOB?height=450&amp;theme-id=1&amp;slug-hash=bNweEOB&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed bNweEOB" title="CodePen Embed bNweEOB" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>And here’s a video in case your browser doesn’t support it yet:</p>



<figure class="wp-block-video"><video height="1804" style="aspect-ratio: 2342 / 1804;" width="2342" controls src="https://css-tricks.com/wp-content/uploads/2026/03/Codepenrecording.mov" playsinline></video></figure>


<h3 class="wp-block-heading" id="conclusion">Conclusion</h3>


<p>CSS sure has come a long way and while I definitely used some cutting edge features there were also a lot of relatively recent additions that made this possible too.</p>



<p>With scroll timelines, we can attach an animation to the scroll either of an entire element or just when an element is in view. The <code>animation-range</code> property let us fine-tune when the animation happened. <code>position: sticky</code> lets us easily hold something on screen while we animate it even as its scrolling. Grid layout allowed overlap elements without pulling them out of flow. Even <code>calc()</code>, viewport units, custom properties, and media queries all had their roles in making this possible. And that doesn’t even count the HTML innovations like preload. <em>Incredible!</em></p>



<p>Maybe we should add a W to WWW: The World Wide <em>Wondrous</em> Web. Okay, okay you can stop groaning, but I’m not wrong&#8230;</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/recreating-apples-vision-pro-animation-in-css/">Recreating Apple’s Vision Pro Animation in CSS</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/recreating-apples-vision-pro-animation-in-css/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/03/AppleSite.mov" length="40640388" type="video/quicktime" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/03/stutter.mov" length="14352806" type="video/quicktime" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/03/Codepenrecording.mov" length="20534354" type="video/quicktime" />

		<post-id xmlns="com-wordpress:feed-additions:1">392970</post-id>	</item>
		<item>
		<title>Enhancing Astro With a Markdown Component</title>
		<link>https://css-tricks.com/astro-markdown-component/</link>
					<comments>https://css-tricks.com/astro-markdown-component/#respond</comments>
		
		<dc:creator><![CDATA[Zell Liew]]></dc:creator>
		<pubDate>Wed, 22 Apr 2026 13:49:57 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[astro]]></category>
		<category><![CDATA[markdown]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392286</guid>

					<description><![CDATA[<p>I use a Markdown Component for two main reasons: (1) It reduces the amount of markup I need to write, and (2) it converts typographic symbols. Here's how it works.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/astro-markdown-component/">Enhancing Astro With a Markdown Component</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>There are two ways to enhance Markdown in an Astro project:</p>



<ol class="wp-block-list">
<li><a href="https://css-tricks.com/markdown-astro/">Through MDX</a></li>



<li>Through a Markdown Component</li>
</ol>



<p>This article focuses on the Markdown Component.</p>



<span id="more-392286"></span>


<h3 class="wp-block-heading" id="why-use-a-markdown-component">Why Use a Markdown Component</h3>


<p>I use a Markdown Component for two main reasons:</p>



<ol class="wp-block-list">
<li>It reduces the amount of markup I need to write.</li>



<li>It converts typographic symbols like <code>'</code> to opening or closing quotes (<code>'</code> or <code>'</code>).</li>
</ol>



<p>So, I can skip several HTML tags — like <code>&lt;p&gt;</code>, <code>&lt;strong&gt;</code>, <code>&lt;em&gt;</code>, <code>&lt;ul&gt;</code>, <code>&lt;ol&gt;</code>, <code>&lt;li&gt;</code>, and <code>&lt;a&gt;</code>. I can also skip heading tags if I don&#8217;t need to add classes to them.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="card">
  &lt;!-- prettier-ignore -->
  &lt;Markdown>
    ## Card Title
    This is a paragraph with **strong** and *italic* text.
    This is the second paragraph with a [link](https://link-somewhere.com)

    - List
    - Of
    - Items
  &lt;/Markdown>
&lt;/div></code></pre>



<p>Notice the <code>prettier-ignore</code> comment? It tells prettier not to format the contents within the <code>&lt;Markdown&gt;</code> block so Prettier won&#8217;t mess up my Markdown content.</p>



<p>The HTML output will be as follows:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="card">
  &lt;h2> Card Title &lt;/h2>
  &lt;p>This is a paragraph with &lt;strong>strong&lt;/strong> and &lt;em>italic&lt;/em> text.&lt;/p>
  &lt;p>This is the second paragraph with a &lt;a href="https://link-somewhere.com">link&lt;/a>&lt;/p>

  &lt;ul>
    &lt;li> List &lt;/li>
    &lt;li> Of &lt;/li>
    &lt;li> Items &lt;/li>
  &lt;/ul>
&lt;/div></code></pre>


<h3 class="wp-block-heading" id="installing-the-markdown-component">Installing the Markdown Component</h3>


<p>Fun Fact: Astro came with a <code>&lt;Markdown&gt;</code> component in its early release, but this <code>&lt;Markdown&gt;</code> component was <a href="https://docs.astro.build/en/guides/upgrade-to/v1/#markdown--component-removed" rel="noopener">migrated to a separate plugin in Version 1</a>, and <a href="https://docs.astro.build/en/guides/upgrade-to/v3/#removed-markdown--component" rel="noopener">completely removed in version 3</a>.</p>



<p>I was upset about it. But I decided to build a Markdown component for myself since I liked using one. <a href="https://splendidlabz.com/docs/astro/components/markdown/#features" rel="noopener">You can the documentation here</a>.</p>



<p>Using the Markdown component is simple: Just import and use it in the way I showed you above.</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">---
import { Markdown } from '@splendidlabz/astro'
---

&lt;Markdown>
  ...
&lt;/Markdown></code></pre>


<h3 class="wp-block-heading" id="respects-indentation-automatically">Respects Indentation Automatically</h3>


<p>You can write your Markdown naturally, as if you&#8217;re writing content normally. This Markdown component detects the indentation and outputs the correct values (without wrapping them in <code>&lt;pre&gt;</code> and <code>&lt;code&gt;</code> tags).</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div>
  &lt;div>
    &lt;!-- prettier-ignore -->
    &lt;Markdown>
      This is a paragraph

      This is a second paragraph
    &lt;/Markdown>
  &lt;/div>
&lt;/div></code></pre>



<p>Here&#8217;s the output:</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div>
  &lt;div>
    &lt;p>This is a paragraph&lt;/p>
    &lt;p>This is a second paragraph&lt;/p>
  &lt;/div>
&lt;/div></code></pre>


<h3 class="wp-block-heading" id="inline-option">Inline Option</h3>


<p>There&#8217;s an <code>inline</code> option that tells the <code>&lt;Markdown&gt;</code> component not to generate paragraph tags.</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;h2 class="max-w-[12em]">
  &lt;Markdown inline> Ain't this cool? &lt;/Markdown>
&lt;/h2></code></pre>



<p>Here&#8217;s the output:</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;h2 class="max-w-[12em]">
  Ain't this cool?
&lt;/h2></code></pre>


<h3 class="wp-block-heading" id="gotchas-and-caveats">Gotchas and Caveats</h3>


<p>Prettier messes up the <code>&lt;!-- prettier-ignore --&gt;</code> block if you have unicode characters like emojis and em dashes anywhere before the block.</p>



<p>Here&#8217;s the original code:</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- prettier-ignore -->
&lt;Markdown>
  Markdown block that contains Unicode characters &#x1f917;
&lt;/Markdown>

&lt;!-- prettier-ignore -->
&lt;Markdown>
  Second Markdown block.
&lt;/Markdown></code></pre>



<p>Here&#8217;s what it looks like after saving:</p>



<pre rel="Markdown" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- prettier-ignore -->
&lt;Markdown>
  Markdown block that contains unicode characters &#x1f917;
&lt;/Markdown>

&lt;!-- prettier-ignore -->
&lt;Markdown>
  Second Markdown block.
&lt;/Markdown></code></pre>



<p>Unfortunately, we can&#8217;t do much about emojis because the issue stems from Prettier&#8217;s formatter.</p>



<p>But, we can use <code>en</code> and <code>em</code> dashes by writing <code>--</code> and <code>---</code>, respectively.</p>


<h3 class="wp-block-heading" id="content-workaround"><strong>Content Workaround</strong></h3>


<p>You can prevent Prettier from breaking all those <code>&lt;!-- prettier-ignore --&gt;</code> comments by not using them!</p>



<p>To do this, you just put your content inside a <code>content</code> property. No need to worry about whitespace as well — that&#8217;s taken care of for you.</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">&lt;Markdown content=`
  This is a paragraph

  This is another paragraph
`/></code></pre>



<p>Personally, I think it doesn’t look at nice as slot version above&#8230;</p>



<p>But it lets you use markdown directly with any JS or json content you load!</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">---
const content = `
  This is a paragraph

  This is another paragraph
`
---

&lt;Markdown {content} /></code></pre>


<h3 class="wp-block-heading" id="taking-it-further">Taking it Further</h3>


<p>I&#8217;ve been building with Astro for 3+ years, and I kept running into the same friction points on content-heavy sites: blog pages, tag pages, pagination, and folder structures that get messy over time.</p>



<p>So I built <a href="https://zellwk.com/courses/practical-astro/content-systems/" rel="noopener">Practical Astro: Content Systems</a> — 7 ready-to-use solutions for Astro content workflows (MDX is just one of them). You get both the code and the thinking behind it.</p>



<p>If you want a cleaner, calmer content workflow, check it out.</p>



<p>I also write about Astro Patterns and Using Tailwind + CSS together on <a href="https://zellwk.com/newsletter/css-tricks/" rel="noopener">my blog</a>. Come by and say hi!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/astro-markdown-component/">Enhancing Astro With a Markdown Component</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/astro-markdown-component/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392286</post-id>	</item>
		<item>
		<title>Markdown + Astro = &#x2764;&#xfe0f;</title>
		<link>https://css-tricks.com/markdown-astro/</link>
					<comments>https://css-tricks.com/markdown-astro/#respond</comments>
		
		<dc:creator><![CDATA[Zell Liew]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 13:55:16 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[astro]]></category>
		<category><![CDATA[markdown]]></category>
		<category><![CDATA[mdx]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392278</guid>

					<description><![CDATA[<p>Although Astro has built-in support for Markdown via .md files, I'd argue that your Markdown experience can be enhanced with MDX.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/markdown-astro/">Markdown + Astro = &#x2764;&#xfe0f;</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Markdown is a great invention that lets us write less markup. It also handles typographical matters like converting straight apostrophes (<code>'</code>) to opening or closing quotes (<code>'</code> or <code>'</code>) for us.</p>



<p>Although Astro has built-in support for Markdown via <code>.md</code> files, I&#8217;d argue that your Markdown experience can be enhanced in two ways:</p>



<ol class="wp-block-list">
<li>MDX</li>



<li>Markdown Component</li>
</ol>



<p>I&#8217;ve cover these in depth in <a href="https://zellwk.com/courses/practical-astro/content-systems/" rel="noopener">Practical Astro: Content Systems</a>.</p>



<p>We&#8217;re going to focus on MDX today.</p>



<span id="more-392278"></span>


<h3 class="wp-block-heading" id="mdx">MDX</h3>


<p><a href="https://mdxjs.com" rel="noopener">MDX</a> is a superset of Markdown. <strong>It lets you use components in Markdown</strong> and simple JSX in addition to all other Markdown features.</p>



<p>For Astro, you can also use components from any frontend framework that you have installed. So you can do something like:</p>



<pre rel="MDX" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">---
# Frontmatter...
---

import AstroComp from '@/components/AstroComp.astro'
import SvelteComp from '@/components/AstroComp.astro'

&lt;AstroComp> ... &lt;/AstroComp>
&lt;SvelteComp> ... &lt;/SvelteComp></code></pre>



<p>It can be a great substitute for content-heavy stuff because it lets you write markup like the following.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="card">
  ### Card Title

  Content goes here

  - List
  - Of
  - Items

  Second paragraph
&lt;/div></code></pre>



<p>Astro will convert the MDX into the following HTML:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="card">
  &lt;h2>Card Title&lt;/h2>

  &lt;p>Content goes here &lt;/p>

  &lt;ul>
    &lt;li> List &lt;/li>
    &lt;li> Of &lt;/li>
    &lt;li> Items &lt;/li>
  &lt;/ul>

  &lt;p>Second paragraph&lt;/p>
&lt;/div></code></pre>



<p>Notice what I did above:</p>



<ul class="wp-block-list">
<li>I used <code>##</code> instead of a full <code>h2</code> tag.</li>



<li>I used <code>-</code> instead of <code>&lt;ul></code> and <code>&lt;li></code> to denote lists.</li>



<li>I didn’t need any paragraph tags.</li>
</ul>



<p>Writing the whole thing in HTML directly would have been somewhat of a pain.</p>


<h3 class="wp-block-heading" id="installing-mdx">Installing MDX</h3>


<p>Astro folks have built an integration for MDX so it&#8217;s easy-peasy to add it to your project. Just follow <a href="https://docs.astro.build/en/guides/integrations-guide/mdx/#installation" rel="noopener">these instructions</a>.</p>


<h3 class="wp-block-heading" id="three-main-ways-to-use-mdx">Three Main Ways to Use MDX</h3>


<p>These methods also work with standard Markdown files.</p>



<ol class="wp-block-list">
<li>Import it directly into an Astro file</li>



<li>Through content collections</li>



<li>Through a layout</li>
</ol>


<h4 class="wp-block-heading" id="import-it-directly">Import it Directly</h4>


<p>The first way is simply to import your MDX file and use it directly as a component.</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">---
import MDXComp from '../components/MDXComp.mdx'
---

&lt;MDXComp /></code></pre>



<p>Because of this, MDX can kinda function like a partial.</p>


<h4 class="wp-block-heading" id="through-content-collections">Through Content Collections</h4>


<p>First, you feed your MDX into a content collection. Note that you have to add the <code>mdx</code> pattern to your glob here.</p>


<h4 class="wp-block-heading" id="import-it-directly">Import it directly</h4>


<p>The first way is simply to import your MDX file and use it directly as a component.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// src/content.config.js
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/blog" }),
});

export const collections = { blog };</code></pre>



<p>Then you retrieve the MDX file from the content collection.</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">---
import { getEntry, render } from 'astro:content'
const { slug } = Astro.props
const post = await getEntry('blog', slug)
const { Content } = await render(post)
---

&lt;Content /></code></pre>



<p>As you&#8217;re doing this, <strong>you can pass components into the MDX files</strong> so you don&#8217;t have to import them individually in every file.</p>



<p>For example, here&#8217;s how I would pass the <code><a href="https://splendidlabz.com/docs/astro/components/image/" rel="noopener">Image</a></code> component from <a href="https://splendidlabz.com" rel="noopener">Splendid Labz</a> into each of my MDX files.</p>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">---
import { Image } from '@splendidlabz/astro'
// ...
const { Content } = await render(post)
const components = { Image }
---

&lt;Content {components} /></code></pre>



<p>In my MDX files, I can now use <code>Image</code> without importing it.</p>



<pre rel="MDX" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;Image src="..." alt="..." /></code></pre>


<h3 class="wp-block-heading" id="use-a-layout">Use a Layout</h3>


<p>Finally, you can add a layout frontmatter in the MDX file.</p>



<pre rel="MDX" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">---
title: Blog Post Title
layout: @/layouts/MDX.astro
---</code></pre>



<p>This <code>layout</code> frontmatter should point to an Astro file.</p>



<p>In that file:</p>



<ul class="wp-block-list">
<li>You can extract frontmatter properties from <code>Astro.props.content</code>.</li>



<li>The MDX content can be rendered with <code>&lt;slot></code>.</li>
</ul>



<pre rel="Astro" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">---
import Base from './Base.astro'
const props = Astro.props.content
const { title } = props
---

&lt;Base>
  &lt;h1>{title}&lt;/h1>
  &lt;slot />
&lt;/Base></code></pre>


<h3 class="wp-block-heading" id="caveats">Caveats</h3>

<h4 class="wp-block-heading" id="formatting-and-linting-fails">Formatting and Linting Fails</h4>


<p>ESLint and Prettier don&#8217;t format MDX files well, so you&#8217;ll end up manually indenting most of your markup.</p>



<p>This is fine for small amounts of markup. But if you have lots of them&#8230; then the Markdown Component will be a much better choice.</p>



<p>More on that in another upcoming post.</p>


<h4 class="wp-block-heading" id="rss-issues">RSS Issues</h4>


<p>The <a href="https://docs.astro.build/en/recipes/rss/" rel="noopener">Astro RSS integration</a> doesn&#8217;t support MDX files out of the box.</p>



<p>Thankfully, this can be handled easily with Astro containers. I&#8217;ll show you how to do this in <a href="https://zellwk.com/courses/practical-astro/" rel="noopener">Practical Astro</a>.</p>


<h3 class="wp-block-heading" id="taking-it-further">Taking it Further</h3>


<p>I&#8217;ve been building with Astro for 3+ years, and I kept running into the same friction points on content-heavy sites: blog pages, tag pages, pagination, and folder structures that get messy over time.</p>



<p>So I built <a href="https://zellwk.com/courses/practical-astro/content-systems/" rel="noopener">Practical Astro: Content Systems</a>, 7 ready-to-use solutions for Astro content workflows (MDX is just one of them). You get both the code and the thinking behind it.</p>



<p>If you want a cleaner, calmer content workflow, check it out.</p>



<p>I also write about Astro Patterns and Using Tailwind + CSS together on <a href="https://zellwk.com/newsletters/css-tricks/" rel="noopener">my blog</a>. Come by and say hi!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/markdown-astro/">Markdown + Astro = &#x2764;&#xfe0f;</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/markdown-astro/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392278</post-id>	</item>
		<item>
		<title>What’s !important #9: clip-path Jigsaws, View Transitions Toolkit, Name-only Containers, and More</title>
		<link>https://css-tricks.com/whats-important-9/</link>
					<comments>https://css-tricks.com/whats-important-9/#respond</comments>
		
		<dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
		<pubDate>Fri, 17 Apr 2026 14:00:41 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[news]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393515</guid>

					<description><![CDATA[<p>This issue of What’s !important brings you clip-path jigsaws, a view transitions toolkit, name-only containers, the usual roundup of new, notable web platform features, and more.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-9/">What’s !important #9: clip-path Jigsaws, View Transitions Toolkit, Name-only Containers, and More</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>This issue of <strong>What’s !important</strong> brings you <code>clip-path</code> jigsaws, a view transitions toolkit, name-only containers, the usual roundup of new, notable web platform features, and more.</p>



<span id="more-393515"></span>


<h3 class="wp-block-heading" id="creating-a-jigsaw-puzzle-using-clippath">Creating a jigsaw puzzle using <code>clip-path</code></h3>


<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_yyazxZv/f10deb6b871f36edff1c2a225f974676" src="//codepen.io/anon/embed/yyazxZv/f10deb6b871f36edff1c2a225f974676?height=450&amp;theme-id=1&amp;slug-hash=yyazxZv/f10deb6b871f36edff1c2a225f974676&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed yyazxZv/f10deb6b871f36edff1c2a225f974676" title="CodePen Embed yyazxZv/f10deb6b871f36edff1c2a225f974676" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p><a href="https://css-tricks.com/author/amitsheen/">Amit Sheen</a> demonstrated <a href="https://frontendmasters.com/blog/creating-puzzle-peices-in-css/" rel="noopener">how to create a full jigsaw puzzle using <code>clip-path</code></a>. While I doubt that you’ll need to create a jigsaw puzzle anytime soon, Amit’s walkthrough offers a fantastic way to acquaint yourself with this evolving CSS property that’s becoming more and more popular every day.</p>



<p>For example, Chrome Canary shipped <a href="https://bsky.app/profile/yisibl.bsky.social/post/3mj26ecigjk2x" rel="noopener">rounded <code>clip-path</code> polygons</a> only last week:</p>



<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:qgw47oogw2rjbhs2or2pcq37/app.bsky.feed.post/3mj26ecigjk2x" data-bluesky-cid="bafyreifwoqpmm4niuyjnokdlqg4zlcob7xcjkzynpvyhf75uccy26jy4ga" data-bluesky-embed-color-mode="system"><p lang="zh">I and Jason are currently working on implementing the CSS `polygon() round` keyword in Chrome. 

This is one of my favorite CSS features! Thanks to @lea.verou.me for bringing it to CSS.

Enable the `enable-experimental-web-platform-features` flag in Chrome Canary
codepen.io/yisi/pen/NPR&#8230;<br><br><a href="https://bsky.app/profile/did:plc:qgw47oogw2rjbhs2or2pcq37/post/3mj26ecigjk2x?ref_src=embed" rel="noopener">[image or embed]</a></p>&mdash; yisibl.bsky.social (<a href="https://bsky.app/profile/did:plc:qgw47oogw2rjbhs2or2pcq37?ref_src=embed" rel="noopener">@yisibl.bsky.social</a>) <a href="https://bsky.app/profile/did:plc:qgw47oogw2rjbhs2or2pcq37/post/3mj26ecigjk2x?ref_src=embed" rel="noopener">Apr 9, 2026 at 7:25</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>



<p>And there’s also <a href="https://github.com/w3c/csswg-drafts/issues/8946" rel="noopener">talk of implementing other <code>corner-shape</code> keywords</a> such as <code>bevel</code>, too.</p>



<p>Finally, since we’re on the topic, and because I somehow completely missed it for <strong><a href="https://css-tricks.com/whats-important-8/">What’s !important #8</a></strong>, here’s <a href="https://karlkoch.me/writing/on-clip-path-animations" rel="noopener">Karl Koch demonstrating some really neat <code>clip-path</code> animations</a>.</p>



<figure class="wp-block-video"><video height="1006" style="aspect-ratio: 1384 / 1006;" width="1384" controls loop muted src="https://css-tricks.com/wp-content/uploads/2026/04/2.mp4"></video></figure>



<p><em>Get clippin’!</em></p>


<h3 class="wp-block-heading" id="view-transitions-toolkit">View transitions toolkit</h3>


<p>The Chrome DevRel team created a <a href="https://chrome.dev/view-transitions-toolkit/" rel="noopener">view transitions toolkit</a>, a collection of utilities that make working with view transitions a bit easier.</p>



<p>Here’s my favorite demo from the site:</p>



<figure class="wp-block-video"><video height="1844" style="aspect-ratio: 2940 / 1844;" width="2940" controls loop muted src="https://css-tricks.com/wp-content/uploads/2026/04/1.mp4"></video></figure>



<p><a href="https://developer.chrome.com/blog/element-scoped-view-transitions" rel="noopener">Chrome shipped element-scoped view transitions</a> only last month, so there’s no better time to dive into this toolkit.</p>


<h3 class="wp-block-heading" id="how-nameonly-containers-can-be-used-for-scoping">How name-only containers can be used for scoping</h3>


<p>Chris Coyier discussed the use of <a href="https://frontendmasters.com/blog/name-only-containers-the-scoping-we-needed/" rel="noopener">name-only containers for scoping</a>, and how they compare to class names and <code>@scope</code>. Personally, I prefer <code>@scope</code> because it tends to result in cleaner HTML, and it seems that Chris has updated his stance to be more <code>@scope</code>-aligned too, but it really comes down to personal preference. What’s your take on it?</p>


<h3 class="wp-block-heading" id="hey-remember-subgrid">Hey, remember subgrid?</h3>


<p>At one point, <a href="https://css-tricks.com/complete-guide-css-grid-layout/#subgrid">subgrid</a> was one of the most highly-anticipated CSS features, but it’s been two and half years since it became Baseline Newly Available, and it’s barely made a dent in the CSS landscape. This is a shame, because subgrid can help us to break out of grids properly and avoid the ‘ol Michael Scofield/nested wrappers/negative margins extravaganza.</p>



<p>But don’t worry, David Bushell’s <a href="https://dbushell.com/2026/04/02/css-subgrid-is-super-good/" rel="noopener">very simple explanation of subgrid</a> has you covered.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" fetchpriority="high" decoding="async" width="1024" height="686" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/3-1024x686.jpeg?resize=1024%2C686&#038;ssl=1" alt="A subgrid-powered web layout featuring Lorem Ipsum placeholder text and some images. Red vertical alignment markers depict the grid columns." class="wp-image-393518" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/3.jpeg?resize=1024%2C686&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/3.jpeg?resize=300%2C201&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/3.jpeg?resize=768%2C514&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/3.jpeg?resize=1536%2C1029&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/3.jpeg?w=1998&amp;ssl=1 1998w" sizes="(min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://dbushell.com/2026/04/02/css-subgrid-is-super-good/" rel="noopener">David Bushell</a> (although the red grid lines were added by me).</figcaption></figure>


<h3 class="wp-block-heading" id="you-might-not-needjavascript">You Might Not Need…<em>JavaScript?</em></h3>


<p>Remember <a href="https://youmightnotneedjquery.com/" rel="noopener">You Might Not Need jQuery</a>? Pavel Laptev’s <a href="https://blog.gitbutler.com/the-great-css-expansion" rel="noopener">The Great CSS Expansion</a> has a similar vibe, noting CSS alternatives to JavaScript libraries (and JavaScript in general) that are smaller and more performant.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" decoding="async" width="1024" height="642" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/4-1024x642.png?resize=1024%2C642&#038;ssl=1" alt="A screenshot of a technical article featuring the Anchor Positioning heading, a comparison table of JavaScript libraries for anchor positioning, and a CSS code example." class="wp-image-393519" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/4-scaled.png?resize=1024%2C642&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/4-scaled.png?resize=300%2C188&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/4-scaled.png?resize=768%2C481&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/4-scaled.png?resize=1536%2C962&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/4-scaled.png?resize=2048%2C1283&amp;ssl=1 2048w" sizes="(min-width: 735px) 864px, 96vw" /></figure>


<h3 class="wp-block-heading" id="missed-hits">Missed hits</h3>


<ul class="wp-block-list">
<li><a href="https://developer.chrome.com/release-notes/147" rel="noopener">Chrome 147</a>
<ul class="wp-block-list">
<li><a href="https://css-tricks.com/exploring-the-css-contrast-color-function-a-second-time/"><code>contrast-color()</code></a> (now baseline)</li>



<li><a href="https://una.im/border-shape" rel="noopener"><code>border-shape</code></a> (no Safari or Firefox support)</li>



<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/CSSPseudoElement" rel="noopener">CSSPseudoElement</a> JavaScript interface (no Safari or Firefox support)</li>



<li><code>scroll</code> range for view timelines (no Safari or Firefox support)</li>



<li>Element-scoped view transitions, as mentioned earlier (no Safari or Firefox support)</li>
</ul>
</li>



<li><a href="https://developer.apple.com/documentation/safari-technology-preview-release-notes/stp-release-240" rel="noopener">Safari TP 240</a>
<ul class="wp-block-list">
<li><code>revert-rule</code> keyword (already supported by Chrome and Firefox)</li>
</ul>
</li>



<li><a href="https://developer.apple.com/documentation/safari-technology-preview-release-notes/stp-release-241" rel="noopener">Safari TP 241</a>
<ul class="wp-block-list">
<li><a href="https://css-tricks.com/almanac/properties/o/overflow-anchor/"><code>overflow-anchor</code></a> (already supported by Chrome and Firefox)</li>



<li><a href="https://css-tricks.com/we-completely-missed-width-height-stretch/"><code>stretch</code></a> (already supported by Chrome)</li>
</ul>
</li>
</ul>



<p>It’s becoming increasingly difficult to keep up with all of these new CSS features. I attempted way too many rounds of Keith Cirkel’s new <a href="https://www.keithcirkel.co.uk/css-or-bs/" rel="noopener">CSS or BS? quiz</a>, and my best score was only 18/20. Sad times. Let me know your score in the comments (<em>unless it’s higher than mine…</em>).</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" decoding="async" width="1024" height="642" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/5-1024x642.png?resize=1024%2C642&#038;ssl=1" alt="A screenshot from an online quiz titled CSS or BS? showing the CSS property font-synthesis in a speech bubble, with buttons to select whether the property is real or fake." class="wp-image-393520" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/5-scaled.png?resize=1024%2C642&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/5-scaled.png?resize=300%2C188&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/5-scaled.png?resize=768%2C481&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/5-scaled.png?resize=1536%2C962&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/5-scaled.png?resize=2048%2C1283&amp;ssl=1 2048w" sizes="(min-width: 735px) 864px, 96vw" /></figure>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-9/">What’s !important #9: clip-path Jigsaws, View Transitions Toolkit, Name-only Containers, and More</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/whats-important-9/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/2.mp4" length="219737" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/04/1.mp4" length="11393992" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">393515</post-id>	</item>
		<item>
		<title>A Well-Designed JavaScript Module System is Your First Architecture Decision</title>
		<link>https://css-tricks.com/the-javascript-module-system-architecture/</link>
					<comments>https://css-tricks.com/the-javascript-module-system-architecture/#comments</comments>
		
		<dc:creator><![CDATA[Amejimaobari Victor]]></dc:creator>
		<pubDate>Thu, 16 Apr 2026 13:53:58 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[architecture]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[javascript modules]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392882</guid>

					<description><![CDATA[<p>Behind every technology, there should be a guide for its use. While JavaScript modules make it easier to write “big” programs, if there are no principles or systems for using them, things could easily become difficult to maintain.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-javascript-module-system-architecture/">A Well-Designed JavaScript Module System is Your First Architecture Decision</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Writing large programs in JavaScript without modules would be pretty difficult. Imagine you only have the global scope to work with. This was the situation in JavaScript before modules. Scripts attached to the DOM were prone to overwriting each other and variable name conflicts.</p>



<p>With JavaScript modules, you have the ability to create private scopes for your code, and also explicitly state which parts of your code should be globally accessible.</p>



<span id="more-392882"></span>



<p>JavaScript modules are not just a way of splitting code across files, but mainly a way to design boundaries between parts of your system.</p>



<p>Behind every technology, there should be a guide for its use. While JavaScript modules make it easier to write “big” programs, if there are no principles or systems for using them, things could easily become difficult to maintain.</p>


<h3 class="wp-block-heading" id="how-esm-traded-flexibility-for-analyzability-">How ESM Traded Flexibility For “Analyzability”</h3>


<p>The two module systems in JavaScript are CommonJS (CJS) and ECMAScript Modules (ESM).</p>



<p>The CommonJS module system was the first JavaScript module system. It was created to be compatible with server-side JavaScript, and as such, its syntax (<code>require()</code>, <code>module.exports</code>, etc.) was not natively supported by browsers.</p>



<p>The import mechanism for CommonJS relies on the <code>require()</code> function, and being a function, it is not restricted to being called at the top of a module; it can also be called in an <code>if</code> statement or even a loop.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// CommonJS — require() is a function call, can appear anywhere
const module = require('./module')

// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
  const logger = require('./productionLogger')
}

// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`)</code></pre>



<p>The same cannot be said for ESM: the import statement has to be at the top. Anything else is regarded as an invalid syntax.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// ESM — import is a declaration, not a function call
import { formatDate } from './formatters'

// invalid ESM — imports must be at the top level, not conditional
if (process.env.NODE_ENV === 'production') {
  import { logger } from './productionLogger' // SyntaxError
}

// the path must be a static string — no dynamic resolution
import { plugin } from `./plugins/${pluginName}` // SyntaxError: : template literals are dynamic paths</code></pre>



<p>You can see that CommonJS gives you more flexibility than ESM. But if ESM was created after CommonJS, why wasn’t this flexibility implemented in ESM too, and how does it affect your code?</p>



<p>The answer comes down to static analysis and tree-shaking. With CommonJS, static tools cannot determine which modules are needed for your program to run in order to remove the ones that aren’t needed. And when a bundler is not sure whether a module is needed or not, it includes it by default. The way CommonJS is defined, modules that depend on each other can only be known at runtime.</p>



<p><strong>ESM was designed to fix this.</strong> By making sure the position of import statements is restricted to the top of the file and that paths are static string literals, static tools can better understand the structure of the dependencies in the code and eliminate the modules that aren’t needed, which in turn, makes bundle sizes smaller.</p>


<h3 class="wp-block-heading" id="why-modules-are-an-architectural-decision">Why Modules Are An Architectural Decision</h3>


<p>Whether you are aware of it or not, every time you create, import, or export modules, you are shaping the structure of your application. This is because modules are the basic building blocks of a project architecture, and the interaction between these modules is what makes an application functional and useful.</p>



<p>The organization of modules defines boundaries, shapes the flow of your dependencies, and even mirrors your team&#8217;s organizational structure. The way you manage the modules in your project can either make or break your project.</p>


<h3 class="wp-block-heading" id="the-dependency-rule-for-clean-architecture">The Dependency Rule For Clean Architecture</h3>


<p>There are so many ways to structure a project, and there is no one-size-fits-all method to organize every project.</p>



<p><strong>Clean architecture</strong> is a controversial methodology and not every team should adopt it. <a href="https://threedots.tech/episode/is-clean-architecture-overengineering/" rel="noopener">It might even be over-engineering</a>, especially smaller projects. However, if you don’t have a strict option for structuring a project, then the clean architecture approach could be a good place to start.</p>



<p>According to <a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" rel="noopener">Robert Martin’s dependency rule</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>“Nothing in an inner circle can know anything at all about something in an outer circle.”</p>
<cite>Robert C. Martin</cite></blockquote>



<p>Based on this rule, an application should be structured in different layers, where the business logic is the application’s core and the technologies for building the application are positioned at the outermost layer. The interface adapters and business rules come in between.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="2560" height="657" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772462582173_Untitled-scaled.png?resize=2560%2C657&#038;ssl=1" alt="A javascript module linear flow diagram going from frameworks to interface adapters, to use cases to entities." class="wp-image-392885" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772462582173_Untitled-scaled.png?w=2560&amp;ssl=1 2560w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772462582173_Untitled-scaled.png?resize=300%2C77&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772462582173_Untitled-scaled.png?resize=1024%2C263&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772462582173_Untitled-scaled.png?resize=768%2C197&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772462582173_Untitled-scaled.png?resize=1536%2C394&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772462582173_Untitled-scaled.png?resize=2048%2C525&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">A simplified representation of the clean architecture concentric circles diagram</figcaption></figure>



<p>From the diagram, the first block represents the outer circle and the last block represents the inner circle. The arrows show which layer depends on the other, and the direction of dependencies flow towards the inner circle. This means that the framework and drivers can depend on the interface adapters, and the interface adapters can depend on the use cases layer, and the use cases layer can depend on the entities. Dependencies must point inward and not outward.</p>



<p>So, based on this rule, the business logic layer should not know anything at all about the technologies used in building the application — which is a good thing because technologies are more volatile than business logic, and you don’t want your business logic to be affected every time you have to update your tech stack. You should build your project around your business logic and not around your tech stack.</p>



<p>Without a proper rule, you are probably freely importing modules from anywhere in your project, and as your project grows, it becomes increasingly difficult to make changes. You’ll eventually have to refactor your code in order to properly maintain your project in the future.</p>


<h3 class="wp-block-heading" id="what-your-module-graph-means-architecturally">What Your Module Graph Means Architecturally</h3>


<p>One tool that can help you maintain good project architecture is the module graph. A <strong>module graph</strong> is a type of dependency flow that shows how different modules in a project rely on each other. Each time you make imports, you are shaping the dependency graph of your project.</p>



<p>A healthy dependency graph could look like this:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1202" height="519" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/clean-architecture.webp?resize=1202%2C519&#038;ssl=1" alt="Diagram of a javascript module clean architecture based on express.js demonstrating dependencies that flow in a single direction." class="wp-image-392890" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/clean-architecture.webp?w=1202&amp;ssl=1 1202w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/clean-architecture.webp?resize=300%2C130&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/clean-architecture.webp?resize=1024%2C442&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/clean-architecture.webp?resize=768%2C332&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Generated with&nbsp;<a href="https://github.com/pahen/madge" target="_blank" rel="noreferrer noopener">Madge</a>&nbsp;and&nbsp;<a href="https://graphviz.org/" target="_blank" rel="noreferrer noopener">Graphviz</a>.</figcaption></figure>



<p>From the graph, you can see dependencies flowing in one direction (following the dependency rule), where high-level modules depend on low-level ones, and never the other way around.</p>



<p>Conversely, this is what an unhealthy one might look like:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="686" height="413" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772805778054_Untitled21.png?resize=686%2C413&#038;ssl=1" alt="A more complex javascript module flow diagram showing how smaller dependencies only rely on larger dependencies, all the way to the end of the flow at which the smallest items circle back to the largest dependency." class="wp-image-392883" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772805778054_Untitled21.png?w=686&amp;ssl=1 686w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_438F18945EAD505ECD4EDF4C4D7332DB9EE1178AECF38D5E1E1966514E384E9B_1772805778054_Untitled21.png?resize=300%2C181&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">I couldn&#8217;t find a project with an unhealthy dependency graph, so I had to modify the Express.js dependency graph above to make it look unhealthy for this example.</figcaption></figure>



<p>From the above graph above, you can see that <code>utils.js</code> is no longer a dependency of <code>response.js</code> and <code>application.js</code> as we would find in a healthy graph, but is also dependent on <code>request.js</code> and <code>view.js</code>. This level of dependence on <code>utils.js</code> increases the blast radius if anything goes wrong with it. And it also makes it harder to run tests on the module.</p>



<p>Yet another issue we can point out with <code>utils.js</code> is how it depends on <code>request.js</code>  this goes against the ideal flow for dependencies. High-level modules should depend on low-level ones, and never the reverse.</p>



<p>So, how can we solve these issues? The first step is to identify what’s causing the problem. All of the issues with <code>utils.js</code> are related to the fact that it is doing too much. That’s where the <a href="https://blog.logrocket.com/solid-principles-single-responsibility-in-javascript-frameworks/" rel="noopener">Single Responsibility Principle</a> comes into play. Using this principle, <code>utils.js</code> can be inspected to identify everything it does, then each cohesive functionality identified from <code>utils.js</code> can be extracted into its own focused module. This way, we won’t have so many modules that are dependent on <code>utils.js</code>, leading to a more stable application.</p>



<p>Moving on from <code>utils.js</code>​, we can see from the graph that there are now two circular dependencies:</p>



<ul class="wp-block-list">
<li><code>express.js</code> → <code>application.js</code> → <code>view.js</code> → <code>express.js</code></li>



<li><code>response.js</code> → <code>utils.js</code>→ <code>view.js</code>→ <code>response.js</code></li>
</ul>



<p>Circular dependencies occur when two or more modules directly or indirectly depend on each other. This is bad because it makes it hard to reuse a module, and any change made to one module in the circular dependency is likely to affect the rest of the modules.</p>



<p>For example, in the first circular dependency (<code>express.js</code> → <code>application.js</code> → <code>view.js</code> → <code>express.js</code>), if <code>view.js</code> breaks, <code>application.js</code> will also break because it depends on <code>view.js</code> — and <code>express.js</code> will also break because it depends on <code>application.js</code>.</p>



<p>You can begin checking and managing your module graphs with tools such as Madge and <a href="https://github.com/sverweij/dependency-cruiser" rel="noopener">Dependency Cruiser</a>. Madge allows you to visualize module dependencies, while Dependency Cruiser goes further by allowing you to set rules on which layers of your application are allowed to import from which other layers.</p>



<p>Understanding the module graph can help you optimize build times and fix architectural issues such as circular dependency and high coupling.</p>


<h3 class="wp-block-heading" id="the-barrel-file-problem">The Barrel File Problem</h3>


<p>One common way the JavaScript module system is being used is through barrel files. A <strong>barrel file</strong> is a file (usually named something like <code>index.js</code>/<code>index.ts</code>) that re-exports components from other files. Barrel files provide a cleaner way to handle a project’s imports and exports.</p>



<p>Suppose we have the following files:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// auth/login.ts
export function login(email: string, password: string) {
  return `Logging in ${email}`;
}

// auth/register.ts
export function register(email: string, password: string) {
  return `Registering ${email}`;
}</code></pre>



<p>Without barrel files, this is how the imports look:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// somewhere else in the app
import { login } from '@/features/auth/login';
import { register } from '@/features/auth/register';</code></pre>



<p>Notice how the more modules we need in a file, the more import lines we’re going to have in that file.</p>



<p>Using barrel files, we can make our imports look like this:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// somewhere else in the app
import { login, register } from '@/features/auth';</code></pre>



<p>And the barrel file handling the exports will look like this:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// auth/index.ts
export * from './login';
export * from './register';</code></pre>



<p>​​Barrel files provide a cleaner way to handle imports and exports. They improve code readability and make it easier to refactor code by reducing the lines of imports you have to manage. However, the benefits they provide come at the expense of performance (by prolonging build times) and less effective tree shaking, which, of course, results in larger JavaScript bundles. Atlassian, for instance, <a href="https://www.atlassian.com/blog/atlassian-engineering/faster-builds-when-removing-barrel-files" rel="noopener">reported to have achieved 75% faster builds</a>, and a slight reduction in their JavaScript bundle size after removing barrel files from their Jira application’s front-end.</p>



<p>For small projects, barrel files are great. But for larger projects, I’d say they improve code readability at the expense of performance. You can also read about <a href="https://thepassle.netlify.app/blog/barrel-files-a-case-study" rel="noopener">the effects barrel files had on the MSW library project</a>.</p>


<h3 class="wp-block-heading" id="the-coupling-issue">The Coupling Issue</h3>


<p><strong>Coupling</strong> describes how the components of your system rely on each other. In practice, you cannot get rid of coupling, as different parts of your project need to interact for them to function well. However, there are two types of coupling you should avoid: (1) <strong>tight coupling</strong> and (2) <strong>implicit coupling</strong>.</p>



<p><strong>Tight coupling</strong> occurs when there is a high degree of interdependence between two or more modules in a project such that the dependent module relies on some of the implementation details of the dependency module. This makes it hard (if not impossible) to update the dependency module without touching the dependent module, and, depending on how tightly coupled your project is, updating one module may require updating several other modules — a phenomenon known as <a href="https://en.wikiversity.org/wiki/Software_Design/Change_amplification" rel="noopener">change amplification</a>.</p>



<p>Implicit coupling occurs when one module in your project secretly depends on another. Patterns like global singletons, shared mutable state, and side effects can cause implicit coupling. Implicit coupling can reduce inaccurate tree shaking, unexpected behavior in your code, and other issues that are difficult to trace.</p>



<p>While coupling cannot be removed from a system, it is important that:</p>



<ul class="wp-block-list">
<li>You are not exposing the implementation details of a module for another to depend on.</li>



<li>You are not exposing the implementation details of a module for another to depend on.</li>



<li>The dependence of one module on another is explicit.</li>



<li>Patterns such as shared mutable states and global singletons are used carefully.</li>
</ul>


<h3 class="wp-block-heading" id="module-boundaries-are-team-boundaries">Module Boundaries Are Team Boundaries</h3>


<p>When building large scale applications, different modules of the application are usually assigned to different teams. Depending on who owns the modules, boundaries are created, and these boundaries can be characterized as one of the following:</p>



<ul class="wp-block-list">
<li><strong>Weak:</strong> Where others are allowed to make changes to code that wasn’t assigned to them, and the ones responsible for the code monitor the changes made by others while also maintaining the code.</li>



<li><strong>Strong:</strong> Where ownership is assigned to different people, and no one is allowed to make contributions to code that is not assigned to them. If anyone needs a change in another person’s module, they’ll have to contact the owner of that module, so the owners can make that change.</li>



<li><strong>Collective:</strong> Where no one owns anything and anyone can make changes to any part of the project.</li>
</ul>



<p>There must be some form of communication regardless of the type of collaboration. With Conway’s Law, we can better infer how different levels of communication coupled with the different types of ownership can affect software architecture.</p>



<p><a href="https://www.melconway.com/Home/Conways_Law.html" rel="noopener">According to Conway’s Law</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization&#8217;s communication structure.</p>
</blockquote>



<p>Based on this, here are some assumptions we can make:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th></th><th><strong>Good Communication</strong></th><th><strong>Poor Communication</strong></th></tr></thead><tbody><tr><td><strong>Weak Code Ownership</strong></td><td>Architecture may still emerge, but boundaries remain unclear</td><td>Fragmented, inconsistent architecture</td></tr><tr><td><strong>Strong Code Ownership</strong></td><td>Clear, cohesive architecture aligned with ownership boundaries</td><td>Disconnected modules; integration mismatches</td></tr><tr><td><strong>Collective Code Ownership</strong></td><td>Highly collaborative, integrated architecture</td><td>Blurred boundaries; architectural drift</td></tr></tbody></table></figure>



<p>Here’s something to keep in mind whenever you define module boundaries: Modules that frequently change together should share the same boundary, since shared evolution is a strong signal that they represent a single cohesive unit.</p>


<h3 class="wp-block-heading" id="conclusion">Conclusion</h3>


<p>Structuring a large project goes beyond organizing files and folders. It involves creating boundaries through modules and coupling them together to form a functional system. By being deliberate about your project architecture, you save yourself from the hassle that comes with refactoring, and you make your project easier to scale and maintain.</p>



<p>If you have existing projects you’d like to manage and you don’t know where to start, you can begin by installing Madge or Dependency Cruiser. Point Madge at your project, and see what the graph actually looks like. Check for circular dependencies and modules with arrows coming in from everywhere. Ask yourself if what you see is what you planned your project to look like.</p>



<p>Then, you can proceed by enforcing boundaries, breaking circular chains, moving modules and extracting utilities. You don’t need to refactor everything at once — you can make changes as you go. Also, if you don&#8217;t have an organized system for using modules, you need to start implementing one.</p>



<p>Are you letting your module structure happen to you, or are you designing it?</p>


<h3 class="wp-block-heading" id="further-reading">Further Reading</h3>


<ul class="wp-block-list">
<li><a href="https://feature-sliced.design/blog/frontend-folder-structure" rel="noopener">The Perfect Folder Structure for Scalable Frontend</a> (Feature-Sliced Design)</li>



<li><a href="https://federicoterzi.com/blog/good-software-architectures-are-mostly-about-boundaries/" rel="noopener">Good Software Architectures are mostly about Boundaries</a> (Federico Terzi)</li>
</ul>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-javascript-module-system-architecture/">A Well-Designed JavaScript Module System is Your First Architecture Decision</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/the-javascript-module-system-architecture/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392882</post-id>	</item>
		<item>
		<title>The Radio State Machine</title>
		<link>https://css-tricks.com/the-radio-state-machine/</link>
					<comments>https://css-tricks.com/the-radio-state-machine/#comments</comments>
		
		<dc:creator><![CDATA[Amit Sheen]]></dc:creator>
		<pubDate>Tue, 14 Apr 2026 13:55:38 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[checkbox hack]]></category>
		<category><![CDATA[forms]]></category>
		<category><![CDATA[state]]></category>
		<category><![CDATA[state machines]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393344</guid>

					<description><![CDATA[<p>One of the best-known examples of CSS state management is the checkbox hack. What if we want a component to be in one of three, four, or seven modes? That is where the Radio State Machine comes in.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-radio-state-machine/">The Radio State Machine</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Managing state in CSS is not exactly the most obvious thing in the world, and to be honest, it is not always the best choice either. If an interaction carries business logic, needs persistence, depends on data, or has to coordinate multiple moving parts, JavaScript is usually the right tool for the job.</p>



<p>That said, not every kind of state deserves a trip through JavaScript.</p>



<p>Sometimes we are dealing with purely visual UI state: whether a panel is open, an icon changed its appearance, a card is flipped, or whether a decorative part of the interface should move from one visual mode to another.</p>



<p>In cases like these, keeping the logic in CSS can be not just possible, but preferable. It keeps the behavior close to the presentation layer, reduces JavaScript overhead, and often leads to surprisingly elegant solutions.</p>



<span id="more-393344"></span>


<h3 class="wp-block-heading" id="the-boolean-solution">The Boolean solution</h3>


<p>One of the best-known examples of CSS state management is <a href="https://css-tricks.com/the-checkbox-hack/">the checkbox hack</a>.</p>



<p>If you have spent enough time around CSS, you have probably seen it used for all kinds of clever UI tricks. It can be used to restyle the checkbox itself, toggle menus, control inner visuals of components, reveal hidden sections, and even switch an entire theme. It is one of those techniques that feels slightly mischievous the first time you see it, and then immediately becomes useful.</p>



<p>If you have never used it before, the checkbox hack concept is very simple:</p>



<ol class="wp-block-list">
<li>We place a hidden checkbox at the top of the document.</li>
</ol>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;input type="checkbox" id="state-toggle" hidden></code></pre>



<ol start="2" class="wp-block-list">
<li>We connect a <code>label</code> to it, so the user can toggle it from anywhere we want.</li>
</ol>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;label for="state-toggle" class="state-button">
  Toggle state
&lt;/label></code></pre>



<ol start="3" class="wp-block-list">
<li>In CSS, we use the <a href="https://css-tricks.com/almanac/pseudo-selectors/c/checked/"><code>:checked</code></a> state and sibling combinators to style other parts of the page based on whether that checkbox is checked.</li>
</ol>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">#state-toggle:checked ~ .element {
  /* styles when the checkbox is checked */
}

.element {
  /* default styles */
}</code></pre>



<p>In other words, the checkbox becomes a little piece of built-in UI state that CSS can react to. Here is a simple example of how it can be used to switch between light and dark themes:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_bNwaqoG/23e99ec7b74dc448e40012df67b064ba" src="//codepen.io/anon/embed/bNwaqoG/23e99ec7b74dc448e40012df67b064ba?height=550&amp;theme-id=1&amp;slug-hash=bNwaqoG/23e99ec7b74dc448e40012df67b064ba&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed bNwaqoG/23e99ec7b74dc448e40012df67b064ba" title="CodePen Embed bNwaqoG/23e99ec7b74dc448e40012df67b064ba" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>


<h3 class="wp-block-heading" id="we-have-has-">We have <code>:has()</code></h3>


<p>Note that I&#8217;ve placed the checkbox at the top of the document, before the rest of the content. This was important in the days before the <a href="https://css-tricks.com/almanac/pseudo-selectors/h/has/"><code>:has()</code></a> pseudo-class, because CSS only allowed us to select elements that come after the checkbox in the DOM. Placing the checkbox at the top was a way to ensure that we could target any element in the page with our selectors, regardless of the <code>label</code> position in the DOM.</p>



<p>But now that <code>:has()</code> is widely supported, we can place the checkbox anywhere in the document, and still target elements that come before it. This gives us much more flexibility in how we structure our HTML. For example, we can place the checkbox right next to the label, and still control the entire page with it.</p>



<p>Here is a classic example of the checkbox hack theme selector, with the checkbox placed next to the label, and using <code>:has()</code> to control the page styles:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="content">
  &lt;!-- content -->
&lt;/div>

&lt;label class="theme-button">
  &lt;input type="checkbox" id="theme-toggle" hidden>
  Toggle theme
&lt;/label></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">body {
  /* other styles */

  /* default to dark mode */
  color-scheme: dark;

  /* when the checkbox is checked, switch to light mode */
  &amp;:has(#theme-toggle:checked) {
    color-scheme: light;
  }
}

/* use the color `light-dark()` on the content */
.content {
  background-color: light-dark(#111, #eee);
  color: light-dark(#fff, #000);
}</code></pre>



<p><strong>Note:</strong> I&#8217;m using the ID selector (<code>#</code>) in the CSS as it is already part of the checkbox hack convention, and it is a simple way to target the checkbox. If you worry about CSS selectors performance, don’t.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_PwGEKOv/4ddae54abddd0a2c10205ca5d93c721b" src="//codepen.io/anon/embed/PwGEKOv/4ddae54abddd0a2c10205ca5d93c721b?height=550&amp;theme-id=1&amp;slug-hash=PwGEKOv/4ddae54abddd0a2c10205ca5d93c721b&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed PwGEKOv/4ddae54abddd0a2c10205ca5d93c721b" title="CodePen Embed PwGEKOv/4ddae54abddd0a2c10205ca5d93c721b" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>


<h3 class="wp-block-heading" id="hidden-not-disabled-and-not-so-accessible-">Hidden, not disabled (and not so accessible)</h3>


<p>Note I&#8217;ve been using the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/hidden" rel="noopener">HTML <code>hidden</code> global attribute</a> to hide the checkbox from view. This is a common practice in the checkbox hack, as it keeps the input in the DOM and allows it to maintain its state, while removing it from the visual flow of the page.</p>



<p>Sadly, the <code>hidden</code> attribute also hides the element from assistive technologies, and the <code>label</code> that controls it does not have any interactive behavior on its own, which means that screen readers and other assistive devices will not be able to interact with the checkbox.</p>



<p>This is a significant accessibility concern, and to fix this, we need a different approach: instead of wrapping the checkbox in a <code>label</code> and hiding it with <code>hidden</code>, we can turn the checkbox into the button itself.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;input type="checkbox" class="theme-button" aria-label="Toggle theme"></code></pre>



<p>No hidden, no label, just a fully accessible checkbox. And to style it like a button, we can use the <a href="https://css-tricks.com/almanac/properties/a/appearance/"><code>appearance</code></a> property to remove the default checkbox styling and apply our own styles.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.theme-button {
  appearance: none;
  cursor: pointer;
  font: inherit;
  color: inherit;
  /* other styles */
  
  /* Add text using a simple pseudo-element */
  &amp;::after {
    content: "Toggle theme";
  }
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_myrxEeP/f32a6dfe16a0d6a0b733e7bc4310c70e" src="//codepen.io/anon/embed/myrxEeP/f32a6dfe16a0d6a0b733e7bc4310c70e?height=550&amp;theme-id=1&amp;slug-hash=myrxEeP/f32a6dfe16a0d6a0b733e7bc4310c70e&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed myrxEeP/f32a6dfe16a0d6a0b733e7bc4310c70e" title="CodePen Embed myrxEeP/f32a6dfe16a0d6a0b733e7bc4310c70e" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>This way, we get a fully accessible toggle button that still controls the state of the page through CSS, without relying on hidden inputs or labels. And we&#8217;re going to use this approach in all the following examples as well.</p>


<h3 class="wp-block-heading" id="getting-more-states">Getting more states</h3>


<p>So, the checkbox hack is a great way to manage simple binary state in CSS, but it also has a very clear limitation. A checkbox gives us two states: checked and not checked. On and off. That is great when the UI only needs a binary choice, but it is not always enough.</p>



<p>What if we want a component to be in one of three, four, or seven modes? What if a visual system needs a proper set of mutually exclusive states instead of a simple toggle?</p>



<p>That is where <strong>the Radio State Machine</strong> comes in.</p>


<h3 class="wp-block-heading" id="simple-three-state-example">Simple three-state example</h3>


<p>The core idea is very similar to the checkbox hack, but instead of a single checkbox, we use a bunch of radio buttons. Each radio button represents a different state, and because radios let us choose one option out of many, they give us a surprisingly flexible way to build multi-state visual systems directly in CSS.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_RNGxxqO/c9f715f3c45063394f458d7246138d3e" src="//codepen.io/anon/embed/RNGxxqO/c9f715f3c45063394f458d7246138d3e?height=550&amp;theme-id=1&amp;slug-hash=RNGxxqO/c9f715f3c45063394f458d7246138d3e&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed RNGxxqO/c9f715f3c45063394f458d7246138d3e" title="CodePen Embed RNGxxqO/c9f715f3c45063394f458d7246138d3e" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>Let&#8217;s break down how this works:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div class="state-button">
  &lt;input type="radio" name="state" data-state="one" aria-label="state one" checked>
  &lt;input type="radio" name="state" data-state="two" aria-label="state two">
  &lt;input type="radio" name="state" data-state="three" aria-label="state three">
&lt;/div></code></pre>



<p>We created a group of radio buttons. Note that they all share the same <code>name</code> attribute (<code>state</code> in this case). This ensures that only one radio can be selected at a time, giving us mutually exclusive states.</p>



<p>We gave each radio button a unique <code>data-state</code> that we can target in CSS to apply different styles based on which state is selected, and the <code>checked</code> attribute to set the default state (in this case, <code>one</code> is the default).</p>


<h4 class="wp-block-heading" id="style-the-buttons">Style the buttons</h4>


<p>The style for the radio buttons themselves is similar to the checkbox button we created earlier. We use <code>appearance: none</code> to remove the default styling, and then apply our own styles to make them look like buttons.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">input[name="state"] {
  appearance: none;
  padding: 1em;
  border: 1px solid;
  font: inherit;
  color: inherit;
  cursor: pointer;
  user-select: none;

  /* Add text using a pseudo-element */
  &amp;::after {
    content: "Toggle State";
  }

  &amp;:hover {
    background-color: #fff3;
  }
}</code></pre>



<p>The main difference is that we have multiple radio buttons, each representing a different state, and we only need to show the one for the next state in the sequence, while hiding the others. We can&#8217;t use <code>display: none</code> on the radio buttons themselves, because that would make them inaccessible, but we can achieve this by adding a few properties as a default, and overriding them for the radio button we want to show.</p>



<ol class="wp-block-list">
<li><code>position: fixed;</code> to take the radio buttons out of the normal flow of the page.</li>



<li><code>pointer-events: none;</code> to make sure the radio buttons themselves are not clickable.</li>



<li><code>opacity: 0;</code> to make the radio buttons invisible.</li>
</ol>



<p>That will hide all the radio buttons by default, while keeping them in the DOM and accessible.</p>



<p>Then we can show the <strong>next</strong> radio button in the sequence by targeting it with the <a href="https://css-tricks.com/css-selectors/#adjacent-combinator">adjacent sibling combinator</a> (<code>+</code>) when the current radio button is checked. This way, only one radio button is visible at a time, and users can click on it to move to the next state.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">input[name="state"] {
  /* other styles */

  position: fixed;
  pointer-events: none;
  opacity: 0;

  &amp;:checked + &amp; {
    position: relative;
    pointer-events: all;
    opacity: 1;
  }
}</code></pre>



<p>And to make the flow circular, we can also add a rule to show the first radio button when the last one is checked. This is, of course, optional, and we&#8217;ll talk about linear and bi-directional flows later.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">&amp;:first-child:has(~ :last-child:checked) {}</code></pre>



<p>One last touch is to add an <code>outline</code> to the radio buttons container. As we are always hiding the checked radio buttons, we are also hiding its outline. By adding an <a href="https://css-tricks.com/almanac/properties/o/outline/"><code>outline</code></a> to the container, we can ensure that users can still see where they are when they navigate through the states using the keyboard.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.state-button:has(:focus-visible) {
  outline: 2px solid red;
}</code></pre>


<h4 class="wp-block-heading" id="style-the-rest">Style the rest</h4>


<p>Now we can add styles for each state using the <code>:checked</code> selector to target the selected radio button. Each state will have its own unique styles, and we can use the <code>data-state</code> attribute to differentiate between them.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">body {
  /* other styles */
  
  &amp;:has([data-state="one"]:checked) .element {
    /* styles when the first radio button is checked */
  }

  &amp;:has([data-state="two"]:checked) .element {
    /* styles when the second radio button is checked */
  }

  &amp;:has([data-state="three"]:checked) .element {
    /* styles when the third radio button is checked */
  }
}

.element {
  /* default styles */
}</code></pre>



<p>And, of course, this pattern can be used for far more than a simple three-state toggle. The same idea can power steppers, view switchers, card variations, visual filters, layout modes, small interactive demos, and even more elaborate CSS-only toys. Some of these use cases are mostly practical, some are more playful, and we are going to explore a few of them later in this article.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_XJjVPrK/186c7f43c150fe4d2556062180604629" src="//codepen.io/anon/embed/XJjVPrK/186c7f43c150fe4d2556062180604629?height=750&amp;theme-id=1&amp;slug-hash=XJjVPrK/186c7f43c150fe4d2556062180604629&amp;default-tab=result" height="750" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed XJjVPrK/186c7f43c150fe4d2556062180604629" title="CodePen Embed XJjVPrK/186c7f43c150fe4d2556062180604629" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>


<h3 class="wp-block-heading" id="utilize-custom-properties">Utilize custom properties</h3>


<p>Now that we are back to keeping all the state inputs in one place, and we are already leaning on <code>:has()</code>, we get another very practical advantage: <a href="https://css-tricks.com/a-complete-guide-to-custom-properties/">custom properties</a>.</p>



<p>In previous examples, we often set the final properties directly per state, which meant targeting the element itself each time. That works, but it can get noisy fast, especially as the selectors become more specific and the component grows.</p>



<p>A cleaner pattern is to assign state values to variables at a higher level, take advantage of how custom properties naturally cascade down, and then consume those variables wherever needed inside the component.</p>



<p>For example, we can define <code>--left</code> and <code>--top</code> per state:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">body {
  /* ... */
  &amp;:has([data-state="one"]:checked) {
    --left: 48%;
    --top: 48%;
  }
  &amp;:has([data-state="two"]:checked) {
    --left: 73%;
    --top: 81%;
  }
  /* other states... */
}</code></pre>



<p>Then we simply consume those values on the element itself:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.map::after {
  content: '';
  position: absolute;
  left: var(--left, 50%);
  top: var(--top, 50%);
  /* ... */
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_gbwvJvb/5a74fe4e920534a93153ee1e8b89161e" src="//codepen.io/anon/embed/gbwvJvb/5a74fe4e920534a93153ee1e8b89161e?height=650&amp;theme-id=1&amp;slug-hash=gbwvJvb/5a74fe4e920534a93153ee1e8b89161e&amp;default-tab=result" height="650" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed gbwvJvb/5a74fe4e920534a93153ee1e8b89161e" title="CodePen Embed gbwvJvb/5a74fe4e920534a93153ee1e8b89161e" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>This keeps state styling centralized, reduces selector repetition, and makes each component class easier to read because it only consumes variables instead of re-implementing state logic.</p>


<h3 class="wp-block-heading" id="use-math-not-just-states">Use math, not just states</h3>


<p>Once we move state into variables, we can also treat state as a number and start doing calculations.</p>



<p>Instead of assigning full visual values for every state, we can define a single numeric variable:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">body {
  /* ... */
  &amp;:has([data-state="one"]:checked) { --state: 1; }
  &amp;:has([data-state="two"]:checked) { --state: 2; }
  &amp;:has([data-state="three"]:checked) { --state: 3; }
  &amp;:has([data-state="four"]:checked) { --state: 4; }
  &amp;:has([data-state="five"]:checked) { --state: 5; }
}</code></pre>



<p>Now we can take that value and use it in calculations on any element we want. For example, we can drive the background color directly from the active state:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card {
  background-color: hsl(calc(var(--state) * 60) 50% 50%);
}</code></pre>



<p>And if we define an index variable like <code>--i</code> per item (at least until <a href="https://css-tricks.com/almanac/functions/s/sibling-index/"><code>sibling-index()</code></a> is more widely available), we can calculate each item&#8217;s style, like position and opacity, relative to the active state and its place in the sequence.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card {
  position: absolute;
  transform:
    translateX(calc((var(--i) - var(--state)) * 110%))
    scale(calc(1 - (abs(var(--i) - var(--state)) * 0.3)));
  opacity: calc(1 - (abs(var(--i) - var(--state)) * 0.4));
}</code></pre>



<p>This is where the pattern becomes really fun: one <code>--state</code> variable drives an entire visual system. You are no longer writing separate style blocks for every card in every state. You define a rule once, give each item its own index (<code>--i</code>), and let CSS do the rest.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_zxKpgRz/e738832facbec02af08fb0634f713caf" src="//codepen.io/anon/embed/zxKpgRz/e738832facbec02af08fb0634f713caf?height=550&amp;theme-id=1&amp;slug-hash=zxKpgRz/e738832facbec02af08fb0634f713caf&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed zxKpgRz/e738832facbec02af08fb0634f713caf" title="CodePen Embed zxKpgRz/e738832facbec02af08fb0634f713caf" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>


<h3 class="wp-block-heading" id="not-every-state-flow-should-loop">Not every state flow should loop</h3>


<p>You may have noticed that unlike the earlier demos, the last example was not circular. Once you reach the last state, you get stuck there. This is because I removed the rule that shows the first radio button when the last one is checked, and instead added a <code>disabled</code> radio button as a placeholder that appears when the last state is active.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;input type="radio" name="state" disabled></code></pre>



<p>This pattern is useful for progressive flows like onboarding steps, checkout progress, or multi-step setup forms where the final step is a real endpoint. That said, the states are still accessible through keyboard navigation, and that is a good thing, unless you don&#8217;t want it to be.</p>



<p>In that case, you can replace the <code>position</code>, <code>pointer-events</code>, and <code>opacity</code> properties with <code>display: none</code> as a default, and <code>display: block</code> (or <code>inline-block</code>, etc.) for the one that should be visible and interactive. This way, the hidden states will not be focusable or reachable by keyboard users, and the flow will be truly linear.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_WbGzONO/1b7e1f863d2b762b3881eaa7e86e880a" src="//codepen.io/anon/embed/WbGzONO/1b7e1f863d2b762b3881eaa7e86e880a?height=550&amp;theme-id=1&amp;slug-hash=WbGzONO/1b7e1f863d2b762b3881eaa7e86e880a&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed WbGzONO/1b7e1f863d2b762b3881eaa7e86e880a" title="CodePen Embed WbGzONO/1b7e1f863d2b762b3881eaa7e86e880a" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>


<h3 class="wp-block-heading" id="bi-directional-flows">Bi-directional flows</h3>


<p>Of course, interaction should not only move forward. Sometimes users need to go back too, so we can add a &#8220;Previous&#8221; button by also showing the radio button that points to the previous state in the sequence.</p>



<p>To update the CSS so each state reveals not one, but two radio buttons, we need to expand the selectors to target both the next and previous buttons for each state. We select the next button like before, using the adjacent sibling combinator (<code>+</code>), and the previous button using <code>:has()</code> to look for the checked state on the next button (<code>:has(+ :checked)</code>).</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">input[name="state"] {
  position: fixed;
  pointer-events: none;
  opacity: 0;
  /* other styles */
  
  &amp;:has(+ :checked),
  &amp;:checked + &amp;  {
    position: relative;
    pointer-events: all;
    opacity: 1;
  }

  /* Set text to "Next" as a default */
  &amp;::after {
    content: "Next";
  }

  /* Change text to "Previous" when the next state is checked */
  &amp;:has(+ :checked)::after {
    content: "Previous";
  }
}</code></pre>



<p>This way, users can navigate in either direction through the states.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_GgjQYQg/198ea1b339a6dd21d6b9cbb286ec2d2a" src="//codepen.io/anon/embed/GgjQYQg/198ea1b339a6dd21d6b9cbb286ec2d2a?height=550&amp;theme-id=1&amp;slug-hash=GgjQYQg/198ea1b339a6dd21d6b9cbb286ec2d2a&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed GgjQYQg/198ea1b339a6dd21d6b9cbb286ec2d2a" title="CodePen Embed GgjQYQg/198ea1b339a6dd21d6b9cbb286ec2d2a" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>This is a simple extension of the previous logic, but it gives us much more control over the flow of the state machine, and allows us to create more complex interactions while still keeping the state management in CSS.</p>


<h3 class="wp-block-heading" id="accessibility-notes">Accessibility notes</h3>


<p>Before wrapping up, one important reminder: this pattern should stay visual in responsibility, but accessible in behavior. Because the markup is built on real form controls, we already get a strong baseline, but we need to be deliberate about accessibility details:</p>



<ul class="wp-block-list">
<li>Make the radio buttons clearly interactive (cursor, size, spacing) and keep their wording explicit.</li>



<li>Keep visible focus styles so keyboard users can always track where they are.</li>



<li>If a step is not available, communicate that state clearly in the UI, not only by color.</li>



<li>Respect reduced motion preferences when state changes animate layout or opacity.</li>



<li>If state changes carry business meaning (validation, persistence, async data), hand that part to JavaScript and use CSS state as the visual layer.</li>
</ul>



<p>In short: the radio state machine works best when it enhances interaction, not when it replaces semantics or application logic.</p>


<h3 class="wp-block-heading" id="closing-thoughts">Closing thoughts</h3>


<p>The radio state machine is one of those CSS ideas that feels small at first, and then suddenly opens a lot of creative doors.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_myrxMzG/8958cf587491c7b17721fe15e75bc98e" src="//codepen.io/anon/embed/myrxMzG/8958cf587491c7b17721fe15e75bc98e?height=550&amp;theme-id=1&amp;slug-hash=myrxMzG/8958cf587491c7b17721fe15e75bc98e&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed myrxMzG/8958cf587491c7b17721fe15e75bc98e" title="CodePen Embed myrxMzG/8958cf587491c7b17721fe15e75bc98e" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>With a few well-placed inputs, and a couple of smart selectors, we can build interactions that feel alive, expressive, and surprisingly robust, all while keeping visual state close to the layer that actually renders it.</p>



<p>But it is still just that: an idea.</p>



<p>Use it when the state is mostly visual, local, and interaction-driven. Skip it when the flow depends on business rules, external data, persistence, or complex orchestration.</p>



<p>Believe me, if there were a prize for forcing complex state management into CSS just because we technically can, I would have won it long ago. The real win is not proving CSS can do everything, but learning exactly where it shines.</p>



<p>So here is the challenge: pick one tiny UI in your project, rebuild it as a mini state machine, and see what happens. If it becomes cleaner, keep it. If it gets awkward, roll it back with zero guilt. And don&#8217;t forget to share your experiments.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-radio-state-machine/">The Radio State Machine</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/the-radio-state-machine/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393344</post-id>	</item>
		<item>
		<title>7 View Transitions Recipes to Try</title>
		<link>https://css-tricks.com/7-view-transitions-recipes-to-try/</link>
					<comments>https://css-tricks.com/7-view-transitions-recipes-to-try/#respond</comments>
		
		<dc:creator><![CDATA[Sunkanmi Fafowora]]></dc:creator>
		<pubDate>Mon, 13 Apr 2026 14:14:43 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392002</guid>

					<description><![CDATA[<p>Craving for a view transition? Sunkanmi has lots of common transitions you can drop into your website right now!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/7-view-transitions-recipes-to-try/">7 View Transitions Recipes to Try</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>View transitions are really, really neat. Not only that, but they&#8217;re starting to pop up <em>everywhere</em>. I&#8217;m sure you&#8217;re like me and have come across more than a few in the wild that both make you go <em>wow</em> and want to instantly use them on your own site or project.</p>



<p>At the same time, view transitions can be tricky to &#8220;get&#8221; at first. <a href="https://css-tricks.com/snippets/css/basic-view-transition/">They can be simple</a>, sure, but most anything beyond a cross-fade involves several moving parts.</p>



<p>I tend to find that the best way to learn something new is to see the code, use them myself, and then build upon them. So, I&#8217;ve collected seven view transition recipes for exactly that. We&#8217;ll go over the basic setup, demo the recipes, and turn you loose to experiment!</p>



<span id="more-392002"></span>



<p>It&#8217;s perfectly fine to go below and just copy the one you like the most, but if you want to understand what view transitions are all about, then I recommend going through a&nbsp;<a href="https://css-tricks.com/toe-dipping-into-view-transitions/">quick introduction first</a>&nbsp;before getting to the recipes.</p>



<p>Oh, and before we jump in, it&#8217;s worth noting that view transitions are indeed Baseline and supported by all major browsers as I&#8217;m writing this. But some types of animations may or may not be supported by a specific browser, so keep an eye on that and test, as always.</p>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="view-transitions"></baseline-status>


<h3 class="wp-block-heading" id="the-set-up">The setup</h3>


<p>For each view transition, we&#8217;ll need to do a little setup beforehand. First off, we need to <em>opt in</em> to them using the&nbsp;<a href="https://css-tricks.com/almanac/rules/v/view-transition/"><code>@view-transition</code></a>&nbsp;at-rule on&nbsp;<strong>both pages</strong> — the page we&#8217;re on and the page we&#8217;re transitioning to. If you&#8217;re using templates on your site, then this might go in the header template so it globally applies everywhere.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: &lt;transition-type>;
  }
}</code></pre>



<p>That <code>&lt;transition-type&gt;</code> is the only part you can&#8217;t directly copy-paste. It&#8217;s a placeholder for the&nbsp;<code>types</code>&nbsp;descriptor, <a href="https://css-tricks.com/what-on-earth-is-the-types-descriptor-in-view-transitions/">something we&#8217;ve covered in detail before</a>. It&#8217;s more nuanced than this, but <code>types</code> are basically the animation name we give to a specific transition. That way, if we&#8217;re working with multiple transitions, we can be explicit about which ones are active to prevent them from conflicting with one another. But read that linked article to get deeper into it.</p>



<p>Notice how we have the&nbsp;<code>@view-transition</code>&nbsp;walled behind the&nbsp;<code><a href="https://css-tricks.com/almanac/rules/m/media/prefers-reduced-motion/">prefers-reduced-motion: no-preference</a></code> media query. Not everyone wants movement on their pages and that&#8217;s a preference that can be set at the OS level, so we&#8217;ll respect that where needed this way.</p>



<p>Lastly, we&#8217;ll apply our animation as follows:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">html:active-view-transition-type(&lt;transition-type>)::view-transition-old(root) {
  animation: a-cool-outgoing-animation 1.4s ease forwards;
}

html:active-view-transition-type(&lt;transition-type>)::view-transition-new(root) {
  animation: a-cool-incoming-animation 1.4s ease forwards;
}</code></pre>



<p>&#8230;where the&nbsp;<code>:active-view-transtion-type()</code>&nbsp;pseudo matches the transition <code>type</code> we define in&nbsp;the <code>@view-transition</code> rule. For example, if we&#8217;re calling an animation that we&#8217;ve named <code>bounce</code>, then we&#8217;d use that in the at-rule like this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: &lt;transition-type>;
  }
}</code></pre>



<p>&#8230;as well as the pseudo like this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* The "current" page */
html:active-view-transition-type(bounce)::view-transition-old(root) {
  animation: bounce-in 1.4s ease forwards;
}

/* The page we're transitioning to */
html:active-view-transition-type(bounce)::view-transition-new(root) {
  animation: bounce-in 1.4s ease forwards;
}</code></pre>



<p>OK, that&#8217;s enough context to get started with the recipes. Again, feel free to use any of these in your own experiments or projects.</p>


<h3 class="wp-block-heading" id="pixelate-dissolve">Pixelate dissolve</h3>


<p>This one&#8217;s sort of like a simple cross-fade, but blurs things out as the old page content fades out and the new page content fades in.</p>



<figure class="wp-block-video"><video height="604" style="aspect-ratio: 1280 / 604;" width="1280" controls src="https://css-tricks.com/wp-content/uploads/2026/02/View-transition-Pixelate-dissolve.mp4" playsinline></video></figure>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" open><summary>Full snippet</summary>
<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: pixelate-dissolve;
  }
}

html:active-view-transition-type(pixelate-dissolve)::view-transition-old(root) {
  animation: pixelate-out 1.4s ease forwards;
}

html:active-view-transition-type(pixelate-dissolve)::view-transition-new(root) {
  animation: pixelate-in 1.4s ease forwards;
}

@keyframes pixelate-out {
  0% {
    filter: blur(0px);
    opacity: 1;
  }
  100% {
    filter: blur(40px);
    opacity: 0;
  }
}

@keyframes pixelate-in {
  0% {
    filter: blur(40px);
    opacity: 0;
  }
  100% {
    filter: blur(0px);
    opacity: 1;
  }
}</code></pre>
</details>


<h3 class="wp-block-heading" id="wipe-up">Wipe up</h3>


<p>Here, we&#8217;re using the <a href="https://css-tricks.com/almanac/properties/c/clip-path/"><code>clip-path</code></a> property to achieve the &#8220;wipe-up&#8221; effect we&#8217;re the content for a new page slides up from the bottom, replacing the &#8220;old&#8221; content.</p>



<p> The process is straightforward: for the outgoing page, we move from its default&nbsp;<code>inset()</code>&nbsp;value of&nbsp;<code>0 0 0 0</code>&nbsp;(which creates a rectangle at the top, right, bottom, and left borders) of the page and change the&nbsp;<strong>bottom</strong>&nbsp;value to&nbsp;<code>100%</code>. Meaning, the page goes from&nbsp;<strong>top</strong>&nbsp;to&nbsp;<strong>bottom</strong>.</p>



<p>The incoming page starts clipping from the <code>top</code> at&nbsp;<code>100%</code>&nbsp;and goes down to&nbsp;<code>0</code>.</p>



<figure class="wp-block-video"><video height="570" style="aspect-ratio: 1280 / 570;" width="1280" controls src="https://css-tricks.com/wp-content/uploads/2026/02/View-transition-techniques_-Wipe-Up.mp4" playsinline></video></figure>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" open><summary>Full snippet</summary>
<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: wipe-up;
  }
}

html:active-view-transition-type(wipe-up)::view-transition-old(root) {
  animation: wipe-out 1.4s ease forwards;
}

html:active-view-transition-type(wipe-up)::view-transition-new(root) {
  animation: wipe-in 1.4s ease forwards;
}

@keyframes wipe-out {
  from {
    clip-path: inset(0 0 0 0);
  }
  to {
    clip-path: inset(0 0 100% 0);
  }
}

@keyframes wipe-in {
  from {
    clip-path: inset(100% 0 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}</code></pre>
</details>



<p id="wipe-right">We could just as easily make things wipe right, wipe bottom, and wipe left simply by changing the inset values. For example, here&#8217;s things wiping right:</p>



<figure class="wp-block-video"><video height="1080" style="aspect-ratio: 1920 / 1080;" width="1920" controls src="https://css-tricks.com/wp-content/uploads/2026/02/wipe-right-edit.mp4"></video></figure>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@keyframes wipe-out {
  from {
    clip-path: inset(0 0 0 0);
  }
  to {
    clip-path: inset(0 0 0 100%);
  }
}

@keyframes wipe-in {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}</code></pre>



<p>The wipe right works similarly to wipe up, except that the outgoing page goes from the center and cuts towards the right. That&#8217;s why the second value goes from&nbsp;<code>0</code>&nbsp;to&nbsp;<code>100%</code>. Similarly, the incoming page goes from&nbsp;<code>100%</code>&nbsp;from the left to&nbsp;<code>0</code>.</p>



<p>Same sort of deal with wiping downward:</p>



<figure class="wp-block-video"><video height="570" style="aspect-ratio: 1280 / 570;" width="1280" controls src="https://css-tricks.com/wp-content/uploads/2026/02/View-Transition-Techniques_-Wipe-down.mp4" playsinline></video></figure>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@keyframes wipe-out {
  from {
    clip-path: inset(0 0 0 0);
  }
  to {
    clip-path: inset(100% 0 0 0);
  }
}

@keyframes wipe-in {
  from {
    clip-path: inset(0 0 100% 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}</code></pre>



<p>You get the idea!</p>


<h3 class="wp-block-heading" id="3-zoom-rotate">Rotate in-out</h3>


<p>This one&#8217;s a little, um, weird. Definitely not the most practical thing in the world, but it does demonstrate how far you can go with view transitions.</p>



<p>We use the the&nbsp;<a href="https://css-tricks.com/almanac/properties/t/transform/#aa-values"><code>scale()</code>&nbsp;and&nbsp;<code>rotate()</code></a>&nbsp;functions to zoom and rotate the page content, where the &#8220;old&#8221; page scales down to <code>0</code> and rotates clockwise by&nbsp;<code>180deg</code>. Following that, the &#8220;new&#8221; page content  scales up to <code>1</code> and rotates counter-clockwise by&nbsp;<code>-180deg</code>. A little <code>opacity</code> is thrown in to help give the illusion that stuff is going out and coming in.</p>



<figure class="wp-block-video"><video height="602" style="aspect-ratio: 1280 / 602;" width="1280" controls src="https://css-tricks.com/wp-content/uploads/2026/02/View-Transition-Zoom-rotate.mp4" playsinline></video></figure>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" open><summary>Full snippet</summary>
<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: zoom-rotate;
  }
}

html:active-view-transition-type(zoom-rotate)::view-transition-old(root) {
  animation: zoom-rotate-out 1.4s ease forwards;
  transform-origin: center;
}

html:active-view-transition-type(zoom-rotate)::view-transition-new(root) {
  animation: zoom-rotate-in 1.4s ease forwards;
  transform-origin: center;
}

@keyframes zoom-rotate-out {
  to {
    transform: scale(0) rotate(180deg);
    opacity: 0;
  }
}

@keyframes zoom-rotate-in {
  from {
    transform: scale(0) rotate(-180deg);
    opacity: 0;
  }
}</code></pre>
</details>


<h3 class="wp-block-heading" id="4-circular-wipe">Circle wipe-out</h3>


<p>This one&#8217;s a lot more subtle than the last one. It could be a lot more noticeable if the content we&#8217;re transitioning to is more distinct. But as you&#8217;ll see in the following video, the &#8220;background between &#8220;old&#8221; and &#8220;new&#8221; pages share the same background, making for a more seamless transition.</p>



<figure class="wp-block-video"><video height="1080" style="aspect-ratio: 1920 / 1080;" width="1920" controls src="https://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Circular-Wipe-1.mp4" playsinline></video></figure>



<p>The circle comes courtesy of the&nbsp;<a href="https://css-tricks.com/almanac/properties/c/clip-path/"><code>clip-pat</code>h</a> property, draws the shape from the center using the&nbsp;<code>circle()</code>&nbsp;function, going from from&nbsp;0% (no size)&nbsp;to&nbsp;150% (sized beyond the content), making it encapsulate the entire page.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" open><summary>Full snippet</summary>
<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: circular-wipe;
  }
}

html:active-view-transition-type(circular-wipe)::view-transition-old(root) {
  animation: circle-wipe-out 1.4s ease forwards;
}

html:active-view-transition-type(circular-wipe)::view-transition-new(root) {
  animation: circle-wipe-in 1.4s ease forwards;
}

@keyframes circle-wipe-out {
  to {
    clip-path: circle(0% at 50% 50%);
  }
}

@keyframes circle-wipe-in {
  from {
    clip-path: circle(0% at 50% 50%);
  }
  to {
    clip-path: circle(150% at 50% 50%);
  }
}</code></pre>
</details>


<h3 class="wp-block-heading" id="5-diagonal-slide">Diagonal push</h3>


<p>This one pushes out the &#8220;old&#8221; page with the &#8220;new&#8221; page from the bottom-right corner of the screen to the top-right corner — or, really, any corner we want by adjusting values.</p>



<p>For the bottom-right, I set the animation to translate to&nbsp;<code>-100%</code>&nbsp;on the X and Y axes, which pushes it away from the screen. Then it comes in from the opposite corner to its default position at&nbsp;<code>0%</code>. A little <code>opacity</code> helps smooth things out.</p>



<figure class="wp-block-video"><video height="602" style="aspect-ratio: 1280 / 602;" width="1280" controls src="https://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Diagonal-Slide.mp4" playsinline></video></figure>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" open><summary>Full snippet</summary>
<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: diagonal-push;
  }
}

html:active-view-transition-type(diagonal-push)::view-transition-old(root) {
  animation: diagonal-out 1.4s ease forwards;
}

html:active-view-transition-type(diagonal-push)::view-transition-new(root) {
  animation: diagonal-in 1.4s ease forwards;
}

@keyframes diagonal-out {
  to {
    transform: translate(-100%, -100%);
    opacity: 0;
  }
}

@keyframes diagonal-in {
  from {
    transform: translate(100%, 100%);
    opacity: 0;
  }
}</code></pre>
</details>


<h3 class="wp-block-heading" id="6-curtain-reveal">Curtain reveal</h3>


<p>This one&#8217;s like the a curtain is closing on the &#8220;old&#8221; page and opens up with the &#8220;new&#8221; page. It&#8217;s another one where the&nbsp;<code>inset()</code>&nbsp;function comes into play. We define rectangles placed 50% at the right and left. This increases to 50% when the page is going out and reduces to 0 when the page is coming in, making the image appear from the middle going to left and right like a curtain!</p>



<figure class="wp-block-video"><video height="1080" style="aspect-ratio: 1920 / 1080;" width="1920" controls src="https://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Curtain-Reveal-1.mp4" playsinline></video></figure>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" open><summary>Full snippet</summary>
<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: curtain;
  }
}

html:active-view-transition-type(curtain)::view-transition-old(root) {
  animation: curtain-out 1.4s ease forwards;
}

html:active-view-transition-type(curtain)::view-transition-new(root) {
  animation: curtain-in 1.4s ease forwards;
}

@keyframes curtain-out {
  from {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes curtain-in {
  from {
    clip-path: inset(0 50% 0 50%);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}</code></pre>
</details>


<h3 class="wp-block-heading" id="7-flip-3d">3D flip</h3>


<p>We&#8217;re sort of faking one page &#8220;flipping&#8221; out like a two-sided card while the next page flips in, both along the Z axis.</p>



<figure class="wp-block-video"><video height="1080" style="aspect-ratio: 1920 / 1080;" width="1920" controls src="https://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Flip-3D-1.mp4" playsinline></video></figure>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" open><summary>Full snippet</summary>
<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: flip-3d;
  }
}

html:active-view-transition-type(flip-3d)::view-transition-old(root) {
  animation: flip-out 1.4s ease forwards;
}

html:active-view-transition-type(flip-3d)::view-transition-new(root) {
  animation: flip-in 1.4s ease forwards;
}

@keyframes flip-out {
  0% {
    transform: rotateY(0deg) translateX(0vw);
  }
  100% {
    transform: rotateY(-90deg) translateX(-100vw);
    opacity: 1;
  }
}

@keyframes flip-in {
  0% {
    transform: rotateY(90deg) translateX(100vw);
  }
  100% {
    transform: rotateY(0deg) translateX(0vw);
  }
}</code></pre>
</details>


<h3 class="wp-block-heading" id="what-about-your-technique-">Any cool recipes you want to share?</h3>


<p>I would <em>love</em> to see more examples and ideas if you have them! Bramus (or Brandi, as I call him) took the time to <a href="https://page-transitions.style/spa/" rel="noopener">create a bunch of view transition examples</a> in an interactive demo that are definitely worth looking at.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/7-view-transitions-recipes-to-try/">7 View Transitions Recipes to Try</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/7-view-transitions-recipes-to-try/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/View-transition-Pixelate-dissolve.mp4" length="658932" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/View-transition-techniques_-Wipe-Up.mp4" length="177582" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/wipe-right-edit.mp4" length="3404539" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/View-Transition-Techniques_-Wipe-down.mp4" length="184035" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/View-Transition-Zoom-rotate.mp4" length="2757351" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Circular-Wipe-1.mp4" length="2237720" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Diagonal-Slide.mp4" length="1324536" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Curtain-Reveal-1.mp4" length="3270052" type="video/mp4" />
<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/View-Transitions-Flip-3D-1.mp4" length="3922085" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">392002</post-id>	</item>
		<item>
		<title>Selecting a Date Range in CSS</title>
		<link>https://css-tricks.com/selecting-a-date-range-in-css/</link>
					<comments>https://css-tricks.com/selecting-a-date-range-in-css/#comments</comments>
		
		<dc:creator><![CDATA[Preethi]]></dc:creator>
		<pubDate>Thu, 09 Apr 2026 13:52:56 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[nth-child]]></category>
		<category><![CDATA[pseudo elements]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393088</guid>

					<description><![CDATA[<p>A clever approach for selecting multiple dates on a calendar where the <code>:nth-child()</code>'s “n of selector” syntax does all the heavy lifting... even in the JavaScript.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/selecting-a-date-range-in-css/">Selecting a Date Range in CSS</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>A date range selector lets users pick a time frame between a start and end date, which is useful in booking trips, sorting info by date blocks, picking time slots, and planning schedules.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1200" height="600" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774012344907_date-selection.jpg?resize=1200%2C600" alt="A calendar month layout with the dates 8 and 18 selected with black backgrounds." class="wp-image-393090" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774012344907_date-selection.jpg?w=1200&amp;ssl=1 1200w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774012344907_date-selection.jpg?resize=300%2C150&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774012344907_date-selection.jpg?resize=1024%2C512&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774012344907_date-selection.jpg?resize=768%2C384&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Example pulled from Airbnb</figcaption></figure>



<p>I’m going to show you an example where, even though JavaScript is involved, the bulk of the work is handled by the “n of selector(s)” syntax of the CSS <code><a href="https://css-tricks.com/almanac/pseudo-selectors/n/nth-child/">:nth-child</a></code> selector, making it easy to build the range selection.</p>



<span id="more-393088"></span>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_RNGobRX" src="//codepen.io/anon/embed/RNGobRX?height=550&amp;theme-id=1&amp;slug-hash=RNGobRX&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed RNGobRX" title="CodePen Embed RNGobRX" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>


<h3 class="wp-block-heading" id="the-n-of-selector-syntax">The “n of selector” syntax</h3>


<p>This syntax of the <code>:nth-child</code> selector <strong>filters elements by a given selector first among all the child elements, before selecting them by a counting order</strong>.</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;p>The reclamation of land...&lt;/p>
&lt;p>The first reclamations can be traced...&lt;/p>
&lt;p class="accent">By 1996, a total of...&lt;/p>
&lt;p>Much reclamation has taken...&lt;/p>
&lt;p class="accent">Hong Kong legislators...&lt;/p></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.accent {
  color: red;
}
.accent:nth-child(2) {
  font-weight: bold; /* does not work */
}
:nth-child(2 of .accent){
  text-decoration: underline;
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_dPpvxmN" src="//codepen.io/anon/embed/dPpvxmN?height=450&amp;theme-id=1&amp;slug-hash=dPpvxmN&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed dPpvxmN" title="CodePen Embed dPpvxmN" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>There are two <code>.accent</code>-ed paragraphs with <code>red</code> text. As we try to target the second accented paragraph, <code>.accent:nth-child(2)</code> fails to select it because it’s trying to find an <code>.accent</code> element that’s <strong>the second child of its parent</strong>.</p>



<p>Whereas, <code>:nth-child(2 of .accent)</code> succeeds in selecting and styling the second accented paragraph because it’s only looking for <strong>the second element among the</strong> <code>**.accent**</code> <strong>elements</strong> rather than the second of all of the children.</p>


<h3 class="wp-block-heading" id="the-layout">The Layout</h3>


<p>Moving onto our main example, let’s put together a month layout. <a href="https://css-tricks.com/a-calendar-in-three-lines-of-css/">It only takes a few lines of CSS.</a></p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;ul id="calendar">
  &lt;li class="day">Mon&lt;/li>
  &lt;li class="day">Tue&lt;/li>
  &lt;!-- up to Sat -->
  &lt;li class="date">01&lt;input type="checkbox" value="01">&lt;/li>
  &lt;li class="date">02&lt;input type="checkbox" value="02">&lt;/li>
  &lt;!-- up to 31  -->
&lt;/ul></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">#calendar {
  display: grid;
  grid-template-columns: repeat(7, 1fr); /* 7 for no. of days in a week */
}</code></pre>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_bNwqXYK" src="//codepen.io/anon/embed/bNwqXYK?height=450&amp;theme-id=1&amp;slug-hash=bNwqXYK&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed bNwqXYK" title="CodePen Embed bNwqXYK" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>


<h3 class="wp-block-heading" id="choose-only-two-dates">Choose Only Two Dates</h3>


<p>Now is when we reach for JavaScript since we can’t check/uncheck a control in CSS. But even here the “n of selector” syntax can be very useful.</p>



<p>When we pick two dates to create a range, clicking on a third date will update the range and remove one of the earlier dates.</p>



<p>You can set up the range re-adjustment logic in any way you like. I’m using this approach: If the <em>third</em> date is either earlier or later than the <em>last return</em> date, it becomes the <em>new return</em> date, and the old one is unselected. If the third date is earlier than the last onward date, it becomes the new onward date, and the old one is unselected.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const CAL = document.getElementById('calendar');
const DT = Array.from(CAL.getElementsByClassName('date')); 

CAL.addEventListener('change', e => {
  if (!CAL.querySelector(':checked')) return;
  
  /* When there are two checked boxes, calendar gets 'isRangeSelected' class  */
  CAL.className = CAL.querySelector(':nth-child(2 of :has(:checked))') ? 'isRangeSelected':'';

  /* When there are three checked boxes */
  if (CAL.querySelector(':nth-child(3 of :has(:checked))')) {

    switch (DT.indexOf(e.target.parentElement)) {

      /* If the newly checked date is first among the checked ones, 
          the second checked is unchecked. Onward date moved earlier. */
      case DT.indexOf(CAL.querySelector(':nth-child(1 of :has(:checked))')):
      CAL.querySelector(':nth-child(2 of :has(:checked)) input').checked = 0; 
      break;

      /* If the newly checked date is second among the checked ones, 
          the third checked is unchecked. Return date moved earlier. */
      case DT.indexOf(CAL.querySelector(':nth-child(2 of :has(:checked))')):
      CAL.querySelector(':nth-child(3 of :has(:checked)) input').checked = 0; 
      break;

      /* If the newly checked date is third among the checked ones, 
          the second checked is unchecked. Return date moved later. */
      case DT.indexOf(CAL.querySelector(':nth-child(3 of :has(:checked))')):
      CAL.querySelector(':nth-child(2 of :has(:checked)) input').checked = 0; 
      break;

    }
  }
});</code></pre>



<p>First, we get the index of the current checked date (<code>DT.indexOf(e.target.parentElement)</code>), then we see if that’s the same as the first checked among all the checked ones (<code>:nth-child(1 of :has(:checked))</code>), second (<code>:nth-child(2 of :has(:checked))</code>), or third (<code>:nth-child(3 of :has(:checked))</code>). Given that, we then uncheck the relevant box to revise the date range.</p>



<p>You’ll notice that by using the “n of selector” syntax, <strong>targeting the <code>:checked</code> box we want by its position among all checked ones</strong> is made much simpler — instead of indexing through a list of checked dates in JavaScript for this, we can directly select it.</p>



<p>Styling the range is even easier than this.</p>


<h3 class="wp-block-heading" id="styling-the-range">Styling the Range</h3>


<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* When two dates are selected */
.isRangeSelected { 
  /* Dates following the first but not the second of selected */
  :nth-child(1 of :has(:checked)) ~ :not(:nth-child(2 of :has(:checked)) ~ .date) {
    /* Range color */
    background-color: rgb(228 239 253); 
  }
}</code></pre>



<p>When there are two dates chosen, the dates between the first (<code>1 of :has(:checked)</code>) and second (<code>2 of :has(:checked)</code>) are colored pale blue, creating a visual range for that block of dates in the month.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1206" height="588" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774013392464_Screenshot2026-03-20at7.29.24AM.png?resize=1206%2C588" alt="A calendar month layout with the dates 9-29 selected. 9 and 19 have a dark blue background and the dates between are light blue." class="wp-image-393089" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774013392464_Screenshot2026-03-20at7.29.24AM.png?w=1206&amp;ssl=1 1206w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774013392464_Screenshot2026-03-20at7.29.24AM.png?resize=300%2C146&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774013392464_Screenshot2026-03-20at7.29.24AM.png?resize=1024%2C499&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_7B1698CD93D2E480B2867F02F52F6807BC326B3FB6B9C5A5A10103D5813DD8E0_1774013392464_Screenshot2026-03-20at7.29.24AM.png?resize=768%2C374&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>The color is declared inside a compound selector that selects dates (<code>.date</code>) following the first of all checked date (<code>:nth-child(1 of :has(:checked))</code>), but not the second of all checked date (<code>:not(:nth-child(2 of :has(:checked))</code>).</p>



<p>Here’s the full example once again:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_RNGobRX" src="//codepen.io/anon/embed/RNGobRX?height=550&amp;theme-id=1&amp;slug-hash=RNGobRX&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed RNGobRX" title="CodePen Embed RNGobRX" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/selecting-a-date-range-in-css/">Selecting a Date Range in CSS</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/selecting-a-date-range-in-css/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393088</post-id>	</item>
		<item>
		<title>Alternatives to the !important Keyword</title>
		<link>https://css-tricks.com/alternatives-to-the-important-keyword/</link>
					<comments>https://css-tricks.com/alternatives-to-the-important-keyword/#comments</comments>
		
		<dc:creator><![CDATA[Saleh Mubashar]]></dc:creator>
		<pubDate>Tue, 07 Apr 2026 13:54:43 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[cascade]]></category>
		<category><![CDATA[specificity]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392696</guid>

					<description><![CDATA[<p>Cascade layers, specificity tricks, smarter ordering, and even some clever selector hacks can often replace <code>!important</code> with something cleaner, more predictable, and far less embarrassing to explain to your future self.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/alternatives-to-the-important-keyword/">Alternatives to the !important Keyword</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Every now and then, I stumble onto an old project of mine, or worse, someone else’s, and I’m reminded just how chaotic CSS can get over time. In most of these cases, the <code>!important</code> keyword seems to be involved in one way or another. And it’s easy to understand why developers rely on it. It provides an immediate fix and forces a rule to take precedence in the cascade.</p>



<p>That’s not to say <code>!important</code> doesn’t have its place. The problem is that once you start using it, you’re no longer working <em>with</em> the cascade; you’re bypassing it. This can quickly get out of hand in larger projects with multiple people working on them, where each new override makes the next one harder.</p>



<p>Cascade layers, specificity tricks, smarter ordering, and even some clever selector hacks can often replace <code>!important</code> with something cleaner, more predictable, and far less embarrassing to explain to your future self.</p>



<p>Let’s talk about those alternatives.</p>



<span id="more-392696"></span>


<h3 class="wp-block-heading" id="specificity-and-important-">Specificity and <code>!important</code></h3>


<p>Selector specificity is a deep rabbit hole, and not the goal of this discussion. That said, to understand why <code>!important</code> exists, we need to look at how CSS decides which rules apply in the first place. I wrote a <a href="https://salehmubashar.com/blog/specificity-in-css-selectors" rel="noopener">brief overview</a> on specificity that serves as a good starting point. <a href="http://css-tricks.com/specifics-on-css-specificity/">Chris also has a concise piece</a> on it. And if you really want to go deep into all the edge cases, Frontend Masters has a <a href="https://frontendmasters.com/blog/css-specificity/" rel="noopener">thorough breakdown</a>.</p>



<p>In short, CSS gives each selector a kind of &#8220;weight.&#8221; When two rules target the same element, the rule with higher specificity wins. If the specificity is equal, the <strong>rule declared later in the stylesheet</strong> takes precedence.</p>



<ul class="wp-block-list">
<li><strong>Inline styles</strong> (<code>style="..."</code>) are the heaviest.</li>



<li><strong>ID selectors</strong> (<code>#header</code>) are stronger than classes or type selectors.</li>



<li><strong>Class, attribute, and pseudo-class selectors</strong> (<code>.btn</code>, <code>[type="text"]</code>, <code>:hover</code>) carry medium weight.</li>



<li><strong>Type selectors and pseudo-elements</strong> (<code>div</code>, <code>p</code>, <code>::before</code>) have the lowest weight. Although, the <code>*</code> selector is even lower with a specificity of 0-0-0 compared to type selectors which have a specificity of 0-0-1.</li>
</ul>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Low specificity (0,0,1) */
p {
  color: gray;
}

/* Medium specificity (0,1,0) */
.button {
  color: blue;
}

/* High specificity (1,1,0) */
#header .button {
  color: red;
}</code></pre>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;!-- Inline style (1,0,0) -->
&lt;p style="color: green;">Hello&lt;/p></code></pre>



<p>Inline styles being the heaviest also explains why they’re often frowned upon and not considered “clean” CSS since they bypass most of the normal structure we try to maintain.</p>



<p><code>!important</code> changes this behavior. It skips normal specificity and source order, pushing that declaration to the top within its origin and cascade layer:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">p {
  color: red !important;
}

#main p {
  color: blue;
}</code></pre>



<p>Even though <code>#main p</code> is more specific, the paragraph will appear red because the <code>!important</code> declaration overrides it.</p>


<h3 class="wp-block-heading" id="why-important-can-be-problematic">Why <code>!important</code> can be problematic</h3>


<p>Here’s the typical lifecycle of <code>!important</code> in a project involving multiple developers:</p>



<p>“Why isn’t this working? Add <code>!important</code>. Okay, fixed.”</p>



<p>Then someone else comes along and tries to change that same component. Their rule doesn’t apply, and after some digging, they find the <code>!important</code>. Now they have a choice:</p>



<ul class="wp-block-list">
<li>remove it and risk breaking something else,</li>



<li>or add another <code>!important</code> to override it.</li>
</ul>



<p>And since no one is completely sure why the first one was added, the safer move often feels like adding another one. This can quickly spiral out of control in larger projects.</p>



<p>On a more technical note, the fundamental problem with <code>!important</code> is that it breaks the intended order of the cascade. CSS is designed to resolve conflicts predictably through specificity and source order. Later rules override earlier ones, and more specific selectors override less specific ones.</p>



<p>A common place where this becomes obvious is theme switching. Consider the example below:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.button {
  color: red !important;
}

.dark .button {
  color: white;
}</code></pre>



<p>Even inside a dark theme, the button stays red. This results in the stylesheet becoming harder to reason about, because the cascade is no longer predictable.</p>



<p>In large teams, especially, this results in maintenance and debugging becoming harder. None of this means <code>!important</code> should never be used. There are legitimate cases for it, especially in utility classes, accessibility overrides, or user stylesheets. But if you’re using it as your go-to method to resolve a selector/styling conflict, it’s usually a sign that something else in the cascade needs attention.</p>



<p>Let’s look at alternatives.</p>


<h3 class="wp-block-heading" id="cascade-layers">Cascade layers</h3>


<p>Cascade layers are a more advanced feature of CSS, and there’s a <strong>lot</strong> of theory on them. For the purposes of this discussion, we’ll focus on how they help you avoid <code>!important</code>. If you want to learn more, <a href="https://css-tricks.com/author/miriam/">Miriam Suzanne</a> wrote a <a href="https://css-tricks.com/css-cascade-layers/">complete guide on CSS Cascade Layers</a> on it that goes into considerable detail.</p>



<p>In short, cascade layers let you define <strong>explicit priority groups</strong> in your CSS. Instead of relying on selector specificity, you decide up front which <em>category</em> of styles should take precedence. You can define your layer order up front:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@layer reset, defaults, components, utilities;</code></pre>



<p>This establishes priority from lowest to highest. Now you can add styles into those layers:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@layer defaults {
  a:any-link {
    color: maroon;
  }
}

@layer utilities {
  [data-color='brand'] {
    color: green;
  }
}</code></pre>



<p>Even though <code>[data-color='brand']</code> has lower specificity than <code>a:any-link</code>, the <code>utilities</code> layer takes precedence because it was defined later in the layer stack.</p>



<p>It’s worth noting that specificity still works <strong>inside</strong> a layer. But <strong>between</strong> layers, layer order is given priority.</p>



<p>With cascade layers, you can prioritize entire categories of styles instead of individual rules. For example, your “overrides” layer always takes precedence over your “base” layer. This sort of architectural thinking, instead of reactive fixing saves a lot of headaches down the line.</p>



<p>One very common example is integrating third-party CSS. If a framework ships with highly specific selectors, you can do this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@layer framework, components;

@import url('framework.css') layer(framework);

@layer components {
  .card {
    padding: 2rem;
  }
}</code></pre>



<p>Now your component styles automatically override the framework styles, regardless of their selector specificity, as long as the framework isn’t using <code>!important</code>.</p>



<p>And while we’re talking about it, it’s good to note that using <code>!important</code> <em>with</em> cascade layers is actually counterintuitive. That’s because <code>!important</code> actually <em>reverses</em> the layer order. It is no longer a quick way to jump to the top of the priorities — but an integrated part of our cascade layering; a way for lower layers to <em>insist</em> that some of their styles are essential.</p>



<p>So, if we were to order a set of layers like this:</p>



<ol class="wp-block-list">
<li><code>utilities</code> (most powerful)</li>



<li><code>components</code></li>



<li><code>defaults</code> (least powerful)</li>
</ol>



<p>Using <code>!important</code> flips things on their head:</p>



<ol class="wp-block-list">
<li><code>!important defaults</code> (most powerful)</li>



<li><code>!important components</code></li>



<li><code>!important utilities</code></li>



<li>normal <code>utilities</code></li>



<li>normal <code>components</code></li>



<li>normal <code>defaults</code> (least powerful)</li>
</ol>



<p>Notice what happens there: it generates three new, reversed important layers that supersede the original three layers while reversing the entire order.</p>


<h3 class="wp-block-heading" id="the-is-pseudo">The <code>:is()</code> pseudo</h3>


<p>The <a href="https://css-tricks.com/almanac/pseudo-selectors/i/is/"><code>:is()</code></a> pseudo-class is interesting because it takes the specificity of its most specific argument. Say you have a component that needs to match the weight of a more specific selector elsewhere in the codebase:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* somewhere in your styles */
#sidebar a {
  color: gray;
}

/* your component */
.nav-link {
  color: blue;
}</code></pre>



<p>Rather than using <code>!important</code>, you can bump <code>.nav-link</code> up by wrapping it in <code>:is()</code> with a more specific argument:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:is(#some_id, .nav-link) {
  color: blue;
}</code></pre>



<p>Now this has id-level specificity while matching only <code>.nav-link</code>. It&#8217;s worth noting that the selector inside <code>:is()</code> doesn’t have to match an actual element. We’re using <code>#some_id</code> purely to increase specificity in this case.</p>



<p><strong>Note:</strong> If <code>#some_id</code> actually exists in your markup, this selector would also match that element. So it would be best to use an id not being used to avoid side effects.</p>



<p>On the flip side, <a href="https://css-tricks.com/almanac/pseudo-selectors/w/where/"><code>:where()</code></a> does the opposite. It always resolves to a specificity of (0,0,0), no matter what&#8217;s inside it. This is handy for reset or base styles where you want <em>anything</em> downstream to override easily.</p>


<h3 class="wp-block-heading" id="doubling-up-a-selector">Doubling up a selector</h3>


<p>A pretty straightforward way of increasing a selectors specificity is repeating the selector. This is usually done with classes. For example:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.button {
  color: blue;
}

.button.button {
  color: red;  /* higher specificity */
}</code></pre>



<p>You would generally not want to do this too often as it can become a readability nightmare.</p>


<h3 class="wp-block-heading" id="reordering">Reordering</h3>


<p>CSS resolves ties in specificity by source order, so a rule that comes later is prioritized. This is easy to overlook, especially in larger stylesheets where styles are spread across multiple files.</p>



<p>If a more generic rule keeps overriding a more targeted one and the specificity is the same, check whether the generic rule is being loaded after yours. Flipping the order can fix the conflict without needing to increase specificity.</p>



<p>This is also why it&#8217;s worth thinking about stylesheet organization from the start. A common pattern is to go from generic to specific (resets and base styles first, then layout, then components, then utilities).</p>


<h3 class="wp-block-heading" id="when-using-important-does-make-sense">When using <code>!important</code> does make sense</h3>


<p>After all that, it&#8217;s worth being clear: <code>!important</code> does have legitimate use cases. <a href="https://css-tricks.com/when-using-important-is-the-right-choice/">Chris discussed this a while back too</a>, and the comments are worth a read too.</p>



<p>The most common case is utility classes. For example, the whole point of classes like <code>.visually-hidden</code> is that they do one thing, everywhere. In this cases, you don&#8217;t want a more specific selector quietly undoing it somewhere else. The same is true for state classes like <code>.disabled</code> or generic component styles like <code>.button</code>.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.visually-hidden {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
  clip-path: inset(50%) !important;
}</code></pre>



<p>Third-party overrides are another common scenario. <code>!important</code> can be used here to either override inline styles being set in JavaScript or normal styles in a stylesheet that you can’t edit.</p>



<p>From an accessibility point of view, <code>!important</code> is irreplaceable for user stylesheets. Since these are applied on all webpages and there’s virtually no way to guarantee if the stylesheets’ selectors will always have the highest specificity, <code>!important</code> is basically the only reliable way to make sure your styles always get precedence.</p>



<p>Another good example is when it comes to respecting a user&#8217;s browser preferences, such as <a href="https://css-tricks.com/almanac/rules/m/media/prefers-reduced-motion/">reducing motion</a>:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}</code></pre>


<h3 class="wp-block-heading" id="wrapping-up">Wrapping up</h3>


<p>The difference between good and bad use of <code>!important</code> really comes down to intent. Are you using it because you understand the CSS Cascade and have made a call that this declaration should always apply? Or are you using it as a band-aid? The latter will inevitably cause issues down the line.</p>


<h3 class="wp-block-heading" id="more-resources">Further reading</h3>


<ul class="wp-block-list">
<li><a href="https://css-tricks.com/specifics-on-css-specificity/">Specifics on CSS Specificity</a> by Chris Coyier</li>



<li><a href="https://frontendmasters.com/blog/css-specificity/" rel="noopener">Tackling CSS Specificity</a> by Emma Bostian</li>



<li><a href="https://css-tricks.com/css-cascade-layers">Cascade Layers Guide</a> by Miriam Suzanne</li>



<li><a href="https://css-tricks.com/when-using-important-is-the-right-choice/">When Using !important is The Right Choice</a> by Chris Coyier</li>
</ul>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/alternatives-to-the-important-keyword/">Alternatives to the !important Keyword</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/alternatives-to-the-important-keyword/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392696</post-id>	</item>
		<item>
		<title>Looking at New CSS Multi-Column Layout Wrapping Features</title>
		<link>https://css-tricks.com/css-multi-column-layout-wrapping-features/</link>
					<comments>https://css-tricks.com/css-multi-column-layout-wrapping-features/#comments</comments>
		
		<dc:creator><![CDATA[Abhishek Pratap Singh]]></dc:creator>
		<pubDate>Mon, 06 Apr 2026 13:55:49 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[css properties]]></category>
		<category><![CDATA[layout]]></category>
		<category><![CDATA[multi-column layout]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392361</guid>

					<description><![CDATA[<p>Chrome 145 introduces the <code>column-height</code> and <code>column-wrap</code> properties, enabling us to wrap the additional content into a new row below, creating a vertical scroll instead of a horizontal scroll.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/css-multi-column-layout-wrapping-features/">Looking at New CSS Multi-Column Layout Wrapping Features</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Multi-column layouts have not been used to their full potential, mostly because once content exceeded a limit, multi-column would force a horizontal scroll. It’s unintuitive and a UX no-no, especially on the modern web where the default scroll is vertical.</p>



<span id="more-392361"></span>



<p>Take the following case for example:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_ogLadmd" src="//codepen.io/anon/embed/ogLadmd?height=550&amp;theme-id=1&amp;slug-hash=ogLadmd&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed ogLadmd" title="CodePen Embed ogLadmd" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>The CSS code for that might look something like this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">body {
  max-width: 700px;
}

.article {
  column-gap: 10px;
  column-count: 3;
  height: 350px;
}</code></pre>



<p>When the content size exceeds the body container, multi-column creates additional columns and a horizontal scroll. However, we finally have the tools that have recently landed in Chrome that “fix” this without having to resort to trickier solutions.</p>



<p><a href="https://developer.chrome.com/blog/chrome-145-beta" rel="noopener">Chrome 145</a> introduces the <code>column-height</code> and <code>column-wrap</code> properties, enabling us to wrap the additional content into a new row below, creating a vertical scroll instead of a horizontal scroll.&nbsp;</p>



<p>So, now we can do something like this in Chrome 145+:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">body {
  max-width: 700px;
}

.article {
  column-gap: 10px;
  column-count: 3;
  column-wrap: wrap;
  height: 350px;
}</code></pre>



<p>And we get this nice multi-column layout that maintains the <code>column-count</code>:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="927" height="531" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/multicolumn-04.png?resize=927%2C531" alt="Multi-column layout example showing three columns from left to right." class="wp-image-392370" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/multicolumn-04.png?w=927&amp;ssl=1 927w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/multicolumn-04.png?resize=300%2C172&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/multicolumn-04.png?resize=768%2C440&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>This effectively transforms Multi-Column layouts into 2D Flows, helping us create a more web-appropriate scroll.</p>



<p class="is-style-explanation">&#x26a0;&#xfe0f; <strong>Browser Support:</strong> As of April 2026, <code><a href="https://caniuse.com/mdn-css_properties_column-wrap" rel="noopener">column-wrap</a></code> and <code><a href="https://caniuse.com/mdn-css_properties_column-height" rel="noopener">column-height</a></code> are available in Chrome 145+. Firefox, Safari, and Edge do not yet support these properties.</p>


<h3 class="wp-block-heading" id="what-this-actually-solves">What this actually solves</h3>


<p>The new properties can be genuinely useful in several cases:</p>


<h4 class="wp-block-heading" id="fixedheight-content-blocks">Fixed-height content blocks</h4>


<p>This is probably one of the most useful use cases for these properties. If you&#8217;re working with content that has predictable or capped heights, like card grids where each card has a max-height, then this works beautifully.&nbsp;</p>



<p>Toggle between <code>column-wrap: wrap</code> and <code>column-wrap: nowrap</code> in the following demo (Chrome 145+ needed) to check the difference.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_RNRegrR" src="//codepen.io/anon/embed/RNRegrR?height=600&amp;theme-id=1&amp;slug-hash=RNRegrR&amp;default-tab=result" height="600" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed RNRegrR" title="CodePen Embed RNRegrR" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>In case you’re checking this in an unsupported browser, this is the <code>nowrap</code> layout:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1247" height="562" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-1.png?resize=1247%2C562" alt="Multi-column layout example of four cards components in a row with horizontal scrolling." class="wp-image-392363" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-1.png?w=1247&amp;ssl=1 1247w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-1.png?resize=300%2C135&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-1.png?resize=1024%2C461&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-1.png?resize=768%2C346&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>And this is the <code>wrap</code> layout:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1108" height="537" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-3.png?resize=1108%2C537" alt="Multi-column layout example of five cards components in a row that wraps to a second row." class="wp-image-392365" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-3.png?w=1108&amp;ssl=1 1108w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-3.png?resize=300%2C145&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-3.png?resize=1024%2C496&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-3.png?resize=768%2C372&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>Wrapping creates a much more seamless flow.&nbsp;</p>



<p>However, in case the content-per-card is unbalanced, then even with wrapping, it can lead to unbalanced layouts:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_gbMZjer" src="//codepen.io/anon/embed/gbMZjer?height=650&amp;theme-id=1&amp;slug-hash=gbMZjer&amp;default-tab=result" height="650" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed gbMZjer" title="CodePen Embed gbMZjer" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1153" height="568" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-5.png?resize=1153%2C568" alt="A broken multi-column layout of card components. Some cards are split into multiple cards because the content is unbalanced." class="wp-image-392367" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-5.png?w=1153&amp;ssl=1 1153w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-5.png?resize=300%2C148&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-5.png?resize=1024%2C504&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image-5.png?resize=768%2C378&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>


<h4 class="wp-block-heading" id="newspaperstyle-and-magazinestyle-layouts">Newspaper-style and Magazine-style layouts</h4>


<p>Another real life use case is when designing newspaper-style layouts or sections where you&#8217;re willing to set explicit container and column heights. As can be seen in the earlier example, the combination of column-height and column-wrap helps make the layout responsive for different screen sizes, while retaining a more intuitive flow of information.&nbsp;</p>


<h4 class="wp-block-heading" id="blockdirection-carousels">Block-direction carousels</h4>


<p>This is my personal favorite use case of the <code>column-wrap</code> feature! By setting the column height to match the viewport (e.g., <code>100dvh</code>), you can essentially treat the multi-column flow as a pagination system, where your content fills the height of the screen and then &#8220;wraps&#8221; vertically. When combined with <code><a href="https://css-tricks.com/almanac/properties/s/scroll-snap-type/">scroll-snap-type</a>: y mandatory</code>, you get a fluid, vertical page-flipping experience that handles content fragmentation without any manual clipping or JavaScript calculation.</p>



<p>Play around with the following demo and check it out for yourself. Unless you&#8217;re on Chrome 145+ you&#8217;ll get a horizontal scroll instead of the intended vertical.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_myEaGEW" src="//codepen.io/anon/embed/myEaGEW?height=450&amp;theme-id=1&amp;slug-hash=myEaGEW&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed myEaGEW" title="CodePen Embed myEaGEW" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<figure class="wp-block-video"><video height="1138" style="aspect-ratio: 1644 / 1138;" width="1644" controls src="https://css-tricks.com/wp-content/uploads/2026/02/Screen-Recording-2026-02-17-at-9.41.27-AM.mov" playsinline></video></figure>



<p>There is a bit of a drawback to this though: If the content on a slide is too long, <code>column-wrap</code> will make it flow vertically, but the flow feels interrupted by that imbalance.&nbsp;</p>


<h3 class="wp-block-heading" id="what-they-dont-solve">What they don’t solve</h3>


<p>While these properties are genuinely helpful, they are not one-stop solutions for all multi-column designs. Here are a few situations where they might not be the “right” approach.</p>


<h4 class="wp-block-heading" id="truly-dynamic-content">Truly dynamic content</h4>


<p>If the content height is unknown or unpredictable in advance (e.g., user-generated content, CMS-driven pages), then these properties are of little use. The design can still be wrapped vertically with the <code>column-wrap</code> property, however, the layout would remain unpredictable without a fixed column height.</p>



<p>It can lead to over-estimating the column height, leaving awkward gaps in the layout. Similarly, it can lead you to under-estimate the height, resulting in unbalanced columns. The fix here is then to use JS to calculate heights, which defeats the idea of a CSS-native solution.</p>


<h4 class="wp-block-heading" id="mediaqueryfree-responsiveness">Media-query-free responsiveness</h4>


<p>For a truly “responsive” layout, we still need to use media queries to adjust <code>column-count</code> and <code>column-height</code> for different viewport sizes. While the wrapping helps and creates incremental benefits for a CSS-native solution, it can only help adjust the overflow behavior. Hence, the dependency on media query persists when supporting varying screen sizes.</p>


<h4 class="wp-block-heading" id="complex-alignment-needs">Complex alignment needs</h4>


<p>If you need precise control over where items sit in relation to each other, <a href="https://css-tricks.com/complete-guide-css-grid-layout/">CSS Grid</a> is still a better option. While multi-column with wrapping gives you flow, it still lacks positioning control.</p>


<h3 class="wp-block-heading" id="comparing-alternatives">Comparing alternatives</h3>


<p>Let’s see how the multi-column approach compares with existing alternatives like CSS Grid, <a href="https://css-tricks.com/snippets/css/a-guide-to-flexbox/">CSS Flexbox</a>, and the <a href="https://css-tricks.com/masonry-watching-a-css-feature-evolve/">evolving CSS Masonry</a>, that offer similar layouts.</p>



<p>One key difference is that while grid and flexbox manage distinct containers, multi-column is the only system that can fragment a single continuous stream of content across multiple columns and rows. This makes it the best fit for presenting long-form content, like we saw in the newspaper layout example.</p>



<p>CSS Grid lets us control placement via the grid structure, making it great for <a href="https://css-tricks.com/snippets/css/css-grid-starter-layouts/">complex layouts requiring precise positioning</a> or following asymmetric designs, like dashboards or responsive image galleries that need to <a href="https://css-tricks.com/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit/"><code>auto-fit</code></a> according to the screen size.</p>



<p>Flexbox with wrapping is great for creating standard UI components like navigation bars and tag clouds that should wrap around on smaller screen sizes.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="778" height="183" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image.png?resize=778%2C183" alt="Multi-column layout showing a navigation of eight items where a second row wraps starting at the fifth items." class="wp-image-392362" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image.png?w=778&amp;ssl=1 778w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image.png?resize=300%2C71&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/image.png?resize=768%2C181&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p class="is-style-explanation"><strong>Note:</strong> Chrome is also <a href="https://bsky.app/profile/did:plc:kesmfbtx2loscqj7ktw5shtt/post/3lpcjcjn4w22r?ref_src=embed&amp;ref_url=https%253A%252F%252Fjeffbridgforth.com%252Fnotes%252Fflex-wrap-balance%252F" rel="noopener">experimenting</a> with a new <code>flex-wrap: balance</code> keyword that could provide more wrapping control as well.</p>



<p>CSS Grid and Flexbox with wrapping are both good fits for layouts where each item is independent. They work well with content of dynamic heights and provide better alignment control compared to a multi-column approach. However, multi-column with the updated properties has an edge when it comes to fragmentation-aware layouts as we’ve seen.</p>



<p>CSS Masonry, on the other hand, will be useful for interlocking items with varying heights. This makes it perfect for creating style boards (like Pinterest) that pack items with varying heights in an efficient and aesthetic manner. Another good use case is e-commerce websites that use a masonry grid for product displays because descriptions and images can lead to differing card heights.</p>


<h3 class="wp-block-heading" id="conclusion">Conclusion</h3>


<p>The new <code>column-wrap</code> and <code>column-height</code> properties supported in Chrome 145 could significantly increase the usability of multi-column layouts. By enabling 2D flows, we have a way to fragment content without losing the vertical scrolling experience.</p>



<p>That said, these features will not be a replacement for the structural precision of CSS Grid or the item-based flexibility of Flexbox. But they will fill a unique niche. As browser support continues to expand, the best way to approach multi-column layout is with an understanding of both its advantages and limitations. They won&#8217;t solve dynamic height issues or eliminate the need for media queries, but will allow flowing continuous content in a 2D space.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/css-multi-column-layout-wrapping-features/">Looking at New CSS Multi-Column Layout Wrapping Features</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/css-multi-column-layout-wrapping-features/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		<enclosure url="https://css-tricks.com/wp-content/uploads/2026/02/Screen-Recording-2026-02-17-at-9.41.27-AM.mov" length="3273030" type="video/quicktime" />

		<post-id xmlns="com-wordpress:feed-additions:1">392361</post-id>	</item>
		<item>
		<title>Making Complex CSS Shapes Using shape()</title>
		<link>https://css-tricks.com/complex-css-shapes-with-shape-function/</link>
					<comments>https://css-tricks.com/complex-css-shapes-with-shape-function/#comments</comments>
		
		<dc:creator><![CDATA[Temani Afif]]></dc:creator>
		<pubDate>Thu, 02 Apr 2026 13:58:24 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[clip-path]]></category>
		<category><![CDATA[CSS functions]]></category>
		<category><![CDATA[css shapes]]></category>
		<category><![CDATA[shapes]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392986</guid>

					<description><![CDATA[<p>Creating rectangles, circles, and rounded rectangles is the basic of CSS. Creating more complex CSS shapes such as triangles, hexagons, stars, hearts, etc. is more challenging but still a simple task if we rely on modern features.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/complex-css-shapes-with-shape-function/">Making Complex CSS Shapes Using shape()</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Creating rectangles, circles, and rounded rectangles is the basic of CSS. Creating more complex <a href="https://css-shape.com/" rel="noopener">CSS shapes</a> such as triangles, hexagons, stars, hearts, etc. is more challenging but still a simple task if we rely on modern features.</p>



<p>But what about those shapes having a bit of randomness and many curves?</p>



<span id="more-392986"></span>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1141" height="325" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225348617_image.png?resize=1141%2C325" alt="Three rectangular shapes with jagged, non-creating edges. the first is blue, then orange, then green." class="wp-image-392987" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225348617_image.png?w=1141&amp;ssl=1 1141w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225348617_image.png?resize=300%2C85&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225348617_image.png?resize=1024%2C292&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225348617_image.png?resize=768%2C219&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>A lot of names may apply here: random wavy, wiggly, blob, squiggly, ragged, torn, etc. Whatever you call them, we all agree that they are not trivial to create, and they generally belong to the SVG world or are created with tools and used as images. Thanks to the new <a href="https://css-tricks.com/almanac/functions/s/shape/"><code>shape()</code></a> function, we can now build them using CSS.</p>



<p>I won’t tell you they are easy to create. They are indeed a bit tricky as they require a lot of math and calculation. For this reason, I built a few generators from which you can easily grab the code for the different shapes.</p>



<ul class="wp-block-list">
<li><a href="https://css-generators.com/wavy-divider/" rel="noopener">Wavy Divider generator</a></li>



<li><a href="https://css-generators.com/fancy-frame/" rel="noopener">Fancy Frame generator</a></li>



<li><a href="https://css-generators.com/blob/" rel="noopener">Blob generator</a></li>
</ul>



<p>All you have to do is adjust the settings and get the code in no time. As simple as that!</p>



<p>While most of you may be tempted to bookmark the CSS generators and leave this article, I advise you to continue reading. Having the generators is good, but understanding the logic behind them is even better. You may want to manually tweak the code to create more shape variations. We will also see a few interesting examples, so stay until the end!</p>



<p class="is-style-explanation"><strong>Notice:</strong> If you are new to <code>shape()</code>, I highly recommend reading <a href="https://css-tricks.com/better-css-shapes-using-shape-part-1-lines-and-arcs/">my</a> <a href="https://css-tricks.com/better-css-shapes-using-shape-part-1-lines-and-arcs/">four</a><a href="https://css-tricks.com/better-css-shapes-using-shape-part-1-lines-and-arcs/">-part series</a> where I explain the basics. It will help you better understand what we are doing here.</p>


<h3 class="wp-block-heading" id="how-does-it-work-">How does it work?</h3>


<p>While many of the shapes you can create with my generators look different, all of them rely on the same technique: a lot of <code>curve</code> commands. The main trick is to ensure two adjacent <code>curve</code> create a smooth curvature so that the full shape appears as one continuous curve.</p>



<p>Here is a figure of what one curve command can draw. I will be using only one control point:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="919" height="279" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773222807669_image.png?resize=919%2C279" alt="A normal curve with a control point in the very center. The second shows another curve with control point veering towards the left, contorting the curve." class="wp-image-392988" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773222807669_image.png?w=919&amp;ssl=1 919w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773222807669_image.png?resize=300%2C91&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773222807669_image.png?resize=768%2C233&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>Now, let’s put two curves next to each other:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="770" height="404" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773223335364_image.png?resize=770%2C404" alt="A wavy curve with two control points, one point up and the other down forming a wave along three points." class="wp-image-392989" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773223335364_image.png?w=770&amp;ssl=1 770w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773223335364_image.png?resize=300%2C157&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773223335364_image.png?resize=768%2C403&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>The ending point of the first curve, E1, is the starting point of the second curve, S2. That point is placed within the segment formed by both the control points C1 and C2. That’s the criterion for having an overall smooth curve. If we don’t have that, we get a discontinued “bad” curve.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="733" height="398" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773223685809_image.png?resize=733%2C398" alt="A wavy curve with two control points. The second point is moved down and toward the right, bending the curves second wav in an undesired way." class="wp-image-392991" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773223685809_image.png?w=733&amp;ssl=1 733w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773223685809_image.png?resize=300%2C163&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>All we have to do is to randomly generate different curves while respecting the previous criterion between two consecutive curves. For the sake of simplicity, I will consider the common point between two curves to be the midpoint of the control points to have less randomness to deal with.</p>


<h3 class="wp-block-heading" id="creating-the-shapes">Creating the shapes</h3>


<p>Let’s start with the easiest shape, a random wavy divider. A random curve on one side.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1418" height="432" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225933449_image.png?resize=1418%2C432&#038;ssl=1" alt="A long blue rectangle with a jagged bottom edge." class="wp-image-392992" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225933449_image.png?w=1418&amp;ssl=1 1418w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225933449_image.png?resize=300%2C91&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225933449_image.png?resize=1024%2C312&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773225933449_image.png?resize=768%2C234&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>Two variables will control the shape: the granularity and the size. The granularity defines how many curves we will have (it will be an integer). The size defines the space where the curves will be drawn.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1129" height="342" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773226919188_image.png?resize=1129%2C342" alt="The same blue renctangle in two versions with two different jagged bottom edges, marked in red to show the shape. The first is labeled Granularity 8 and the second, with more and deeper jags, is labeled Granularity 18." class="wp-image-392994" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773226919188_image.png?w=1129&amp;ssl=1 1129w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773226919188_image.png?resize=300%2C91&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773226919188_image.png?resize=1024%2C310&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773226919188_image.png?resize=768%2C233&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>The first step is to create N points and evenly place them at the bottom of the element (N is the granularity).</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="665" height="244" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773227352470_image.png?resize=665%2C244" alt="A white rectangle with a black border and seven control points evenly spaced along the bottom edge." class="wp-image-392996" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773227352470_image.png?w=665&amp;ssl=1 665w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773227352470_image.png?resize=300%2C110&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>Then, we randomly offset the vertical position of the points using the size variable. Each point will have an offset equal to a random value within the range <code>[0 size]</code>.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="713" height="226" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773228225993_image.png?resize=713%2C226" alt="A white rectangle with a black border and seven control points evenly spaced in a wavy formation along the bottom edge. A red label saying Size indicates the vertical height between the highest point and lowest point." class="wp-image-392997" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773228225993_image.png?w=713&amp;ssl=1 713w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773228225993_image.png?resize=300%2C95&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>From there, we take two adjacent points and define their midpoint. We get more points.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="715" height="226" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773228252968_image.png?resize=715%2C226&#038;ssl=1" alt="A white rectangle with a black border and thirteen control points evenly spaced in a wavy formation along the bottom edge. A red label saying Size indicates the vertical height between the highest point and lowest point. Every even point is marked in blue." class="wp-image-392998" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773228252968_image.png?w=715&amp;ssl=1 715w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773228252968_image.png?resize=300%2C95&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>Do you start to see the idea? A first set of points is randomly placed while a second set is placed in a way that meets the criterion we defined previously. From there, we draw all the curves, and we get our shape.</p>



<p>The CSS code will look like this:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.shape {
  clip-path: shape(from Px1 Py1,
    curve to Px2 Py2 with Cx1 Cy1,
    curve to Px3 Py3 with Cx2 Cy2,
    /* ... */
    curve to Pxi Pyi with Cx(i-1) Cy(i-1)
    /* ... */
  )
}</code></pre>



<p>The <code>Ci</code> are the points we randomly place (the control points) and <code>Pi</code> are the midpoints.</p>



<p>From there, we apply the same logic to the different sides to get different variation (bottom, top, bottom-top, all sides, etc.).</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="726" height="525" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229121842_image.png?resize=726%2C525" alt="A two-by-two grid of the same blue rectangle with different configurations of wavy edges. The first on the bottom, the second on the top, the third on the top and bottom, and the fourth all along the shape." class="wp-image-393000" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229121842_image.png?w=726&amp;ssl=1 726w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229121842_image.png?resize=300%2C217&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>As for <a href="https://css-generators.com/blob/" rel="noopener">the blob</a>, the logic is slightly different. Instead of considering a rectangular shape and straight lines, we use a circle.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1138" height="463" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229324543_image.png?resize=1138%2C463&#038;ssl=1" alt="Two white circles with black borders that contain a smaller circle with a dashed border. The first circle has eight black control points around the outer circle evenly spaced. The second has 15 control points around it, even other one in blue and positioned between the outer and inner circles to form a wavy shape." class="wp-image-393002" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229324543_image.png?w=1138&amp;ssl=1 1138w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229324543_image.png?resize=300%2C122&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229324543_image.png?resize=1024%2C417&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229324543_image.png?resize=768%2C312&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>We evenly place the points around the circle (the one formed by the element if it has <code>border-radius: 50%</code>). Then, we randomly offset them closer to the center. Finally, we add the midpoints and draw the shape.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="549" height="341" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229605074_image.png?resize=549%2C341" alt="A large green blob shape." class="wp-image-393003" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229605074_image.png?w=549&amp;ssl=1 549w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773229605074_image.png?resize=300%2C186&amp;ssl=1 300w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>We can still go fancier and combine the first technique with the circular one to consider a rectangle with rounded corners.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1114" height="348" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773230703420_image.png?resize=1114%2C348" alt="A blue rounded rectangle next to another version of itself with a large number of jagged edges all around it." class="wp-image-393005" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773230703420_image.png?w=1114&amp;ssl=1 1114w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773230703420_image.png?resize=300%2C94&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773230703420_image.png?resize=1024%2C320&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/s_8249102E6BD5A7D94BC02F00D913B3B576539AF394F7402EF7DE7C968CBF8D3E_1773230703420_image.png?resize=768%2C240&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>This was the trickiest one to implement as I had to deal with each corner, each side, and work with different granularities. However, the result was quite satisfying as it allows us to create a lot <a href="https://css-generators.com/fancy-frame/" rel="noopener">of fancy frames</a>!</p>


<h3 class="wp-block-heading" id="show-me-the-cool-demos-">Show me the cool demos!</h3>


<p>Enough theory, let’s see some cool examples and how to simply use the generators to create complex-looking shapes and animations.</p>



<p>We start with a classic layout featuring numerous wavy dividers!</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_EayNMgo" src="//codepen.io/anon/embed/EayNMgo?height=450&amp;theme-id=1&amp;slug-hash=EayNMgo&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed EayNMgo" title="CodePen Embed EayNMgo" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>We have four shapes in that demo, and all of them are a simple copy/paste from <a href="https://css-generators.com/wavy-divider/" rel="noopener">the wavy divider generator</a>. The header uses the bottom configuration, the footer uses the top configuration and the other elements use the top + bottom configuration.</p>



<p>Let’s get fancy and add some animation.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_yyaVpMy/180506c933b34c7113fc26650622b300" src="//codepen.io/anon/embed/yyaVpMy/180506c933b34c7113fc26650622b300?height=450&amp;theme-id=1&amp;slug-hash=yyaVpMy/180506c933b34c7113fc26650622b300&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed yyaVpMy/180506c933b34c7113fc26650622b300" title="CodePen Embed yyaVpMy/180506c933b34c7113fc26650622b300" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>Each element will have the following code:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@media screen and (prefers-reduced-motion: no-preference) {
  .element {
    --s1: shape( ... );
    --s2: shape( ... );
    animation: dance linear 1.6s infinite alternate;
  }

  @keyframes dance {
    0% {clip-path: var(--s1)}
    to {clip-path: var(--s2)}
  }
}</code></pre>



<p>From the generator, you fix the granularity and size, then you generate two different shapes for each one of the variables (<code>--s1</code> and <code>--s2</code>). The number of curves will be the same, which means the browser can have an interpolation between both shapes, hence we get a nice animation!</p>



<p>And what about introducing scroll-driven animation to have the animation based on the scroll? All you have to do is add <a href="https://css-tricks.com/almanac/functions/s/scroll/"><code>animation-timeline: scroll()</code></a> and it’s done.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_yyaVpxO/b326aeaf99c07d19f05bac209aa49d5a" src="//codepen.io/anon/embed/yyaVpxO/b326aeaf99c07d19f05bac209aa49d5a?height=450&amp;theme-id=1&amp;slug-hash=yyaVpxO/b326aeaf99c07d19f05bac209aa49d5a&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed yyaVpxO/b326aeaf99c07d19f05bac209aa49d5a" title="CodePen Embed yyaVpxO/b326aeaf99c07d19f05bac209aa49d5a" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>Here is the same effect with a sticky header.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_RNRKjBm/ebcd464305e40a043f4390705afe541d" src="//codepen.io/anon/embed/RNRKjBm/ebcd464305e40a043f4390705afe541d?height=450&amp;theme-id=1&amp;slug-hash=RNRKjBm/ebcd464305e40a043f4390705afe541d&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed RNRKjBm/ebcd464305e40a043f4390705afe541d" title="CodePen Embed RNRKjBm/ebcd464305e40a043f4390705afe541d" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>For this one, you play with the size. You fix the granularity and the shape ID then you consider a size equal to <code>0</code> for the initial shape (a rectangle) and a size different from <code>0</code> for the wavy one. Then you let the browser animate between both.</p>



<p>Do you see all the possibilities we have? You can either use the shapes as static decorations or create fancy animations between two (or more) by using the same granularity and adjusting the other settings (size and shape ID).</p>



<p>What cool demo can you create using those tricks? Share it in the comment section.</p>



<p>I will leave you with more examples you can use as inspiration.</p>



<p>A bouncing hover effect with blob shapes:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_PwwJgyr" src="//codepen.io/anon/embed/PwwJgyr?height=450&amp;theme-id=1&amp;slug-hash=PwwJgyr&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed PwwJgyr" title="CodePen Embed PwwJgyr" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_yyyPONb" src="//codepen.io/anon/embed/yyyPONb?height=450&amp;theme-id=1&amp;slug-hash=yyyPONb&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed yyyPONb" title="CodePen Embed yyyPONb" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>A <a href="https://css-tip.com/squishy-button/" rel="noopener">squishy button</a> with a hover and click effect:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_ZYpLGvX" src="//codepen.io/anon/embed/ZYpLGvX?height=450&amp;theme-id=1&amp;slug-hash=ZYpLGvX&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed ZYpLGvX" title="CodePen Embed ZYpLGvX" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>A<a href="https://css-tip.com/wobbling-animation/" target="_blank" rel="noreferrer noopener">&nbsp;wobbling frame animation</a>:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_zxKzrKe" src="//codepen.io/anon/embed/zxKzrKe?height=450&amp;theme-id=1&amp;slug-hash=zxKzrKe&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed zxKzrKe" title="CodePen Embed zxKzrKe" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>A&nbsp;<a href="https://css-tip.com/sliding-liquid/" target="_blank" rel="noreferrer noopener">liquid reveal effect</a>:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_OPRZBxY" src="//codepen.io/anon/embed/OPRZBxY?height=450&amp;theme-id=1&amp;slug-hash=OPRZBxY&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed OPRZBxY" title="CodePen Embed OPRZBxY" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>And a set of <a href="https://css-loaders.com/squishy/" rel="noopener">fancy CSS loaders</a> you can find at my site.</p>


<h3 class="wp-block-heading" id="conclusion">Conclusion</h3>


<p>Do you see all the potential of the new <code>shape()</code> function? We now have the opportunity to create complex-looking shapes without resorting to SVG or images. In addition to that, we can easily have nice transition/animation.</p>



<p>Don’t forget to bookmark my <a href="https://css-generators.com/" rel="noopener">CSS Generators</a> website, from where you can get the code of the shapes we studied and more. I also have the <a href="https://css-shape.com/" rel="noopener">CSS Shape</a> website which I will soon update to utilize the new <code>shape()</code> for most of the shapes and optimize a lot of old code!</p>



<p>What about you? Can you think about a complex shape we can create using <code>shape()</code>? Perhaps you can give me the idea for my next generator!</p>



<p>Let&#8217;s end this article with a fun demo. A CSS-only art of the one and only Chris Coyier:</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_qEaLagJ" src="//codepen.io/anon/embed/qEaLagJ?height=450&amp;theme-id=1&amp;slug-hash=qEaLagJ&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed qEaLagJ" title="CodePen Embed qEaLagJ" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>Each drawing is a single div and a single <code>shape()</code> implementation. I relied on my <a href="https://css-generators.com/svg-to-css/" rel="noopener">SVG-to-CSS converter</a> and other tools to generate the code, but it&#8217;s still a cool demo to illustrate the power of the <code>shape()</code> function.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/complex-css-shapes-with-shape-function/">Making Complex CSS Shapes Using shape()</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/complex-css-shapes-with-shape-function/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392986</post-id>	</item>
		<item>
		<title>Front-End Fools: Top 10 April Fools’ UI Pranks of All Time</title>
		<link>https://css-tricks.com/front-end-april-fools-top-10/</link>
					<comments>https://css-tricks.com/front-end-april-fools-top-10/#comments</comments>
		
		<dc:creator><![CDATA[Lee Meyer]]></dc:creator>
		<pubDate>Wed, 01 Apr 2026 14:00:26 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[news]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=392794</guid>

					<description><![CDATA[<p>These are the historical pranks I consider the top 10 most noteworthy, rather than the “best.” You’ll see that some of them crossed the line and/or backfired.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/front-end-april-fools-top-10/">Front-End Fools: Top 10 April Fools’ UI Pranks of All Time</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>April Fools’ Day pranks on the web imply that we’re not trying to fool each other every day in web design anyway. Indeed, one of my <a href="https://css-tricks.com/scrollytelling-on-steroids-with-scroll-state-queries/#comment-1883701">favorite comments</a> I received on an article was, “I can’t believe my eyes!” You shouldn’t, since <a href="https://www.interaction-design.org/literature/topics/illusions-in-design?srsltid=AfmBOorUffn89UTjtk5jgvNVMfKFhVTuSlTciyAOjyGon8dDF0rbgUU_#1._understand_the_principles_of_perception-11" rel="noopener">web design relies on fooling the user’s brain</a> by manipulating the way we process visual information via <a href="https://www.interaction-design.org/literature/topics/gestalt-principles" rel="noopener">Gestalt laws</a>, which make a website feel real.</p>



<p>April Fools’ Day on the web exemplifies what philosopher <a href="https://en.wikipedia.org/wiki/Jean_Baudrillard" rel="noopener">Jean Baudrillard</a> called a <a href="https://www.ipl.org/essay/Jean-Boaudillards-Theory-Of-Jean-Baaudrillard-Theory-FKL6SC5K6CED6" rel="noopener">deterrence machine</a> — a single day on the calendar to celebrate funny fake news is like a theme park designed to make the fake constructs beyond its gates seem real by comparison. And oftentimes, the online pranks on April 1st are indistinguishable from the <a href="https://www.bbc.com/news/uk-56597184" rel="noopener">bizarreness</a> that ensues all year round in the “real” virtual world.</p>



<span id="more-392794"></span>


<h3 class="wp-block-heading" id="real-things-that-looked-like-april-fools-pranks">Real things that looked like April Fools’ pranks</h3>


<p>Tech has a history of April Fools’ Day announcements that remind me of what Philip K. Dick called “fake fakes,” emerging every year like <a href="https://philipdick.com/mirror/essays/How_to_Build_a_Universe.pdf" rel="noopener">real animals surreptitiously replacing the fake ones at Disneyland</a>.</p>



<p>For instance, in 2004, people famously thought Gmail <a href="https://en.wikipedia.org/wiki/List_of_Google_April_Fools%27_Day_jokes#2004" rel="noopener">was an April Fools’ joke</a> since it was announced on April 1st.</p>



<p>And on April Fools’ Day in 2013, long before the current generation of AI, <a href="https://tom7.org/" rel="noopener">Tom Murphy</a> announced <a href="https://tom7.org/mario/" rel="noopener">an AI that learns to play NES games</a>. It was the real deal, even though he published the research paper and source code on “<a href="http://sigbovik.org/2013" rel="noopener">SIGBOVIK 2013</a>, an April 1st conference that usually publishes fake research. Mine is real!” In Tom’s <a href="https://www.youtube.com/watch?v=xOCurBYI_gY" rel="noopener">demo</a>, the AI even devised the strategy of indefinitely pausing Tetris, because in that game on NES, “The only way to win is <a href="https://en.wikipedia.org/wiki/Tetris_(NES_video_game)#Gameplay" rel="noopener">not to play</a>.”</p>



<p>To give a more personal example of real tech that could be mistaken for an April Fools’ joke, my article on <a href="https://css-tricks.com/worlds-collide-keyframe-collision-detection-using-style-queries/">pure CSS collision detection</a> was published on April 1st, 2025, <a href="https://www.google.com/search?q=march+31st+US+time+in+AEST&amp;newwindow=1&amp;sca_esv=3d646b66db581d96&amp;biw=1163&amp;bih=605&amp;sxsrf=ANbL-n5k_h4tZKn5GJXplIYnR5lxWqmjKg%3A1772227081965&amp;ei=CQqiabnZOraTseMP86WBwAk&amp;ved=0ahUKEwj5gbqtzPqSAxW2SWwGHfNSAJgQ4dUDCBE&amp;uact=5&amp;oq=march+31st+US+time+in+AEST&amp;gs_lp=Egxnd3Mtd2l6LXNlcnAiGm1hcmNoIDMxc3QgVVMgdGltZSBpbiBBRVNUMgUQIRigATIFECEYoAEyBRAhGKABMgQQIRgVSMtdUJEZWO9ZcAl4AZABAJgB-wGgAe4pqgEGMC4yNi41uAEDyAEA-AEBmAIkoAL2KsICCxAuGIAEGJECGIoFwgILEAAYgAQYkQIYigXCAgsQABiABBixAxiDAcICCBAAGIAEGLEDwgIOEC4YgAQYsQMYgwEYigXCAg4QLhiABBixAxjRAxjHAcICDhAAGIAEGLEDGIMBGIoFwgIKEC4YgAQYQxiKBcICChAAGIAEGEMYigXCAhcQLhiABBixAxiDARjHARiKBRiOBRivAcICFBAuGIAEGLEDGIMBGMcBGI4FGK8BwgIFEC4YgATCAggQLhiABBixA8ICDRAuGIAEGLEDGEMYigXCAhMQLhiABBixAxjRAxhDGMcBGIoFwgILEC4YgAQYsQMYgwHCAhEQABiABBiRAhixAxiDARiKBcICEBAAGIAEGLEDGEMYgwEYigXCAgQQABgDwgIFEAAYgATCAgYQABgWGB7CAgcQIRigARgKmAMAiAYBkgcGNS4yNS42oAfPjQKyBwYwLjI1Lja4B-AqwgcGMS4yNi45yAdXgAgA&amp;sclient=gws-wiz-serp" rel="noopener">my local time</a>. I was amused when someone <a href="https://css-tricks.com/worlds-collide-keyframe-collision-detection-using-style-queries/#comment-1882402">commented</a> that using <code>min</code> to detect if a paddle was in range of a ball seemed like a clever hack that &#8220;brings up the question: <em>Should game logic be done in CSS?”</em> Of course it shouldn’t! I wasn’t seriously proposing this as the future of web game development.</p>



<p>I replied that if the commenter can take the idea seriously for a minute, it’s a testament to how far CSS has come as a language. It seems even funnier in hindsight, now that <a href="https://css-tricks.com/the-range-syntax-has-come-to-container-style-queries-and-if/">the range syntax has come to style queries</a>, meaning we no longer need the <code>min</code> hack. So, maybe everyone should make games in CSS now, if the <code>min</code> hack was the only deal breaker (I kid because I love).</p>



<p><a href="https://codepen.io/leemeyer/pen/ZYEJQNO" rel="noopener">My CSS collision detection demo</a> had a resurgence in popularity recently, when Chris Coyier <a href="https://x.com/CodePen/status/2006806143213973723">chose it as a picked Pen</a>. And in that CodePen, a comment again made me laugh: “Can it be multiplayer/online?” Yet, once I stopped laughing, I found myself trying to get a multiplayer mode working. Whether I can or not, I guess the joke’s on me for taking CSS hacking too seriously.</p>



<p>The thing is, much of what <a href="https://rentahuman.ai/" rel="noopener">we have</a> on the web this year seemed unthinkable last year.</p>



<p>Even the story of the origin of April Fool’s Day sounds like a geeky April Fools’ joke — the leading theory is that the 15th-century equivalent of the <a href="https://en.wikipedia.org/wiki/Year_2000_problem" rel="noopener">Y2K bug</a> had some foolish people incorrectly celebrating the new year on April 1st when the Pope changed the calendars in France from the Julian Calendar to the Gregorian Calendar. And — April Fools’ again! — <a href="https://www.snopes.com/fact-check/april-fools39-day-origins/" rel="noopener">that’s a legend nobody has been able to prove happened</a>.</p>



<p>But whichever way you feel about the constant disruptions at the heart of the evolution of tech, the disruptions work like pranks by flipping common narratives on their heads in the same way April Fools&#8217; Day does. With that in mind, let’s go through history with an eye for exploring <a href="https://hcspire.com/2025/03/28/why-theres-a-little-truth-in-every-joke/" rel="noopener">the core of truth inside the jokes</a> of April Fools’ Days passed.</p>



<p class="is-style-explanation"><strong>Note:</strong> These are the historical pranks I consider the top 10 most noteworthy, rather than the “best.” You’ll see that some of them crossed the line and/or backfired.</p>


<h3 class="wp-block-heading" id="google-april-fools-games">Google April Fools’ games</h3>


<p>Google is <a href="https://en.wikipedia.org/wiki/List_of_Google_April_Fools%27_Day_jokes" rel="noopener">famous for its April Fools’ pranks</a>, but they’ve also historically blurred the line between pranks and features. For example, on April 1st 2019, Google introduced a <a href="https://au.lifehacker.com/news/79649/how-to-turn-google-calendar-into-space-invaders" rel="noopener">temporary easter egg</a> that transformed Google Calendar into a Space Invaders game. It was such a cool “joke” that nowadays, there’s a <a href="https://eieio.games/blog/breaktime/" rel="noopener">Chrome extension</a> that offers a similar experience, turning your Google Calendar into a Breakout game. This extension also offers the option to actually delete items that your ball hit from your calendar at the end of a game.</p>



<p>On April Fools&#8217; Day the same year as the original calendar game, Google also released a feature that allowed Google Maps users to <a href="https://youtu.be/LRfGo7LTjro?si=BXLRGG7pSuMAgP_4&amp;t=68" rel="noopener">play Snake on maps</a>.</p>



<p class="is-style-explanation"><strong>Personal Sidenote:</strong> The Google gag inspired an unreleased game I once made with an overworld that’s a gamified calendar, in which your character is trying to avoid an abusive partner by creating excuses not to be at home at the same time as their partner, but that’s a little dark for April Fools’.</p>


<h3 class="wp-block-heading" id="prank-npm-packages">Prank npm packages</h3>


<p>In March 2016, a legit — if arguably trivial — eleven-line package was deleted from the npm registry after its creator decided to boycott npm. Turns out that deletion <a href="https://en.wikipedia.org/wiki/Npm_left-pad_incident" rel="noopener">disrupted big companies whose code relied on the <code>left-pad</code> package</a> and this prompted npm to <a href="https://en.wikipedia.org/wiki/Npm_left-pad_incident#Reactions" rel="noopener">change its policies on which packages can be deleted</a>. I mention this because the humour of the npm packages released as jokes often revolves around poking fun at JavaScript developers’ overuse of dependencies that might not be needed.</p>



<p><a href="https://www.npmjs.com/package/vanilla-javascript" rel="noopener">Here is a 0kb npm package called <code>vanilla-javascript</code></a> and a <a href="http://vanilla-js.com/" rel="noopener">page for the Vanilla JS “framework”</a> that is always 0kb, no matter which features you add to the “bundle.&#8221; It lists all the JavaScript frameworks as “plugins.” Some of the dependent packages for <code>vanilla-javascript</code> are quite funny. I like <a href="https://www.npmjs.com/package/@falsejs/falsejs" rel="noopener">false-js</a>, which ensures <code>true</code> and <code>false</code> are defined properly. The library can be initialized with the settings <code>disableAprilFoolsSideEffects</code>, <code>definitelyDisableAprilFoolsSideEffects</code>, and <code>strictDisableAprilFoolsSideEffectsCheck</code>. If you read the source code, there is a <a href="https://github.com/10xly/FalseJS/blob/universe/aprilFoolsCalculateFalse.js" rel="noopener">comment</a> saying, “Haha, this code is obfuscated, you&#8217;ll never figure out what happens on April Fools.”</p>



<p>There is also this useless <a href="https://www.npmjs.com/package/get-current-day" rel="noopener">library</a> to get the current day. It seems plausible till you look carefully at the <a href="https://marmelab.com/get-current-day/" rel="noopener">website</a> and the description: “<em>This package is ephemeral for April Fools&#8217; Day and will be removed at some point.</em>“ The testimonials from fictional time-traveling characters are also a bit of a giveaway, and you have to love that he updated it every day for months, “because&#8230; why not? &#x1f937;&#x200d;&#x2642;&#xfe0f;”</p>



<p>More &#8220;terrible npm packages&#8221; for April Fools&#8217; are <a href="https://davidlozzi.com/2022/04/01/completely-terrible-npm-packages/" rel="noopener">here</a>.</p>


<h3 class="wp-block-heading" id="-aprilfools-css-https-gist-github-com-steveosoule-5295646-">aprilFools.css</h3>


<p>There’s another category of dependencies that are functional but used for <em>playing</em> April Fools pranks. For instance, <a href="https://gist.github.com/steveosoule/5295646" rel="noopener">aprilFools.css</a> by Wes Bos, which has a comment at the top saying:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/*
  I assume no responsibility for angry co-workers or lost productivity
  Put these CSS definitons into your co-workers Custom.css file.
  They will be applied to every website they visit as well as their developer tools.
*/</code></pre>



<p>It does things like use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/transform" rel="noopener">CSS transforms</a> to turn the page upside down.</p>



<p>It strikes me that following the advice in the comments could be a slippery slope to a dark place of workplace bullying, if you were to try it on the wrong coworker, just because they left their computer unlocked. As Chris Coyier pointed out in <a href="https://css-tricks.com/practical-jokes-in-the-browser/">his post on practical jokes in the browser</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>&#8220;Fair warning on this stuff&#8230; you gotta be tasteful. Putting someone’s stapler in the jello is pretty hilarious unless it’s somehow a family heirloom, or it’s someone who’s been the target of a little too much office prankery to the point it isn’t funny anymore.&#8221;</p>
</blockquote>


<h3 class="wp-block-heading" id="april-fool-s-pranks-using-vs-code-extensions">April Fool’s pranks using VS Code Extensions</h3>


<p>While we’re on the topic of behavior that blurs the line between pranks and workplace bullying, let’s talk about <a href="https://justcodeit.medium.com/april-fools-pranks-with-vs-code-extensions-701a0d35647a" rel="noopener">this list of VS Code Extensions</a> that could be used to prank a coworker by causing their code editor UI to behave unexpectedly. Most of the examples sound funny and harmless, like having the IDE intermittently pop up “Dad Jokes” or make funny sounds when typing. Changing the code editor to resemble Slack using a theme is also funny.</p>



<p>Then there’s the last example that made me do a double-take: “Imagine hitting <kbd>CTRL</kbd> + <kbd>S</kbd> to save your work and then it gets erased!” Yeah, if I were interviewing someone and they mentioned they consider this a funny joke, I would end the interview there. And if anyone ever does this to me, I’m going to HR.</p>


<h3 class="wp-block-heading" id="pranks-by-the-w3c">Pranks by the W3C</h3>


<p>I don’t think of the W3C as having a sense of humor, although I guess getting me excited about <a href="https://www.w3.org/TR/html-imports/" rel="noopener">HTML imports</a> back in the day, only to discontinue them, was funny in hindsight, if you have a dark sense of humor. Nevertheless, they have posted pranks on their official website, such as restyling to make their <a href="https://www.youtube.com/watch?v=gbiv-b5uVso" rel="noopener">page look like a nineties GeoCities website in 2012</a>, or claiming they were <a href="https://www.w3.org/press-releases/2021/blink/" rel="noopener">reviving the <code>&lt;blink&gt;</code> tag in 2021</a>. There’s a theme of playing on the nostalgia of people my age who want these things to be real.</p>



<p class="is-style-explanation"><strong>Sidenote:</strong> If you want more Nineties internet experiences, the game <a href="https://www.youtube.com/watch?v=i5EWQSW4Nd8" rel="noopener">Hypnospace Outlaw</a>, set on a retro internet in an alternative 1999, might be up your alley.</p>



<p>Other sites over the years have played a similar joke, which can never fail to charm an old-timer like me who remembers using a web like this at the public library, back when the internet was too expensive for my family to afford at home.</p>


<h3 class="wp-block-heading" id="stackoverflow-retro-restyle">StackOverflow retro restyle</h3>


<p>I can’t get enough of these nostalgia trips, so here’s what StackOverflow looked like on <a href="https://www.reddit.com/r/web_design/s/eKBc3NkvpT" rel="noopener">April Fools&#8217; Day in 2019</a>. They turned the site &#8220;full GeoCities&#8221; for fun. Yet everything comes full circle. Now StackOverflow itself <a href="https://www.ericholscher.com/blog/2025/jan/21/stack-overflows-decline/" rel="noopener">seems destined to become as fossilized as GeoCities</a>. Even so, the site is currently attempting a new, real redesign to <a href="https://stackoverflow.blog/2026/02/25/your-sneak-peek-at-the-redesigned-stack-overflow/" rel="noopener">survive rather than for fun</a>. It&#8217;s sobering to consider that maybe the only StackOverflow experience for the next generation of coders will be if ChatGPT gets a StackOverflow restyle on a future April Fools&#8217;.</p>


<h3 class="wp-block-heading" id="stack-egg">Stack Egg</h3>


<p>While we’re on the topic of StackOverflow, their <a href="https://balpha.de/2015/04/the-making-of-stackegg/" rel="noopener">Stack Egg prank</a> from 2015 was very cool, but it might win my award for the most over-engineered April Fools’ prank that caused the most serious problems for a website. The premise was another Nineties throwback, this time to the nineties <a href="https://en.wikipedia.org/wiki/Tamagotchi" rel="noopener">Tamagotchi</a> craze.</p>



<p>The idea, as the creator describes it, was that every site on the Stack Exchange network would have its own “Stack Egg,” representing that site. The goal was to collaboratively keep your metaphorical “site” alive using hypothetical actions named after real actions on the site, such as upvotes to feed the Tamagotchi, and review actions to clean up the poop so the Tamagotchi doesn’t get sick.</p>



<p>It was a nifty concept, although like Google’s April Fools’ games, it’s more neat than laugh-out-loud funny. The part that does make me laugh — I don’t feel too guilty saying it since it was more than a decade ago — was that this is a game about keeping the websites alive, and it inadvertently <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" rel="noopener">DDoS-ed</a> its <a href="https://meta.stackoverflow.com/questions/289044/is-stackegg-causing-me-problems-or-something-else/289045#289045" rel="noopener">own websites and took down the whole StackExchange network</a>.</p>



<p>And yet, the creators thought the fact that they had the foresight to implement a feature flag that allowed switching it off meant this was a case study in “<a href="https://cacm.acm.org/practice/operational-excellence-in-april-fools-pranks/" rel="noopener">Operational Excellence in AFPs (April Fools’ Pranks)</a>.” Yep, that is an actual article published in a peer-reviewed journal. According to the article, the engineers involved pushed a fix about two hours later to salvage the prank. <a href="https://meta.stackexchange.com/a/252403" rel="noopener">Code Golf was the winner of the game</a>, in case you’re wondering. According to the same post that announced the winner, “it&#8217;s by no means designed to withstand exploits,” and in the two days the feature was live, users discovered a vulnerability that was “close to <a href="https://blog.stackoverflow.com/2008/12/vote-fraud-and-you/" rel="noopener">voting fraud</a>.”</p>



<p>I mentioned the over-engineering, so here’s the part that makes the unintentional punchline even funnier: rather than investing more time guarding against the basics, such as not bringing down the website and considering security, the creator spent time making his own <a href="https://en.wikipedia.org/wiki/Turing_completeness" rel="noopener">Turing-complete</a> language to handle the LCD-style <a href="https://stackexchange.github.io/stackegg/" rel="noopener">animations</a>, <a href="https://stackexchange.github.io/stackegg/" rel="noopener"></a>“because I wanted to! Creating a programming language is fun.”</p>



<p>That’s such a classically geeky way to prioritize!</p>


<h3 class="wp-block-heading" id="google-mic-drop">Google Mic Drop</h3>


<p>If Stack Egg created the most issues I’ve ever heard of for a website that created the prank, the most mean-spirited high-profile UI prank — which caused the most problems for users — has to be <a href="https://blog.google/products-and-platforms/products/gmail/introducing-gmail-mic-drop/" rel="noopener">Google Mic Drop</a>. It dropped (pun intended) on April Fools’ Day 2016, shortly after Google <a href="https://www.engadget.com/2015-10-02-alphabet-do-the-right-thing.html" rel="noopener">changed its motto</a> from “don’t be evil” to “do the right thing.&#8221; Then, they promptly redefined the &#8220;right thing&#8221; as sabotaging people’s professional reputations with a minion GIF.</p>



<p>Google added a button, nice and close to the regular “Send” button in Gmail, that would send a farewell message to the recipient with an animated Minion dropping a mic then <strong>block all emails from that recipient permanently, without prompting the sender to confirm first</strong>. Better still, there was a bug that meant the recipient could receive that &#8220;GIF of death&#8221; and the block, even if the sender managed to press the correct “Send” button in the confusing new UI.</p>



<p>The <a href="https://www.9news.com.au/technology/google-april-fools-day-prank-backfires-man-loses-job/4a9699dc-06ba-426a-a84e-aad1f0eae313" rel="noopener">“hilarity” that ensued</a> included:</p>



<ul class="wp-block-list">
<li>A funeral home accidentally sent a mic drop and block to a grieving family.</li>



<li><a href="https://www.9news.com.au/technology/google-april-fools-day-prank-backfires-man-loses-job/4a9699dc-06ba-426a-a84e-aad1f0eae313" rel="noopener"></a>A man posted on the Gmail help forum, “Thanks to Mic Drop, I just lost my job.”</li>
</ul>



<p>Google <a href="https://blog.google/products-and-platforms/products/gmail/introducing-gmail-mic-drop/" rel="noopener">disabled the feature</a> before the end of April Fools’ Day and issued an apology saying, “It looks like we pranked ourselves this year.” I am not sure how the joke was on Google, so much as the people whose livelihoods and relationships were destroyed.</p>



<p>Remember when I said in the intro that April Fools’ is a distraction from how the joke is on us for believing that the web is what it seems? This Google prank was a reminder that if you believe an <a href="https://about.google/company-info/how-our-business-works/" rel="noopener">advertising company masquerading as a search company</a> has the judgment and ethics to prioritize your interests, when they <a href="https://www.digitaltrends.com/computing/googles-new-policy-tracks-all-your-devices-with-no-opt-out/" rel="noopener">hoard your personal data</a> and <a href="https://www.wheresyoured.at/the-men-who-killed-google/" rel="noopener">don’t actually care if you can find anything</a>, the real mic drop moment is when you realize that your career and relationships are a data point in Google&#8217;s next <a href="https://www.reddit.com/r/YoutubeMusic/comments/15b4adh/come_on_google_stop_being_so_ridiculous_with_the/" rel="noopener">A/B test</a>.</p>


<h3 class="wp-block-heading" id="prank-ui-ux-research-articles">Prank UI/UX research articles</h3>


<p>The funniest part of <a href="https://www.nngroup.com/topic/ux-humor/" rel="noopener">these April Fools&#8217; UI/UX advice articles</a> is that they&#8217;re published by a serious, high-profile consultancy and research group, so the authors work hard to make it obvious these are April Fools&#8217; hoaxes. In each article, “APRIL FOOLS” is in the title in ALL CAPS. And in the first paragraph of the newer hoax articles: &#8220;<em>This article was published as an April Fool&#8217;s hoax and does not contain real recommendations.&#8221;</em> I like to imagine the marketing department thought this was a great idea, and then the authors of the articles tried their best not to make fools of themselves. I noticed the group stopped posting hoax content after 2022.</p>



<p class="is-style-explanation"><strong>Sidenote:</strong> Educational resources people rely on as a source may not be the best place for prank posts. It reminds me of this peer-reviewed <a href="https://radiopaedia.org/" rel="noopener">radiology website</a> that on April Fools &#8216; Day 2015 posted a hoax X-ray image under the title <em>“Ectopia cordis interna &#8211; <a href="https://en.wikipedia.org/wiki/Tin_Man_(Oz)" rel="noopener">Tin(Man</a>) syndrome.”</em> Over the years, medical professionals circulated the image unaware it was a hoax, and then, in 2025, <a href="https://retractionwatch.com/2025/09/02/tin-man-syndrome-five-other-case-studies-retracted-following-retraction-watch-coverage/" rel="noopener">six medical journal case studies involving the made-up condition had to be retracted</a>.</p>



<p>Actually, the hoax UI/UX articles are educational, in a <a href="https://ui-patterns.com/blog/User-Interface-AntiPatterns" rel="noopener">UI antipatterns</a> kind of way, such as “<a href="https://www.nngroup.com/articles/users-love-change/" rel="noopener">Users Love Change: Combatting a UX Myth</a>,” which advocates redesigning the UI as often as possible for the heck of it — except I can’t help but feel <a href="https://community.atlassian.com/forums/Jira-questions/Why-are-there-so-many-UI-changes-happening-continuously/qaq-p/939786" rel="noopener">JIRA took that advice literally</a>. The “<a href="https://www.nngroup.com/articles/dog-ux/" rel="noopener">Canine UX</a>” article teaches ideas of user personas and design in a fun way. And “<a href="https://www.nngroup.com/articles/ux-public-bathrooms/" rel="noopener">The User Experience of Public Bathrooms</a>” reads as if <a href="https://www.reddit.com/r/seinfeld/comments/v7xfiw/georges_obsession_with_bathrooms/" rel="noopener">George Costanza</a> from <em>Seinfeld</em> turned his toilet obsession into a lesson in usability.</p>


<h3 class="wp-block-heading" id="digitalocean-buys-codepen-io">DigitalOcean buys codepen.io</h3>


<p>Regular readers of CSS-Tricks know that the founder, Chris Coyier, <a href="https://css-tricks.com/css-tricks-is-joining-digitalocean/">really did decide in 2022 to sell the website to our current stewards, DigitalOcean</a>, so that he could focus on his other projects, such as <a href="https://codepen.io/" rel="noopener">CodePen</a>. Therefore, the <a href="https://youtu.be/dQw4w9WgXcQ?si=Jx-MFGEMeBp6nyan" rel="noopener">announcement</a> on CodePen that DigitalOcean was also buying that website seemed maddeningly plausible. The level of detail in the hoax announcement increased verisimilitude. For instance, the claim that users could use custom domain names on CodePen for free, as long as the domain was DigitalOcean-hosted. In fact, the only sign it was a prank is that nobody anywhere announced anything like this, unless you count me posting it today on a DigitalOcean-owned website.</p>



<p><em>Happy April Fools’ Day, everyone!</em></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/front-end-april-fools-top-10/">Front-End Fools: Top 10 April Fools’ UI Pranks of All Time</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/front-end-april-fools-top-10/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392794</post-id>	</item>
		<item>
		<title>Sniffing Out the CSS Olfactive API</title>
		<link>https://css-tricks.com/css-olfactive-api/</link>
					<comments>https://css-tricks.com/css-olfactive-api/#respond</comments>
		
		<dc:creator><![CDATA[John Rhea]]></dc:creator>
		<pubDate>Wed, 01 Apr 2026 13:54:47 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[api]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393070</guid>

					<description><![CDATA[<p>A deep sniff of the new CSS Olfactive API, a set of proposed features for immersive user experiences using smell.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/css-olfactive-api/">Sniffing Out the CSS Olfactive API</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>A lot has happened in CSS in the last few years, but there’s nothing we needed less than the upcoming Olfactive API. Now, I know what you’re going to say, expanding the web in a more immersive way is a good thing, and in general I’d agree, but there’s no generalized hardware support for this yet and, in my opinion, it’s too much, too early.</p>



<span id="more-393070"></span>



<p>First let’s look at the hardware. Disney World and other theme parks have done some niche so-called 4D movies (which is nonsense since there isn’t a fourth dimensional aspect, and if you consider time the fourth dimension then every movie is fourth dimensional). And a few startups have tried to bring <a href="https://en.wikipedia.org/wiki/Olfactory_system" rel="noopener">olfactory senses</a> into the modern day, but as of this writing, the hardware isn’t consumer-ready yet. That said, it’s in active development and one startup assured me the technology would be available within the year. (And startups never, ever lie about when their products will launch, right?)</p>



<p>Even if it does come out within the year, would we even want this? I mean <a href="https://en.wikipedia.org/wiki/Smell-O-Vision" rel="noopener">Smell-O-Vision</a> totally caught on, right? It’s definitely not considered one of the worst inventions of all time… But, alas, no one cares about the ravings of a mad man, at least, not <em>this</em> mad man, so the API rolls on.</p>



<p>Alright, I’m going to step off my soap box now and try to focus on the technology and how it works.</p>


<h3 class="wp-block-heading" id="smell-tech">Smell Tech</h3>


<p>One of the fights currently going on in the CSS Working Group is whether we should limit smells to those considered pleasing by the perfume industry or whether to open websites to a much wider variety. For instance, while everyone’s olfactory sense is different, the perfume industry has centered on a selection of fragrances that will be pleasing to a wide swath of people.</p>



<p>That said, there are a large number of pleasing fragrances that would not be included in this, such as food-based smells: fresh baked bread etc. Fragrances that the Big Food Lobby is itching to include in their advertisements. As of now the CSS Olfactive API only includes the twelve general categories used by the perfume industry, but just like there are ways to expand the color gamut, the system is built to allow for expanded smells in the future should the number of available fragrance fragments increase.</p>


<h3 class="wp-block-heading" id="smelly-families">Smelly Families</h3>


<p>You don’t have to look far online to find something called the <a href="https://en.wikipedia.org/wiki/Fragrance_wheel" rel="noopener">Scent Wheel</a> (alternately called the Fragrance Wheel or the Wheel of Smell-Tune, but that last one is only used by me). There are four larger families of smell:</p>



<ul class="wp-block-list">
<li>Floral</li>



<li>Amber (previously called Oriental)</li>



<li>Woody</li>



<li>Fresh</li>
</ul>



<p>These four are each subdivided into additional categories though there are overlaps between where one of the larger families begins/ends and the sub families begin/end</p>



<ul class="wp-block-list">
<li>Floral:
<ul class="wp-block-list">
<li>Floral (<code>fl</code>)</li>



<li>Soft Floral (<code>sf</code>)</li>



<li>Floral Amber (<code>fa</code>)</li>
</ul>
</li>



<li>Amber:
<ul class="wp-block-list">
<li>Soft Amber (<code>sa</code>)</li>



<li>Amber (<code>am</code>)</li>



<li>Woody Amber (<code>wa</code>)</li>
</ul>
</li>



<li>Woody:
<ul class="wp-block-list">
<li>Woods (<code>wo</code>)</li>



<li>Mossy Woods (<code>mw</code>)</li>



<li>Dry Woods (<code>dw</code>)</li>
</ul>
</li>



<li>Fresh (<code>fr</code>)
<ul class="wp-block-list">
<li>Aromatic (<code>ar</code>)</li>



<li>Citrus (<code>ct</code>)</li>



<li>Water (<code>ho</code>)</li>



<li>Green (<code>gr</code>)</li>



<li>Fruity (<code>fu</code>)</li>
</ul>
</li>
</ul>



<p>It’s from these fifteen fragrance categories that a scent can be made by mixing different amounts using the two letter identifiers. (We’ll talk about this when we discuss the <code>scent()</code> function later on. Note that “Fresh” is the only large family with its own identifier (<code>fr</code>) as the other larger families are duplicated in the sub-families)</p>


<h3 class="wp-block-heading" id="implementation">Implementation</h3>


<p>First of all, its implemented (wisely) in HTML in much the same way video and audio are with the addition of the <code>&lt;scent&gt;</code> element, and <code>&lt;source&gt;</code> was again used to give the browser different options for wafting the scent toward your sniffer. Three competing file formats are being developed <code>.smll</code>, <code>.arma</code>, and, I kid you not, <code>.smly</code>. One by Google, one by Mozilla, and one, again, not kidding, by Frank’s Fine Fragrances who intends to jump on this “fourth dimension of the web.”</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;scent controls autosmell="none">
  &lt;source src=“mossywoods.smll” type=“scent/smll”>
  &lt;source src=“mossywoods.arma” type=“scent/arma”>
  &lt;source src=“mossywoods.smly” type=“scent/smly”>
  &lt;a href=“mossywoods.smll”>Smell our Mossy Woods scent&lt;/a>
&lt;/scent></code></pre>



<p>For accessibility, be sure that you set the <code>autosmell</code> attribute to <code>none</code>. In theory, this isn’t required, but some of the current hardware has a bug that turns on the wafter even if a smell hasn’t been activated.</p>



<p>However, similar to how you can use an image or video in the background of an element, you can also attach a scent profile to an element using the new <code>scent-profile</code> property.</p>



<p><code>scent-profile</code> can take one of three things.</p>



<p>The keyword <code>none</code> (default):</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">scent-profile: none;</code></pre>



<p>A <code>url()</code> function and the path to a file e.g.:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">scent-profile: url(mossywoods.smll);</code></pre>



<p>Or a set of aromatic identifiers using the <code>scent()</code> function:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">scent-profile: scent(wo, ho, fu);</code></pre>



<p>This produces a scent that has notes of woody, water, and fruity which was described to me as “an orchard in the rain” but to me smelled more like “a wooden bowl of watered-down applesauce.” Please take that with a grain of salt, though, as I have been told I have “the nasal palette of a dead fish.”</p>



<p>You can add up to five scent sub-families at once. This is an arbitrary limit, but more than that would likely muddle the scent. Equal amounts of each will be used, but you can use the new <code>whf</code> unit to adjust how much of each is used. <code>100whf</code> is the most potent an aroma can be. Unlike most units, your implementation, must add up to <code>100whf</code> or less. If your numbers add up to more than 100, the browser will take the first <code>100whf</code>s it gets and ignore everything afterward.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">scent-profile: scent(wo 20whf, ho 13whf, fu 67whf);</code></pre>



<p>&#8230;or you could reduce the overall scent by choosing <code>whf</code>s less than 100:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">scent-profile: scent(wo 5whf, ho 2whf, fu 14whf);</code></pre>



<p>In the future, should other fragrances be allowed, they would simply need to add some new fragrance fragments from which to construct the aromatic air.</p>


<h3 class="wp-block-heading" id="sniffing-out-limitations">Sniffing Out Limitations</h3>


<p>One large concern for the working group was that some developer would go crazy placing <code>scent-profile</code>s on every single element, both overwhelming the user and muddling each scent used.</p>



<p>As such it was decided that the browser will only allow one <code>scent-profile</code> to be set per the parent element’s sub tree. This basically means that once you set a <code>scent-profile</code> on a particular element you cannot add a scent profile to any of its descendants, nor can you add a scent profile to any of its siblings. In this way, a scent profile set on a hungry selector (e.g. <code>*</code> or <code>div</code>) will create a fraction of the scent profiles than what might otherwise be created. While there are clearly easy ways to maliciously get around this limitation, it was thought that this should at least prevent a developer from accidentally overwhelming the user.</p>


<h3 class="wp-block-heading" id="aromatic-accessibility">Aromatic Accessibility</h3>


<p>Since aromas can be overpowering they’ve also added a media-query:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.reeks {
  scent-profile: scent(fl, fa, fu);
}

@media (prefers-reduced-pungency: reduce) {
  .reeks {
    scent-profile: scent(fl 10whf, fa 10whf, fu 10whf);
  }
}

@media (prefers-reduced-pungency: remove) {
  .reeks {
    scent-profile: none;
  }
}</code></pre>


<h3 class="wp-block-heading" id="browser-support">Browser Support</h3>


<p>Surprisingly, despite Chrome Canary literally being named after a bird who would smell gas in the mine, Chrome has not yet begun experimenting with it. The only browser you can test things out on, as of this writing, is the KaiOS Browser.</p>


<h3 class="wp-block-heading" id="conclusion">Conclusion</h3>


<p>There you have it. I still don’t think we need this, but with the continuing march of technology it’s probably not something we can stop. So let&#8217;s make an agreement between you reading this and me here writing this that you’ll always use your new-found olfactory powers for good&#8230; <em>and</em> that you won’t ever say this article stinks.</p>



<p>Learn more about the <a href="https://css-tricks.com/front-end-april-fools-top-10/">CSS Olfactive API</a>.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/css-olfactive-api/">Sniffing Out the CSS Olfactive API</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/css-olfactive-api/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393070</post-id>	</item>
		<item>
		<title>What’s !important #8: Light/Dark Favicons, @mixin, object-view-box, and More</title>
		<link>https://css-tricks.com/whats-important-8/</link>
					<comments>https://css-tricks.com/whats-important-8/#comments</comments>
		
		<dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
		<pubDate>Tue, 31 Mar 2026 14:14:07 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[news]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393306</guid>

					<description><![CDATA[<p>Short n’ sweet but ever so neat, this issue covers light/dark favicons, @mixin, anchor-interpolated morphing, object-view-box, new web features, and more.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-8/">What’s !important #8: Light/Dark Favicons, @mixin, object-view-box, and More</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Short n’ sweet but ever so neat, this issue covers light/dark favicons, <code>@mixin</code>, anchor-interpolated morphing, <code>object-view-box</code>, new web features, and more.</p>



<span id="more-393306"></span>


<h3 class="wp-block-heading" id="svg-favicons-that-respect-the-color-scheme">SVG favicons that respect the color scheme</h3>


<p>I’m a sucker for colorful logos with about 50% lightness that look awesome on light <em>and</em> dark backgrounds, but not all logos can be like that. Paweł Grzybek showed us <a href="https://pawelgrzybek.com/svg-favicons-that-respect-theme-preference/" rel="noopener">how to implement SVG favicons that respect the color scheme</a>, enabling us to display favicons conditionally, but the behavior isn’t consistent across web browsers. It’s an interesting read and there appears to be a campaign to get it working correctly.</p>



<p>And once that happens, here’s a <a href="https://bsky.app/profile/freefrontend.bsky.social/post/3mhxjk6qnqc2j" rel="noopener">skeuomorphic egg-themed CSS toggle</a> that I found last week. Perfect timing, honestly.</p>



<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:7wef2htaxqe5rkxf62htfkun/app.bsky.feed.post/3mhxjk6qnqc2j" data-bluesky-cid="bafyreigzryvjpdpibnbjwxppugl676omwxr62llsdruwfnoh4lxilzmry4" data-bluesky-embed-color-mode="system"><p lang="en">Skeuomorphic Egg Toggle Switch [HTML + CSS + JS]

Organic mechanics. Complex box-shadow layering and border-radius manipulation. Tactile feedback through depth. Source code: freefrontend.com/code/skeuomo&#8230;<br><br><a href="https://bsky.app/profile/did:plc:7wef2htaxqe5rkxf62htfkun/post/3mhxjk6qnqc2j?ref_src=embed" rel="noopener">[image or embed]</a></p>&mdash; FreeFrontend (<a href="https://bsky.app/profile/did:plc:7wef2htaxqe5rkxf62htfkun?ref_src=embed" rel="noopener">@freefrontend.bsky.social</a>) <a href="https://bsky.app/profile/did:plc:7wef2htaxqe5rkxf62htfkun/post/3mhxjk6qnqc2j?ref_src=embed" rel="noopener">Mar 26, 2026 at 11:42</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>


<h3 class="wp-block-heading" id="help-the-css-wg-shape-mixin">Help the CSS WG shape <code>@mixin</code></h3>


<p>It seems that <code>@mixin</code> is taking a step forward. <a href="https://github.com/LeaVerou/blog/discussions/137" rel="noopener">Lea Verou showed us a code snippet and asked what we think of it</a>.</p>



<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:eagnfcoqnbtzpkglrtej6ayg/app.bsky.feed.post/3mhyr2rw2ls2r" data-bluesky-cid="bafyreih5m66lqr7bgq64utaupzjkdu3hrqtwmhn2loqlfrbkeiv3zqq3ve" data-bluesky-embed-color-mode="system"><p lang="en">&#x1f6a8; Want mixins in CSS? Help the CSS WG by telling us what feels natural to you! Look at the code in the screenshot. What resulting widths would *you* find least surprising for each of div, div &gt; h2, div + p? Polls: ┣ Github: github.com/LeaVerou/blo&#8230; ┗ Mastodon: front-end.social/@leaverou/11&#8230;<br><br><a href="https://bsky.app/profile/did:plc:eagnfcoqnbtzpkglrtej6ayg/post/3mhyr2rw2ls2r?ref_src=embed" rel="noopener">[image or embed]</a></p>&mdash; Lea Verou, PhD (<a href="https://bsky.app/profile/did:plc:eagnfcoqnbtzpkglrtej6ayg?ref_src=embed" rel="noopener">@lea.verou.me</a>) <a href="https://bsky.app/profile/did:plc:eagnfcoqnbtzpkglrtej6ayg/post/3mhyr2rw2ls2r?ref_src=embed" rel="noopener">Mar 26, 2026 at 23:29</a></blockquote>


<h3 class="wp-block-heading" id="anchorinterpolated-morphing-tutorial">Anchor-interpolated morphing tutorial</h3>


<p>Chris Coyier showed us <a href="https://frontendmasters.com/blog/image-gallery-with-popovers-and-aim-anchor-interpolated-morph/" rel="noopener">how to build an image gallery using popovers and something called AIM</a> (Anchor-Interpolated Morphing). I’m only hearing about this now but <a href="https://argyle.ink/anchor-interpolated-morphing/" rel="noopener">Adam Argyle talked about AIM</a> back in January. It’s not a new CSS feature but rather the idea of animating something from its starting position to an anchored position. Don’t miss this one.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_019cb5ec-807e-727a-b18a-5eb41b0fc901" src="//codepen.io/editor/anon/embed/019cb5ec-807e-727a-b18a-5eb41b0fc901?height=450&amp;theme-id=1&amp;slug-hash=019cb5ec-807e-727a-b18a-5eb41b0fc901&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed 019cb5ec-807e-727a-b18a-5eb41b0fc901" title="CodePen Embed 019cb5ec-807e-727a-b18a-5eb41b0fc901" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>



<p>Also, do you happen to remember <a href="https://codepen.io/t_afif/pen/wBWWKxP" rel="noopener">Temani’s demo</a> that I shared a few weeks ago? Well, Frontend Masters have published <a href="https://frontendmasters.com/blog/two-circles-one-arrow-and-anchor-positioning/" rel="noopener">the tutorial for that</a> too!</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_wBWWKxP" src="//codepen.io/anon/embed/wBWWKxP?height=450&amp;theme-id=1&amp;slug-hash=wBWWKxP&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed wBWWKxP" title="CodePen Embed wBWWKxP" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>


<h3 class="wp-block-heading" id="remember-objectviewbox-me-neither">Remember <code>object-view-box</code>? Me neither</h3>


<p>CSS <code>object-view-box</code> allows an element to be zoomed, cropped, or framed in a way that resembles how SVG’s <code>viewBox</code> works, but since Chrome implemented it back in August 2022, there’s been no mention of it. To be honest, I don’t remember it at all, which is a shame because it sounds useful. In a Bluesky thread, Victor Ponamariov explains <a href="https://bsky.app/profile/vpon.me/post/3mhsxu4bcpd23" rel="noopener">how <code>object-view-box</code> works</a>. Hopefully, Safari and Firefox implement it soon.</p>



<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:k5j6zg5xqnaady5e77smdiaz/app.bsky.feed.post/3mhsxu4bcpd23" data-bluesky-cid="bafyreia5jttfh5ohjyqwsekwycahokao3qlvvmvc73eyvsudsmtitp3cqe" data-bluesky-embed-color-mode="system"><p lang="en">Wouldn&#x27;t it be great to have native image cropping in CSS? It actually exists: object-view-box.<br><br><a href="https://bsky.app/profile/did:plc:k5j6zg5xqnaady5e77smdiaz/post/3mhsxu4bcpd23?ref_src=embed" rel="noopener">[image or embed]</a></p>&mdash; Victor (<a href="https://bsky.app/profile/did:plc:k5j6zg5xqnaady5e77smdiaz?ref_src=embed" rel="noopener">@vpon.me</a>) <a href="https://bsky.app/profile/did:plc:k5j6zg5xqnaady5e77smdiaz/post/3mhsxu4bcpd23?ref_src=embed" rel="noopener">Mar 24, 2026 at 16:15</a></blockquote>


<h3 class="wp-block-heading" id="cornershape-for-everyday-ui-elements"><code>corner-shape</code> for everyday UI elements</h3>


<p>Much has been said about <a href="https://css-tricks.com/?s=corner-shape">CSS <code>corner-shape</code></a>, by us and the wider web dev community, despite only being supported by Chrome for now. It’s such a fun feature, offering so many ways to turn boxes into interesting shapes, but <a href="https://www.smashingmagazine.com/2026/03/beyond-border-radius-css-corner-shape-property-ui/" rel="noopener">Brecht De Ruyte’s <code>corner-shape</code> article</a> focuses more on how we might use <code>corner-shape</code> for everyday UI elements/components.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="576" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/1-1024x576.png?resize=1024%2C576&#038;ssl=1" alt="An interface design titled Buttons and Tags showcasing various UI component shapes using the corner-shape property. The display includes a row of solid buttons in different colors labeled Bevel, Superellipse, Squircle, Notch, and Scoop, followed by a set of outlined buttons and a series of decorative status tags like Shipped and Pending. Below these are directional tags with arrow shapes and a row of notification badges featuring icons for a bell, message, and alert with numerical counters." class="wp-image-393307" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/1.png?resize=1024%2C576&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/1.png?resize=300%2C169&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/1.png?resize=768%2C432&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/1.png?resize=1536%2C864&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/1.png?w=1920&amp;ssl=1 1920w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://www.smashingmagazine.com/2026/03/beyond-border-radius-css-corner-shape-property-ui/" rel="noopener">Smashing Magazine</a>.</figcaption></figure>


<h3 class="wp-block-heading" id="the-layout-maestro">The Layout Maestro</h3>


<p>Ahmad Shadeed’s course — <a href="https://thelayoutmaestro.com/" rel="noopener">The Layout Maestro</a> — teaches you how to plan and build CSS layouts using modern techniques. Plus, you can learn how to master building the bones of websites using an extended trial of the web development browser, <a href="https://polypane.app/" rel="noopener">Polypane</a>, which comes free with the course.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="419" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/2-1024x419.png?resize=1024%2C419&#038;ssl=1" alt="A bento grid layout featuring multiple rounded rectangular panels in a very light lavender hue. The central panel displays a logo consisting of a purple stylized window icon and the text The Layout Maestro in black and purple sans-serif font, accented by small purple sparkles. The surrounding empty panels vary in size and aspect ratio, creating a clean and modern asymmetrical composition against a white background." class="wp-image-393308" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/2-scaled.png?resize=1024%2C419&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/2-scaled.png?resize=300%2C123&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/2-scaled.png?resize=768%2C315&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/2-scaled.png?resize=1536%2C629&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/2-scaled.png?resize=2048%2C839&amp;ssl=1 2048w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://thelayoutmaestro.com/" rel="noopener">The Layout Maestro</a>.</figcaption></figure>


<h3 class="wp-block-heading" id="new-web-platform-features">New web platform features</h3>


<p>Firefox and Safari shipped new features (none baseline, sadly):</p>



<ul class="wp-block-list">
<li><a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/149" rel="noopener">Firefox 149</a>
<ul class="wp-block-list">
<li><a href="https://una.im/popover-hint/" rel="noopener"><code>popover=hint</code></a> (also supported by Chrome)</li>



<li>Name-only <a href="https://css-tricks.com/css-container-queries/">containers</a> (e.g., <code>@container name { }</code>)</li>
</ul>
</li>



<li><a href="https://developer.apple.com/documentation/safari-release-notes/safari-26_4-release-notes" rel="noopener">Safari 26.4</a>
<ul class="wp-block-list">
<li>Name-only containers (as above)</li>



<li><a href="https://css-tricks.com/masonry-layout-is-now-grid-lanes/"><code>display: grid-lanes</code></a> and <a href="https://www.w3.org/TR/css-grid-3/#placement-tolerance" rel="noopener"><code>flow-tolerance</code></a></li>
</ul>
</li>



<li><a href="https://developer.apple.com/documentation/safari-technology-preview-release-notes/stp-release-240" rel="noopener">Safari TP 240</a>
<ul class="wp-block-list">
<li><a href="https://github.com/WebKit/WebKit/commit/cdf824701b8c4d6c2047d7318deb2a9da0e0fbd2" rel="noopener"><code>revert-rule</code></a></li>
</ul>
</li>
</ul>



<p>Also, Bramus said that Chrome 148 will have <a href="https://www.bram.us/2026/03/15/at-rule/" rel="noopener">at-rule feature queries</a>, while Chrome 148 and Firefox 150 will allow <a href="https://www.bram.us/2026/03/19/more-easy-light-dark-mode-switching-light-dark-is-about-to-support-images/" rel="noopener"><code>background-image</code> to support <code>light-dark()</code></a>. In any case, there’s a new website called <a href="https://basewatch.dev/" rel="noopener">BaseWatch</a> that tracks baseline status for all of these CSS features.</p>



<p>Ciao!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-8/">What’s !important #8: Light/Dark Favicons, @mixin, object-view-box, and More</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/whats-important-8/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393306</post-id>	</item>
		<item>
		<title>Form Automation Tips for Happier User and Clients</title>
		<link>https://css-tricks.com/form-automation-tips-for-happier-user-and-clients/</link>
					<comments>https://css-tricks.com/form-automation-tips-for-happier-user-and-clients/#respond</comments>
		
		<dc:creator><![CDATA[Iqra Naaem]]></dc:creator>
		<pubDate>Mon, 30 Mar 2026 14:12:14 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[forms]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=391993</guid>

					<description><![CDATA[<p>That gap between "the form works" and "the business works" is something we don't really tend to discuss much as front-enders. We focus a great deal on user experience, validation methods, and accessibility, yet we overlook what the data does once it leaves our control</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/form-automation-tips-for-happier-user-and-clients/">Form Automation Tips for Happier User and Clients</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></description>
										<content:encoded><![CDATA[
<p>I deployed a contact form that last month that, in my opinion, was well executed. It had all the right semantics, seamless validation, and great keyboard support. You know, all of the features you&#8217;d want in your portfolio.</p>



<p>But&#8230; a mere two weeks after deployment, my client called.&nbsp;<q>We lost a referral because it was sitting in your inbox over the weekend.</q></p>



<p>The form worked perfectly. The workflow didn&#8217;t.</p>



<span id="more-391993"></span>


<h3 class="wp-block-heading" id="the-problem-nobody-talks-about">The Problem Nobody Talks About</h3>


<p>That gap between &#8220;the form works&#8221; and &#8220;the business works&#8221; is something we don&#8217;t really tend to discuss much as front-enders. We focus a great deal on user experience, validation methods, and accessibility, yet we overlook what the data does once it leaves our control. That is exactly where things start to fall apart in the real world.</p>



<p>Here&#8217;s what I learned from that experience that would have made for a much better form component.</p>


<h3 class="wp-block-heading" id="why-send-email-on-submit-fails">Why &#8220;Send Email on Submit&#8221; Fails</h3>


<p>The pattern we all use looks something like this:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">fetch('/api/contact', {
  method: 'POST',
  body: JSON.stringify(formData)
})

// Email gets sent and we call it done</code></pre>



<p>I have seen duplicate submissions cause confusion, specifically when working with CRM systems, like Salesforce. For example, I have encountered inconsistent formatting that hinders automated imports. I have also experienced weekend queries that were overlooked until Monday morning. I have debugged queries where copying and pasting lost decimal places for quotes. There have also been &#8220;required&#8221; fields for which &#8220;required&#8221; was simply a misleading label.</p>



<p>I had an epiphany: the reality was that having a working form was just the starting line, not the end. The fact is that the email is not a notification; rather, it&#8217;s a handoff. If it&#8217;s treated merely as a notification, it puts us into a bottleneck with our own code. In fact, Litmus, as shown in their&nbsp;<a href="https://www.litmus.com/state-of-email-reports" rel="noopener">2025 State of Email Marketing Report</a>&nbsp;(sign-up required), found inbox-based workflows result in lagging follow-ups, particularly with sales teams that rely on lead generation.</p>



<figure class="wp-block-image size-full is-resized ticss-1f641bd8"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1142" height="1476" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-2.png?resize=1142%2C1476" alt="Detailing a broken workflow for a submitted form. User submits form, email reaches inbox, manual spreadsheet entries, formatting errors, and delays." class="wp-image-392005" style="width:500px" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-2.png?w=1142&amp;ssl=1 1142w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-2.png?resize=232%2C300&amp;ssl=1 232w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-2.png?resize=792%2C1024&amp;ssl=1 792w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-2.png?resize=768%2C993&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>


<h3 class="wp-block-heading" id="designing-forms-for-automation">Designing Forms for Automation</h3>


<p><strong>The bottom line is that front-end decisions directly influence back-end automation.</strong>&nbsp;In recent research from HubSpot, data at the front-end stage (i.e., the user interaction) makes or breaks what is coming next.</p>



<p>These are the practical design decisions that changed how I build forms:</p>


<h4 class="wp-block-heading" id="required-vs-optional-fields">Required vs. Optional Fields</h4>


<p>Ask yourself:&nbsp;<q>What does the business rely on the data for?</q>&nbsp;Are phone calls the primary method for following up with a new lead? Then let&#8217;s make that field required. Is the lead&#8217;s professional title a crucial context for following up? If not, make it optional. This takes some interpersonal collaboration before we even begin marking up code.</p>



<p>For example, I made an incorrect assumption that a phone number field was an optional piece of information, but the CRM required it. The result? My submissions were invalidated and the CRM flat-out rejected them.</p>



<p>Now I know to drive my coding decisions from a business process perspective, not just my assumptions about what the user experience ought to be.</p>


<h4 class="wp-block-heading" id="normalize-data-early">Normalize Data Early</h4>


<p>Does the data need to be formatted in a specific way once it&#8217;s submitted? It&#8217;s a good idea to ensure that some data, like phone numbers, are formatted consistently so that the person on the receiving has an easier time scanning the information. Same goes when it comes to trimming whitespace and title casing.</p>



<p>Why? Downstream tools are dumb. They are utterly unable to make the correlation that &#8220;John Wick&#8221; and &#8220;john wick&#8221; are related submissions. I once watched a client manually clean up 200 CRM entries because inconsistent casing had created duplicate records. That&#8217;s the kind of pain that five minutes of front-end code prevents.</p>


<h4 class="wp-block-heading" id="prevent-duplicate-entries-from-the-front-end">Prevent Duplicate Entries From the Front End</h4>


<p>Something as simple as disabling the Submit button on click can save the headache of sifting through duplicative submissions. Show clear &#8220;submission states&#8221; like a loading indicator that an action is being processed. Store a flag that a submission is in progress.</p>



<p>Why? Duplicate CRM entries cost real money to clean up. Impatient users on slow networks will absolutely click that button multiple times if you let them.</p>


<h4 class="wp-block-heading" id="success-and-error-states-that-matter">Success and Error States That Matter</h4>


<p>What should the user know once the form is submitted? I think it&#8217;s super common to do some sort of default &#8220;Thanks!&#8221; on a successful submission, but how much context does that really provide? Where did the submission go? When will the team follow up? Are there resources to check out in the meantime? That&#8217;s all valuable context that not only sets expectations for the lead, but gives the team a leg up when following up.</p>



<p>Error messages should help the business, too. Like, if we&#8217;re dealing with a duplicate submission, it&#8217;s way more helpful to say something like,&nbsp;<em>&#8220;This email is already in our system&#8221;</em>&nbsp;than some generic&nbsp;<em>&#8220;Something went wrong&#8221;</em>&nbsp;message.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1202" height="818" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-3.png?resize=1202%2C818" alt="Comparing two types of submitted raw data. Formatting problems displayed on the left and properly formatted data on the right." class="wp-image-392006" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-3.png?w=1202&amp;ssl=1 1202w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-3.png?resize=300%2C204&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-3.png?resize=1024%2C697&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-3.png?resize=768%2C523&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>


<h3 class="wp-block-heading" id="a-better-workflow">A Better Workflow</h3>


<p>So, how exactly would I approach form automation next time? Here are the crucial things I missed last time that I&#8217;ll be sure to hit in the future.</p>


<h4 class="wp-block-heading" id="better-validation-before-submission">Better Validation Before Submission</h4>


<p>Instead of simply checking if fields exist:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const isValid = email &amp;&amp; name &amp;&amp; message;</code></pre>



<p>Check if they&#8217;re actually usable:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">function validateForAutomation(data) {
  return {
    email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email),
    name: data.name.trim().length >= 2,
    phone: !data.phone || /^\d{10,}$/.test(data.phone.replace(/\D/g, ''))
  };
}</code></pre>



<p><strong>Why this matters:</strong>&nbsp;CRMs will reject malformed emails. Your error handling should catch this before the user clicks submit, not after they&#8217;ve waited two seconds for a server response.</p>



<p>At the same time, it&#8217;s worth noting that the phone validation here covers common cases, but is not bulletproof for things like international formats. For production use, consider a library like&nbsp;<a href="https://github.com/google/libphonenumber" rel="noopener">libphonenumber</a>&nbsp;for comprehensive validation.</p>


<h4 class="wp-block-heading" id="consistent-formatting">Consistent Formatting</h4>


<p>Format things before it sends rather than assuming it will be handled on the back end:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">function normalizeFormData(data) {
  return {
    name: data.name.trim()
      .split(' ')
      .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
      .join(' '),
    email: data.email.trim().toLowerCase(),
    phone: data.phone.replace(/\D/g, ''), // Strip to digits
    message: data.message.trim()
  };
}</code></pre>



<p><strong>Why I do this:</strong>&nbsp;Again, I&#8217;ve seen a client manually fix over 200 CRM entries because &#8220;JOHN SMITH&#8221; and &#8220;john smith&#8221; created duplicate records. Fixing this takes five minutes to write and saves hours downstream.</p>



<p>There&#8217;s a caveat to this specific approach. This name-splitting logic will trip up on single names, hyphenated surnames, and edge cases like &#8220;McDonald&#8221; or names with multiple spaces. If you need rock-solid name handling, consider asking for separate first name and last name fields instead.</p>


<h4 class="wp-block-heading" id="prevent-double-submissions">Prevent Double Submissions</h4>


<p>We can do that by disabling the Submit button on click:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">let submitting = false;
  async function handleSubmit(e) {
    e.preventDefault();
    if (submitting) return;
    submitting = true;

const button = e.target.querySelector('button[type="submit"]');
button.disabled = true;
button.textContent = 'Sending...';

try {
  await sendFormData();
    // Success handling
  } catch (error) {
    submitting = false; // Allow retry on error
    button.disabled = false;
    button.textContent = 'Send Message';
  }
}</code></pre>



<p><strong>Why this pattern works:</strong>&nbsp;Impatient users double-click. Slow networks make them click again. Without this guard, you&#8217;re creating duplicate leads that cost real money to clean up.</p>


<h4 class="wp-block-heading" id="structuring-data-for-automation">Structuring Data for Automation</h4>


<p>Instead of this:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const formData = new FormData(form);</code></pre>



<p>Be sure to structure the data:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const structuredData = {
  contact: {
    firstName: formData.get('name').split(' ')[0],
    lastName: formData.get('name').split(' ').slice(1).join(' '),
    email: formData.get('email'),
    phone: formData.get('phone')
  },
  inquiry: {
    message: formData.get('message'),
    source: 'website_contact_form',
    timestamp: new Date().toISOString(),
    urgency: formData.get('urgent') ? 'high' : 'normal'
  }
};</code></pre>



<p><strong>Why structured data matters:</strong>&nbsp;Tools like Zapier, Make, and even custom webhooks expect it. When you send a flat object, someone has to write logic to parse it. When you send it pre-structured, automation &#8220;just works.&#8221; This mirrors Zapier&#8217;s own recommendations for building more reliable, maintainable workflows rather than fragile single-step &#8220;simple zaps.&#8221;</p>



<p><a href="https://youtu.be/yhYOFVXr_lY?si=i-tkWdoumcjXeIHm" rel="noopener">Watch How Zapier Works</a>&nbsp;(YouTube) to see what happens after your form submits.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1528" height="1014" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-4.png?resize=1528%2C1014" alt="Comparing flat JSON data on the left with properly structured JSON data." class="wp-image-392007" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-4.png?w=1528&amp;ssl=1 1528w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-4.png?resize=300%2C199&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-4.png?resize=1024%2C680&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-4.png?resize=768%2C510&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>


<h3 class="wp-block-heading" id="care-about-what-happens-after-submit">Care About What Happens After Submit</h3>


<p>An ideal flow would be:</p>



<ol class="wp-block-list">
<li>User submits form&nbsp;</li>



<li>Data arrives at your endpoint (or form service)&nbsp;</li>



<li>Automatically creates CRM contact&nbsp;</li>



<li>A Slack/Discord notification is sent to the sales team&nbsp;</li>



<li>A follow-up sequence is triggered&nbsp;</li>



<li>Data is logged in a spreadsheet for reporting</li>
</ol>



<p>Your choices for the front end make this possible:</p>



<ul class="wp-block-list">
<li>Consistency in formatting = Successful imports in CRM&nbsp;</li>



<li>Structured data = Can be automatically populated using automation tools&nbsp;</li>



<li>De-duplication = No messy cleanup tasks required&nbsp;</li>



<li>Validation = Less &#8220;invalid entry&#8221; errors</li>
</ul>



<p><strong>Actual experience from my own work:</strong>&nbsp;After re-structuring a lead quote form, my client&#8217;s automated quote success rate increased from 60% to 98%. The change? Instead of sending&nbsp;<code>{ "amount": "$1,500.00"}</code>, I now send&nbsp;<code>{ "amount": 1500}</code>. Their Zapier integration couldn&#8217;t parse the currency symbol.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1530" height="1024" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-5.png?resize=1530%2C1024" alt="Showing the change in rate of success after implementation automation, from 60% to 98% with an example of a parsed error and an accepted value below based on formatting money in dollars versus a raw number." class="wp-image-392009" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-5.png?w=1530&amp;ssl=1 1530w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-5.png?resize=300%2C201&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-5.png?resize=1024%2C685&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/02/auto-forms-5.png?resize=768%2C514&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>


<h3 class="wp-block-heading" id="my-set-of-best-practices-for-form-submissions">My Set of Best Practices for Form Submissions</h3>


<p>These lessons have taught me the following about form design:</p>



<ol class="wp-block-list">
<li><strong>Ask about the workflow early.</strong>&nbsp;&#8220;What happens after someone fills this out?&#8221; needs to be the very first question to ask. This surfaces exactly what really needs to go where, what data needs to come in with a specific format, and integrations to use.&nbsp;</li>



<li><strong>Test with Real Data.</strong>&nbsp;I am also using my own input to fill out forms with extraneous spaces and strange character strings, such as mobile phone numbers and bad uppercase and lowercase letter strings. You might be surprised by the number of edge cases that can come about if you try inputting &#8220;JOHN SMITH &#8221; instead of &#8220;John Smith.&#8221;&nbsp;</li>



<li><strong>Add timestamp and source.</strong>&nbsp;It makes sense to design it into the system, even though it doesn&#8217;t necessarily seem to be necessary. Six months into the future, it&#8217;s going to be helpful to know when it was received.&nbsp;</li>



<li><strong>Make it redundant.</strong>&nbsp;Trigger an email&nbsp;<em>and</em>&nbsp;a webhook. When sending via email, it often goes silent, and you won&#8217;t realize it until someone asks,&nbsp;<em>&#8220;Did you get that message we sent you?&#8221;</em></li>



<li><strong>Over-communicate success.</strong>&nbsp;Setting the lead&#8217;s expectations is crucial to a more delightful experience.&nbsp;<em>&#8220;Your message has been sent. Sarah from sales will answer within 24 hours.&#8221;</em>&nbsp;is much better than a plain old&nbsp;<em>&#8220;Success!&#8221;</em></li>
</ol>


<h3 class="wp-block-heading" id="the-real-finish-line">The Real Finish Line</h3>


<p>This is what I now advise other developers: &#8220;Your job doesn&#8217;t stop when a form posts without errors. Your job doesn&#8217;t stop until you have confidence that your business can act upon this form submission.&#8221;</p>



<p>That means:</p>



<ul class="wp-block-list">
<li>No &#8220;copy paste&#8221; allowed&nbsp;</li>



<li>No &#8220;I&#8217;ll check my email later&#8221;&nbsp;</li>



<li>No duplicate entries to clean up&nbsp;</li>



<li>No formatting fixes needed</li>
</ul>



<p>The code itself is not all that difficult. The switch in attitude comes from understanding that a form is actually part of a larger system and not a standalone object. Once you think about forms this way, you think differently about them in terms of planning, validation, and data.</p>



<p>The next time you&#8217;re putting together a form, ask yourself:&nbsp;<q>What happens when this data goes out of my hands?</q>&nbsp;Answering that question makes you a better front-end developer.</p>



<p>The following CodePen demo is a side-by-side comparison of a standard form versus an automation-ready form. Both look identical to users, but the console output shows the dramatic difference in data quality.</p>



<div class="wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper"><iframe id="cp_embed_PwzpPWL" src="//codepen.io/anon/embed/preview/PwzpPWL?height=1000&amp;theme-id=1&amp;slug-hash=PwzpPWL&amp;default-tab=result" height="1000" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed PwzpPWL" title="CodePen Embed PwzpPWL" class="cp_embed_iframe" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe></div>


<h3 class="wp-block-heading" id="references-further-reading">References &amp; Further Reading</h3>


<ul class="wp-block-list">
<li><a href="https://www.litmus.com/resources/state-of-email/" rel="noopener">&#8220;2025 State of Email Marketing Report&#8221;</a>&nbsp;(Litmus)&nbsp;</li>



<li>&#8220;<a href="https://blog.hubspot.com/marketing/form-design-best-practices" rel="noopener">Form Design Best Practices for Lead Capture</a>&#8221; (HubSpot)&nbsp;</li>



<li><a href="https://www.youtube.com/watch?v=h5qqmE83Tes" rel="noopener">&#8220;How to set custom error messages for your HTML forms&#8221;</a>&nbsp;(Kevin Powell, YouTube)</li>
</ul>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/form-automation-tips-for-happier-user-and-clients/">Form Automation Tips for Happier User and Clients</a> originally handwritten and published with love on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>. You should really <a href="https://css-tricks.com/newsletters/">get the newsletter</a> as well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://css-tricks.com/form-automation-tips-for-happier-user-and-clients/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">391993</post-id>	</item>
	</channel>
</rss>

<!-- plugin=object-cache-pro client=phpredis metric#hits=7864 metric#misses=8 metric#hit-ratio=99.9 metric#bytes=5949713 metric#prefetches=389 metric#store-reads=27 metric#store-writes=2 metric#store-hits=398 metric#store-misses=4 metric#sql-queries=30 metric#ms-total=410.41 metric#ms-cache=24.07 metric#ms-cache-avg=0.8598 metric#ms-cache-ratio=5.9 -->
