<?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>Fri, 01 May 2026 13:43:28 +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>What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, and More</title>
		<link>https://css-tricks.com/whats-important-10/</link>
					<comments>https://css-tricks.com/whats-important-10/#respond</comments>
		
		<dc:creator><![CDATA[Daniel Schwarz]]></dc:creator>
		<pubDate>Fri, 01 May 2026 13:43:26 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[news]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=394456</guid>

					<description><![CDATA[<p>Developers have been experimenting with HTML-in-Canvas, a hexagonal world map-analytics feature, a web-based OS for e-ink devices, replacing image sources using the content property, and more. This is What’s !important #10.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-10/">What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, 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>Developers have been experimenting with HTML-in-Canvas, a hexagonal world map-analytics feature, a web-based OS for e-ink devices, replacing <code>img</code> <code>src</code>s using <code>content</code>, and more. This is <strong>What’s !important #10</strong>.</p>



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


<h3 class="wp-block-heading" id="htmlincanvas-experiments">HTML-in-Canvas experiments</h3>


<p>HTML-in-Canvas, a new API that enables us to render real semantic HTML in a <code>&lt;canvas&gt;</code> with visual effects, is the talk of the town right now, so let’s lead with that. <a href="https://css-tricks.com/author/amitsheen/">Amit Sheen</a> showed us <a href="https://frontendmasters.com/blog/the-web-is-fun-again-first-experiments-with-html-in-canvas/" rel="noopener">how the HTML-in-Canvas API works</a>, and also created some <a href="https://hicshowroom.com/" rel="noopener">demos over at the HiC Showroom</a>, like this one (requires Chrome 146 with the <code>chrome://flags/#canvas-draw-element</code> flag enabled):</p>



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


<h3 class="wp-block-heading" id="building-a-hexagonal-world-mapanalytics-feature">Building a hexagonal world map-analytics feature</h3>


<p>Ben Schwarz (awesome name, but no relation) talked about <a href="https://calibreapp.com/blog/building-our-beloved-hex-map" rel="noopener">building a hexagonal world map-analytics feature</a>. While it’s more of a retrospective than a developer walkthrough, it’s a <em>really</em> interesting read about analytics, design constraints, inspiration, engineering, and of course SVG and CSS.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" fetchpriority="high" decoding="async" width="1760" height="990" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=1760%2C990&#038;ssl=1" alt="A world map composed of small hexagons colored in orange, green, and red." class="wp-image-394491" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?w=1760&amp;ssl=1 1760w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=300%2C169&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=1024%2C576&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=768%2C432&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1-1.png?resize=1536%2C864&amp;ssl=1 1536w" sizes="(min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption">Source: <a href="https://calibreapp.com/blog/building-our-beloved-hex-map" rel="noopener">Calibre</a>.</figcaption></figure>


<h3 class="wp-block-heading" id="rekindle-a-webbased-os-for-eink-devices">Rekindle — a web-based OS for e-ink devices</h3>


<p><a href="https://rekindle.ink/" rel="noopener">Rekindle</a> is basically a web-based operating system for e-ink devices like Kindle, Kobo, and Boox, which are often low-powered with few features. Rekindle includes an insane number of features and apps, and is designed in black-and-white, with no animations, and no doubt with many more e-ink optimizations.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" decoding="async" width="2560" height="1606" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=2560%2C1606&#038;ssl=1" alt="A black and white user interface for Rekindle that primarily shows a grid of app icons." class="wp-image-394492" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?w=2560&amp;ssl=1 2560w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=300%2C188&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=1024%2C642&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=768%2C482&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=1536%2C963&amp;ssl=1 1536w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/2-scaled.png?resize=2048%2C1285&amp;ssl=1 2048w" sizes="(min-width: 735px) 864px, 96vw" /></figure>



<p>The takeaway isn’t a tutorial (unfortunately) or even some commentary (like with the world map retrospective above), it’s that we have a whole bunch of media queries that’d be so useful for e-ink devices if it weren’t for the fact that they’re shipping with low-powered, proprietary web browsers that don’t recognize them. <a href="https://www.w3.org/TR/mediaqueries-5/" rel="noopener">Media Queries Level 5</a> can query hover capability, the precision of pointers, display update frequency, color depth, monochromatic bit-depth, color index size, dynamic range, and more, probably.</p>



<p>Thoughts? Is e-ink optimization likely to break out in the coming years, or is low demand for these media queries why a dedicated service like Rekindle needs to exist? It’s worth noting that the browsers and many of the media queries are in active development, so I don’t know. Watch this space, maybe?</p>



<p>Either way, I’d love to see a dev deep dive on Rekindle!</p>


<h3 class="wp-block-heading" id="replacing-img-srcs-using-content">Replacing <code>img</code> <code>src</code>s using <code>content</code></h3>


<p><a href="https://bsky.app/profile/scrwd.mastodon.social.ap.brid.gy/post/3mjwgpvgy7k32" rel="noopener">Jon discovered</a> that CSS can be used to replace image sources, like this:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;img src="image.png" alt="Alt text"></code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">img {
  content: url(new-image.png) / "New alt text";
}</code></pre>



<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:wjkfbooeb4bmapfc2op7eeza/app.bsky.feed.post/3mjwgpvgy7k32" data-bluesky-cid="bafyreifcnpfsgjnbzi2x5kpbzrb7six3zsrupp2vqovedyrf2y3dvziizi" data-bluesky-embed-color-mode="system"><p lang="en">TIL! Who knew you could change the &quot;src&quot; of an #HTML &lt;img&gt; using #CSS:

img { content: url(whatever.png) }

NO PSEUDOS!

<iframe title="Untitled" id="cp_embed_MYjRQje" src="https://codepen.io/jon/embed/preview/MYjRQje?default-tabs=html%2Cresult&amp;height=300&amp;host=https%3A%2F%2Fcodepen.io&amp;slug-hash=MYjRQje" scrolling="no" frameborder="0" height="300" allowtransparency="true" allowfullscreen="true" allowpaymentrequest="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;"></iframe>

Seems to work in all current browsers too. How did I miss this?</p>&mdash; Jon (<a href="https://bsky.app/profile/did:plc:wjkfbooeb4bmapfc2op7eeza?ref_src=embed" rel="noopener">@scrwd.mastodon.social.ap.brid.gy</a>) <a href="https://bsky.app/profile/did:plc:wjkfbooeb4bmapfc2op7eeza/post/3mjwgpvgy7k32?ref_src=embed" rel="noopener">Apr 20, 2026 at 13:09</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>



<p>It’s really interesting to learn this about the <a href="https://css-tricks.com/almanac/properties/c/content/"><code>content</code></a> property, which has been Baseline for 11 years now. I experimented a bit more and discovered that this trick also works with the <a href="https://css-tricks.com/almanac/functions/i/image-set/"><code>image-set()</code></a> function:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">img {
  content: image-set(
    url("image.png") 1x,
    url("image-2x.png") 2x
  );
}</code></pre>



<p>So if you’re working on a website with non-responsive <code>&lt;img&gt;</code>s and no way to change the markup, write the logic in CSS instead.</p>


<h3 class="wp-block-heading" id="implementing-responsive-images-with-sizesauto">Implementing responsive images with <code>sizes=auto</code></h3>


<p>Having said that, if you <em>do</em> have access to the HTML, you’ll want to serve responsive images using the <code>srcset</code> and <code>sizes</code> HTML attributes. <a href="https://css-tricks.com/author/wilto/">Mat Marquis</a> demonstrated <a href="https://piccalil.li/blog/the-end-of-responsive-images/" rel="noopener">how the new <code>sizes=auto</code> attribute-value combination replaces responsive breakpoints for images that are loaded lazily</a>.</p>



<p>If you’re interested, Amit Sheen also talked about <a href="https://frontendmasters.com/blog/building-a-ui-without-breakpoints/" rel="noopener">building layouts (not necessarily images) without breakpoints</a>.</p>


<h3 class="wp-block-heading" id="new-web-platform-features-and-updates">New web platform features and updates</h3>


<ul class="wp-block-list">
<li><a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/150" rel="noopener">Firefox 150</a>
<ul class="wp-block-list">
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:muted" rel="noopener"><code>:muted</code></a> and all other media-based pseudo-classes (no Chrome support)</li>



<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/revert-rule" rel="noopener"><code>revert-rule</code></a> (no Safari support)</li>



<li><a href="https://piccalil.li/blog/the-end-of-responsive-images/" rel="noopener"><code>sizes=auto</code></a> (no Safari support)</li>



<li><a href="https://css-tricks.com/almanac/functions/l/light-dark/"><code>light-dark()</code></a> with image support (no Chrome/Safari support)</li>



<li><a href="https://css-tricks.com/almanac/functions/c/color-mix/"><code>color-mix()</code></a> with the syntax for two or more colors (no Chrome/Safari support)</li>



<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaNotify" rel="noopener"><code>ariaNotify()</code></a> (no Chrome/Safari support)</li>
</ul>
</li>



<li><a href="https://developer.apple.com/documentation/safari-technology-preview-release-notes/stp-release-242" rel="noopener">Safari TP 242</a>
<ul class="wp-block-list">
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby" rel="noopener"><code>closedby</code></a></li>



<li>Advanced <a href="https://css-tricks.com/almanac/functions/a/attr/"><code>attr()</code></a> (no Firefox support)</li>
</ul>
</li>
</ul>



<p>If you’re keen for more content, here’s Wes Bos and Scott Tolinski of Syntax.fm discussing <a href="https://www.youtube.com/watch?v=unqPqGeJMck" rel="noopener">10 new CSS and HTML APIs</a>:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe title="10 New CSS and HTML APIs" width="500" height="281" src="https://www.youtube.com/embed/unqPqGeJMck?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<p>Until next time!</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/whats-important-10/">What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, 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-10/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">394456</post-id>	</item>
		<item>
		<title>The Importance of Native Randomness in CSS</title>
		<link>https://css-tricks.com/the-importance-of-native-randomness-in-css/</link>
					<comments>https://css-tricks.com/the-importance-of-native-randomness-in-css/#respond</comments>
		
		<dc:creator><![CDATA[Alvaro Montoro]]></dc:creator>
		<pubDate>Thu, 30 Apr 2026 15:26:18 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[CSS functions]]></category>
		<category><![CDATA[random]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393373</guid>

					<description><![CDATA[<p>We're getting new functions for generating random numbers in CSS! But the road to get here has been a long and winding one.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-importance-of-native-randomness-in-css/">The Importance of Native Randomness 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>Recently, I published a <a href="https://alvaromontoro.com/blog/68092/native-random-values-in-css" rel="noopener">story about the new random functions that have landed in CSS</a> and how they work. In this article, we’ll explore the challenges of randomness in CSS, how the concept has evolved over time, and why this native feature is a big deal.</p>



<p>One of the first things I wanted to do when I started developing websites was create unique experiences that changed from person to person. Just little things: a random background here, random colors there… Even small micro-interactions, like confetti or falling snow, needed some level of randomness to feel natural.</p>



<p>And I was not alone! I soon discovered that many web developers (“webmasters,” at the time) wanted to do things like that: adding wow factors and a sense of uniqueness to their sites. But we had a problem: CSS.</p>



<p>CSS is a declarative and deterministic language. Two characteristics that clash with the idea of natural variation:</p>



<ul class="wp-block-list">
<li><strong>Declarative</strong> means that it focuses on the <em>what</em>, not the <em>how</em>. In contrast to imperative languages, developers using CSS tell the browser what the expected result is, but not how to achieve it.</li>



<li><strong>Deterministic</strong> means that for a given input we will get the same output. Always the same. If you specify that a color will be red, that color will be red, not blue or yellow.</li>
</ul>



<p>This is by design, and it’s one of the things that makes CSS predictable and reliable. If you understand how the layout engine works, you can tell which styles will be applied at any given time. Which is great… but not so great if you want to generate random content.</p>



<p>And so began a challenging (and sometimes tortuous) journey for designers and developers to achieve natural variation from a deterministic system.</p>



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


<h3 class="wp-block-heading" id="the-long-and-winding-road-to-random-styles">The Long and Winding Road to Random Styles</h3>


<p>The path to random styles in CSS is paved with multiple attempts and shortcomings. But at every step along the way, developers found new solutions that improved on the previous ones. Even if only a little.</p>



<p><strong>Note:</strong> This timeline reflects logical progress more than a strict historical or chronological order.</p>


<h4 class="wp-block-heading" id="css-pseudo-randomness-and-patterns">CSS Pseudo-Randomness and Patterns</h4>


<p>We can simulate randomness in CSS by creating patterns. But this is not truly random. The results will always be the same, and sooner or later people will notice the pattern.</p>



<p>One way to create this simulation is by using <a href="https://css-tricks.com/almanac/pseudo-selectors/n/nth-child/"><code>:nth-child()</code></a> selectors or by playing with animations. The first method is easy but yields subpar results; the second may trick and impress some people.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Warning:</strong> Auto-playing media</summary>
<figure class="wp-block-image size-full ticss-3ca19010"><img data-recalc-dims="1" loading="lazy" decoding="async" width="400" height="460" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1774042970501_random-0.webp?resize=400%2C460" alt="Animated illustrated of five numbered cards swapping positions with the first card moving toward the back in succession." class="wp-image-393374" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1774042970501_random-0.webp?w=400&amp;ssl=1 400w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1774042970501_random-0.webp?resize=261%2C300&amp;ssl=1 261w" sizes="auto, (min-width: 735px) 864px, 96vw" /><figcaption class="wp-element-caption"><strong>Credit:</strong> Alvaro Montoro</figcaption></figure>
</details>



<p>Needless to say, these methods are hacks that don’t provide randomization at any level. A human may not be able to precisely predict which value comes next — at least not without some effort — but a machine certainly can.</p>


<h4 class="wp-block-heading" id="pre-processors-to-the-rescue">Pre-Processors to the Rescue</h4>


<p>We turned to the next best thing: tooling. In particular, CSS preprocessors such as Sass, SCSS, Less, and the like. These tools include math modules that provide random functions we can use at compilation time.</p>



<p>The key phrase in the previous paragraph is “<strong>at compilation time.</strong>” Yes, we are generating random values for our CSS properties. But once those values are produced during compilation, they are frozen forever (or until the next compilation, to be more precise). Just like a mosquito stuck in amber.</p>



<p>The values will be random when the CSS is generated, but every time visitors visit or refresh the page, they will get the same ones. To produce new values, we would need to recompile the stylesheets.</p>



<p>This was a baby step toward styling randomization, but there was still a long way to go.</p>


<h4 class="wp-block-heading" id="server-side-randomness">Server-Side Randomness</h4>


<p>We moved to the next best thing: using other languages to generate random values and passing them to CSS through HTML. Server-side languages like PHP, Java, ASP, and others were perfect for this task while generating the HTML (or even the CSS itself).</p>



<p>This approach works well: we get new random values every time the page is generated, which usually means every time it is visited or refreshed. We also have full control over the randomization, since we can implement our own functions.</p>



<p>It has shortcomings, too. If new content is added dynamically to the page, it gets stuck with the “frozen” values generated during the initial page load. Better than patterns, better than preprocessors… but still not perfect.</p>



<p>This limitation became an even bigger problem with the rise and widespread adoption of single-page applications and client-side JavaScript architectures.</p>


<h4 class="wp-block-heading" id="and-javascript-finally-">And JavaScript&#8230; Finally!</h4>


<p>With the proliferation of web applications, it made sense to move randomness to JavaScript. The language is already heavily used, and adding a few random functions to the mix doesn’t seem like a big stretch.</p>



<p>And JavaScript finally solved it! For the first time, styles could actually behave with natural variation: random on creation, on refresh, and even on mutation.</p>



<p>It can be done in many ways, too: using frameworks, CSS-in-JS libraries, or plain vanilla JavaScript. The methods to incorporate styling through this language are vast and well supported. There are some performance and complexity concerns, but JavaScript gets the job done.</p>



<p>We finally had true randomization in web styles… just not in CSS itself.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1098" height="568" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?resize=1098%2C568" alt="Summary of the different technologies and how they handle randomization" class="wp-image-393376" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?w=1098&amp;ssl=1 1098w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?resize=300%2C155&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?resize=1024%2C530&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/1AN3S3F-_uTESNck1us4Hkg.png?resize=768%2C397&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>


<h3 class="wp-block-heading" id="a-web-problem-and-a-web-solution">A Web Problem, and a Web Solution</h3>


<p>That last part is important. We have randomization on the web (JavaScript gets the job done) but something feels off. Something doesn’t quite feel right. At its core, that discomfort comes from two things:</p>



<ul class="wp-block-list">
<li>We are applying an imperative solution to a declarative problem.</li>



<li>We are moving layout decisions from CSS to JavaScript.</li>
</ul>


<h4 class="wp-block-heading" id="an-imperative-solution-to-a-declarative-problem">An Imperative Solution to a Declarative Problem</h4>


<p>We mentioned earlier that CSS is a declarative language that focuses on the <em>what</em>, while JavaScript is an imperative language that focuses on the <em>how</em>.</p>



<p>By moving randomization to JavaScript, we are trying to answer a <em>what</em> question with a <em>how</em> answer. It works, but it’s not ideal.</p>



<p>Using JavaScript, we finally achieved style randomness at all levels: when the page is created, when it is refreshed, and when elements are added or changed (mutation). But in doing so, we are breaking the model.</p>



<p>CSS handles layout, and JavaScript handles logic. We solved a CSS limitation by moving layout decisions into JavaScript, creating a mismatch that produces that subtle “this isn’t quite right” feeling — even when everything technically works.</p>


<h4 class="wp-block-heading" id="the-css-solution">The CSS Solution</h4>


<p>The solution to this model mismatch is simple: <strong>move randomization to CSS</strong>. Solve a layout problem directly in the layout layer instead of delegating it to a different tool or language. And this happened with the introduction of two new random functions as part of the <a href="https://www.w3.org/TR/css-values-5/#randomness" rel="noopener">CSS Values and Units Module Level 5</a>:</p>



<ul class="wp-block-list">
<li><strong><code>random()</code>:</strong> generates a random value between a minimum and a maximum.</li>



<li><strong><code>random-item()</code>:</strong> selects a random value from a given list.</li>
</ul>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1324" height="596" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?resize=1324%2C596&#038;ssl=1" alt="Showing a CSS code snippet of the random and random-item functions." class="wp-image-393378" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?w=1324&amp;ssl=1 1324w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?resize=300%2C135&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?resize=1024%2C461&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/s_CE13D78A2D1C5E5CFBD3EBB73274C0516735336AF6D86F2BD275A2E87DE47B98_1775479621638_image.png?resize=768%2C346&amp;ssl=1 768w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>This approach also aligns with the <a href="https://www.w3.org/2001/tag/doc/leastPower.html" rel="noopener"><strong>Rule of Least Power</strong></a>, which suggests choosing the least powerful language suitable for a given purpose. In practice, this means solving a problem using the least powerful language capable of expressing and solving it.</p>



<p>Usually, that language will be better suited to the task. Its features will be adapted to the level at which they are applied, making them simpler, more efficient, and better performing. While a more powerful language can certainly do the job, it often introduces an unnecessary layer of complexity and abstraction.</p>



<p>On the web platform, we have <strong>HTML</strong> for structure (least powerful), <strong>CSS</strong> for styling and layout (more powerful), and <strong>JavaScript</strong> (significantly more powerful). By implementing randomization in CSS, we move the solution to the appropriate layer while also following the Rule of Least Power.</p>



<p>And that’s one of the reasons the new random CSS features are such a big deal… and why they represent something much bigger than just <em>another feature</em>.</p>


<h3 class="wp-block-heading" id="the-big-deal">The Big Deal</h3>


<p>CSS has always been deterministic by design, and native randomness breaks with that tradition. It isn’t just another feature, it represents a shift in how we think about CSS as a language and about the web platform itself.</p>



<p>For the first time, CSS can model natural systems with variation directly: no hacks, no tools, no outsourcing layout decisions to other languages. Randomization takes an honored place in the styling layer, where it always belonged.</p>



<p>This unlocks creative possibilities: generative layouts, organic patterns, playful micro-interactions, and design systems that feel alive and unique. But it also restores architectural clarity: each layer of the web once again does the job it was designed for.</p>



<p>With this change, CSS moves from being purely a styling language toward becoming a generative layout system. It is no longer just a passive actor in web development; it becomes an active participant in the rendering process, defining a space of possible outcomes that the browser resolves into a concrete page.</p>



<p>And that’s the real big deal. Native randomness isn’t just about making things look different; it’s about making the platform more coherent and expressive.</p>



<p>It’s also a reminder that CSS is still evolving, and that sometimes the features people overlook can reshape how we think about a language, and what we imagine is possible on the web.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/the-importance-of-native-randomness-in-css/">The Importance of Native Randomness 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/the-importance-of-native-randomness-in-css/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393373</post-id>	</item>
		<item>
		<title>contrast()</title>
		<link>https://css-tricks.com/almanac/functions/c/contrast/</link>
		
		<dc:creator><![CDATA[Gabriel Shoyombo]]></dc:creator>
		<pubDate>Wed, 29 Apr 2026 14:58:19 +0000</pubDate>
				<category><![CDATA[filter]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=392899</guid>

					<description><![CDATA[<p>The <code>contrast()</code> filter function increases or decreases the contrast of an element.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/c/contrast/">contrast()</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>The CSS <code>contrast()</code> filter function increases or decreases the contrast of an element, either making colors pop out more or dulling them to gray. Unlike other <code><a href="https://css-tricks.com/almanac/properties/f/filter/">filter</a></code> functions like <code><a href="https://css-tricks.com/almanac/functions/b/brightness/">brightness()</a></code> or <code>saturate()</code>, <code>contrast()</code> affects both saturation and lightness, keeping only the color&#8217;s hue.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.low {
  filter: contrast(50%);
}

.normal {
  filter: contrast(100%);
}

.high {
  filter: contrast(200%);
}</code></pre>



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



<p>The&nbsp;<code>contrast()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/filter-effects/#funcdef-filter-contrast" rel="noopener">Filter Effects Module Level 1</a>&nbsp;specification.</p>


<h3 class="wp-block-heading" id="syntax">Syntax</h3>


<p>The official syntax for the&nbsp;<code>contrast()</code>&nbsp;function is:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;contrast()> = contrast( [ &lt;number> | &lt;percentage> ]? )</code></pre>



<p>Or simply:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">filter: contrast(&lt;amount>);</code></pre>



<p>The <code>contrast()</code> function is only compatible with the CSS <code><a href="https://css-tricks.com/almanac/properties/f/filter/">filter</a></code> and <code><a href="https://css-tricks.com/almanac/properties/b/backdrop-filter/">backdrop-filter</a></code> properties.</p>


<h3 class="wp-block-heading" id="argument">Arguments</h3>


<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Using percentages */
filter: contrast(0%); /* Totally grayed out */
filter: contrast(50%); /* Partially grayed out */
filter: contrast(100%); /* No change */
filter: contrast(150%); /* Element is 1.5 times more defined */

/* Using numbers (0–1 range) */
filter: contrast(0); /* Totally grayed out */
filter: contrast(0.5); /* Partially grayed out */
filter: contrast(1); /* No change */
filter: contrast(1.5); /* Element is 1.5 times more defined */

/* Using percentages */
filter: contrast(0%); /* Totally grayed out */
filter: contrast(50%); /* Partially grayed out */
filter: contrast(100%); /* No change */
filter: contrast(150%); /* Element is 1.5 times more defined */

/* Using numbers (0–1 range) */
filter: contrast(0); /* Totally grayed out */
filter: contrast(0.5); /* Partially grayed out */
filter: contrast(1); /* No change */
filter: contrast(1.5); /* Element is 1.5 times more defined */

/* Works with CSS variables */
--amount: 200%;
filter: contrast(--amount);

/* No argument */
filter: contrast(); /* No change */

/* Negative value */
filter: contrast(-1.5); /* No effect */
filter: contrast(--amount);

/* No argument */
filter: contrast(); /* No change */

/* Negative value */
filter: contrast(-1.5); /* No effect */</code></pre>



<p>The <code>contrast()</code> function takes a single argument, which can be a positive decimal or percentage value. The argument determines the new contrast for the element, where:</p>



<ul class="wp-block-list">
<li><code>0</code>&nbsp;or&nbsp;<code>0%</code>&nbsp;dries out all contrast from the element, resulting in a completely gray image.</li>



<li><code>1</code>&nbsp;or&nbsp;<code>100%</code>&nbsp;leaves the element completely unchanged.</li>



<li>Values above&nbsp;<code>1</code>&nbsp;or&nbsp;<code>100%</code>&nbsp;increase the contrast linearly.</li>
</ul>



<p>Negative values aren&#8217;t allowed. But CSS variables are:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.element {
  --filter-amount: 150%;
  filter: contrast(var(--filter-amount));
}</code></pre>


<h3 class="wp-block-heading" id="how-contrast-affects-color">How <code>contrast()</code> affects color</h3>


<p>Like other filter functions, the&nbsp;<code>contrast()</code>&nbsp;filter operates purely on RGB math. Specifically, given an&nbsp;<code>&lt;amount&gt;</code>&nbsp;it multiplies each RGB channel by that&nbsp;<code>&lt;amount&gt;</code>&nbsp;and then adds&nbsp;<code>255 * (0.5 - 0.5 * &lt;amount&gt;)</code>&nbsp;to the result. In practice, this affects colors in one of two ways:</p>



<ul class="wp-block-list">
<li>High contrast (greater than&nbsp;<code>1</code>) makes light pixels get lighter and dark pixels get darker, so colors become more vivid.</li>



<li>Low contrast (smaller than&nbsp;<code>1</code>) pulls all pixels toward a middle gray. This reduces the difference between light and dark areas, making the image look flat and muted.</li>
</ul>


<h3 class="wp-block-heading" id="basic-usage">Basic usage</h3>


<p>Some background images, usually in hero sections or carousels, can make the foreground text difficult to read. Especially if it has very bright and dark colors, which compete with any text color. To solve this, we can use&nbsp;contrast()&nbsp;to reduce the difference between the image&#8217;s whites and blacks, making text more readable against the whole image.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">img {
    filter: contrast(70%) brightness(60%);
}</code></pre>



<p>The low contrast flattens the image, and as a plus, we can also reduce the image&#8217;s brightness to make the text pop regardless of its colors.</p>



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


<h3 class="wp-block-heading" id="making-product-card-images-pop-on-hover">Demo: Making product card images pop on hover</h3>


<p>Another useful application for&nbsp;<code>contrast()</code>&nbsp;is to highlight an image in a user&#8217;s interaction. For example, in a row of image cards, we could increase the image&#8217;s contrast and also scale it on hover</p>



<pre rel="SCSS" class="wp-block-csstricks-code-block language-scss" data-line=""><code markup="tt">.card img {
  transition:
    filter 0.4s ease,
    transform 0.4s ease;
}

.card:hover img {
  filter: contrast(125%);
  transform: scale(1.05);
}</code></pre>



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


<h3 class="wp-block-heading" id="is-contrast-the-same-as-contrast-color-">Is <code>contrast()</code> the same as <code>contrast-color()</code>?</h3>


<p>While both CSS functions have similar names, they are not to be confused with each other.</p>



<ul class="wp-block-list">
<li><strong><code>contrast()</code> is a filter function that makes an element more vivid</strong> by making whites lighter and blacks darker.</li>



<li><strong><a href="https://css-tricks.com/exploring-the-css-contrast-color-function-a-second-time/"><code>contrast-color()</code></a> returns the text color with the highest contrast to a solid background.</strong> Its resulting color is either white or black, depending on which color contrasts most with the background. It is also not a filter function.</li>
</ul>


<h3 class="wp-block-heading" id="browser-support">Browser support</h3>


<p>The&nbsp;<code>contrast()</code>&nbsp;function is currently supported across all modern browsers.</p>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="backdrop-filter"></baseline-status>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="filter"></baseline-status>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/c/contrast/">contrast()</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>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392899</post-id>	</item>
		<item>
		<title>contrast-color()</title>
		<link>https://css-tricks.com/almanac/functions/c/contrast-color/</link>
		
		<dc:creator><![CDATA[Gabriel Shoyombo]]></dc:creator>
		<pubDate>Wed, 29 Apr 2026 14:57:48 +0000</pubDate>
				<category><![CDATA[color]]></category>
		<category><![CDATA[contrast-color()]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=392903</guid>

					<description><![CDATA[<p>The <code>contrast-color()</code> function takes a <code>&#60;color&#62;</code> and returns either black or white, whichever is the most contrasting color for that value.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/c/contrast-color/">contrast-color()</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>The CSS <code>contrast-color()</code> function takes a <code>&lt;color></code> value (as well as a variable) and returns either black or white, whichever is the most contrasting color for that value.</p>



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



<p>In other words, <code>contrast-color()</code> is sort of an accessibility tool for conforming to <a href="https://w3c.github.io/wcag/guidelines/22/#contrast-minimum" rel="noopener">WCAG contrast requirements</a>.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card {
  background-color: var(--swatch);
  color: contrast-color(var(--swatch));
}</code></pre>



<p>For example, on the next demo update the background color to see the text color change automatically.</p>



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



<p>The&nbsp;<code>contrast-color()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-color-5/#contrast-color" rel="noopener">CSS Color Module Level 5</a>&nbsp;specification.</p>


<h3 class="wp-block-heading" id="syntax">Syntax</h3>


<p>The CSS <code>contrast-color()</code> function syntax is is formatted like this:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">contrast-color() = contrast-color( &lt;color> )</code></pre>



<p>Let&#8217;s break that down with examples.</p>


<h3 class="wp-block-heading" id="argument">Arguments</h3>


<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Using a custom variable */
contrast-color(var(--base-background));

/* Passing a color directly */
contrast-color(#34cdf2);
contrast-color(green);</code></pre>



<p><code>contrast-color()</code>&nbsp;takes a&nbsp;<code>&lt;color&gt;</code>&nbsp;as its only argument and resolves to white or black, depending on which has the highest contrast. If both white and black have the same contrast level, the function defaults to white.</p>


<h3 class="wp-block-heading" id="basic-usage">Basic usage</h3>


<p>The&nbsp;<code>contrast-color()</code>&nbsp;give us a simple alternative to defining multiple background and text colors, while also ensuring they are contrasting enough. Imagine we had the following scenario:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  --primary-text: #f1f8e9;
  --primary-bg: #2d5a27;
  --secondary-text: #311b92;
  --secondary-bg: #d1c4e9;
  --tertiary-text: #002b36;
  --tertiary-bg: #ff5722;
}

.primary {
  color: var(--primary-text);
  background-color: var(--primary-bg);
}

.secondary {
  color: var(--secondary-text);
  background-color: var(--secondary-bg);
}

.tertiary {
  color: var(--tertiary-text);
  background-color: var(--tertiary-bg);
}</code></pre>



<p>We defined a text color for each background color in our variables, and if we had more than three possible backgrounds, we&#8217;d have had to define them all. Instead, using&nbsp;<code>contrast-color()</code>, we could define only the background color for each theme and let the function return the appropriate contrasting color for the texts.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  --primary: #2d5a27;
  --secondary: #d1c4e9;
  --tertiary: #ff5722;
}

.primary {
  color: contrast-color(var(--primary));
  background-color: var(--primary);
}

.secondary {
  color: contrast-color(var(--secondary));
  background-color: var(--secondary);
}

.tertiary {
  color: contrast-color(var(--tertiary-bg));
  background-color: var(--tertiary-bg);
}</code></pre>



<p><strong>It is important to note that <code>contrast-color()</code> is still a work in progress</strong> (at the time of this writing), and in some cases might not be appropriate from a design standpoint since it only returns black or white. Therefore, I recommend using it only in simple scenarios where either black or white make sense.</p>



<p>In fact, it has some shortcomings that are worth noting.</p>


<h3 class="wp-block-heading" id="-contrast-color-shortcomings"><code>contrast-color()</code> shortcomings</h3>


<p>While&nbsp;<code>contrast-color()</code>&nbsp;appears to improve web accessibility, it has&nbsp;<em>buts</em>&nbsp;we should be aware of before using it.</p>



<ul class="wp-block-list">
<li><strong>It resolves to only black or white texts.</strong> Although the draft promises more control in the future, we have to stick to those two colors for now.</li>



<li><strong>We&#8217;re stuck with white when using colors where neither black nor white is a sufficient contrast</strong>, or they both have the same contrast.</li>



<li><strong><code>contrast-color()</code> only works with colors for now.</strong> So, in cases where you&#8217;re working with text on background images or using font weights to increase contrast, you&#8217;ll have to find a different way to meet contrast requirements. And even if it can be technically used with gradients, these too can only go between black to white which might not provide enough contrast between the gradient colors.</li>



<li><strong><code>contrast-color()</code> doesn&#8217;t account for the <code>font-size</code></strong>, which is a defining criterion, in choosing a contrast color. Hopefully, this will be accounted for in the future.</li>
</ul>



<p>So, at the time of writing, it seems it&#8217;s better to manually define colors that are contrasting enough in our themes as&nbsp;<code>contrast-color()</code>&nbsp;isn&#8217;t really feasible right now.</p>


<h3 class="wp-block-heading" id="older-syntax">Older syntax</h3>


<p>Based on earlier <a href="https://css-tricks.com/exploring-color-contrast-for-the-first-time/">articles</a>, the <code>contrast-color()</code> function used to take multiple <code>color</code> arguments–the base color versus multiple contrasting color options to choose from:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">contrast-color(var(--bg) vs red, lightgreen, blue)</code></pre>



<p>This syntax no longer exists in the draft. It&#8217;s one color and one color only.</p>


<h3 class="wp-block-heading" id="specification">Specification</h3>


<p>The&nbsp;<code>contrast-color()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-color-5/#contrast-color" rel="noopener">CSS Color Module Level 5</a>&nbsp;specification.</p>


<h3 class="wp-block-heading" id="browser-support">Browser support</h3>



<baseline-status class="wp-block-css-tricks-baseline-status" featureId="contrast-color"></baseline-status>



<p>While browser support is limited at the time of this writing, it&#8217;s a good idea to include a fallback if you&#8217;re planning to use it on a project. We can use the <a href="https://css-tricks.com/almanac/rules/s/supports/"><code>@supports</code></a> at-rule to detect if the browser understands the function:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.card {
  --bg-color: #2d5a27;
  background-color: var(--bg-color);

  /* Default Fallback */
  color: ghostwhite;
}

/* Use the function if supported */
@supports (color: contrast-color(red)) {
  .card {
    color: contrast-color(var(--bg-color));
  }
}</code></pre>


<h3 class="wp-block-heading" id="references-earlier-articles-">Further reading:</h3>


<ul class="wp-block-list">
<li><a href="https://www.smashingmagazine.com/2022/05/accessible-design-system-themes-css-color-contrast/" rel="noopener">Manage Accessible Design System Themes With CSS Color-Contrast()</a> (Daniel Yuschick)</li>
</ul>



    		
    <div class="in-article-cards">
      <article class="in-article-card articles" id="mini-post-391853">

      <div class="tags">
      <a href="https://css-tricks.com/tag/accessibility/" rel="tag">accessibility</a> <a href="https://css-tricks.com/tag/color/" rel="tag">color</a> <a href="https://css-tricks.com/tag/contrast-color/" rel="tag">contrast-color()</a> <a href="https://css-tricks.com/tag/relative-color/" rel="tag">relative color</a>    </div>
  
  <time datetime="2026-02-11" title="Originally published Feb 11, 2026">
    <strong>
                
        Article
      </strong>

    on

    Feb 11, 2026  </time>

  <h3>
    <a href="https://css-tricks.com/approximating-contrast-color-with-other-css-features/">
      Approximating contrast-color() With Other CSS Features    </a>
  </h3>

  
  <div class="author-row">
    <a href="https://css-tricks.com/author/kevinhamer/" aria-label="Author page of Kevin Hamer">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/b3681cc5fdff5f6445d6405c9a765816c05815cd85b88bdb69804b721ff0d14b.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/b3681cc5fdff5f6445d6405c9a765816c05815cd85b88bdb69804b721ff0d14b.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/kevinhamer/">
      Kevin Hamer    </a>
  </div>

</article>
<article class="in-article-card articles" id="mini-post-386829">

      <div class="tags">
      <a href="https://css-tricks.com/tag/accessibility/" rel="tag">accessibility</a> <a href="https://css-tricks.com/tag/color/" rel="tag">color</a> <a href="https://css-tricks.com/tag/contrast-color/" rel="tag">contrast-color()</a> <a href="https://css-tricks.com/tag/css-functions/" rel="tag">CSS functions</a>    </div>
  
  <time datetime="2025-06-05" title="Originally published Jun 5, 2025">
    <strong>
                
        Article
      </strong>

    on

    Jun 5, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/exploring-the-css-contrast-color-function-a-second-time/">
      Exploring the CSS contrast-color() Function… a Second Time    </a>
  </h3>

  
  <div class="author-row">
    <a href="https://css-tricks.com/author/danielschwarz/" aria-label="Author page of Daniel Schwarz">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/b6c928ce1b901c34f86f3856b5a3bcefe9a4fb94698b9778fb3df5802d66e25d.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/b6c928ce1b901c34f86f3856b5a3bcefe9a4fb94698b9778fb3df5802d66e25d.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/danielschwarz/">
      Daniel Schwarz    </a>
  </div>

</article>
<article class="in-article-card links" id="mini-post-389698">

      <div class="tags">
      <a href="https://css-tricks.com/tag/color/" rel="tag">color</a> <a href="https://css-tricks.com/tag/contrast-color/" rel="tag">contrast-color()</a> <a href="https://css-tricks.com/tag/css-functions/" rel="tag">CSS functions</a>    </div>
  
  <time datetime="2025-10-08" title="Originally published Oct 8, 2025">
    <strong>
                
        Link
      </strong>

    on

    Oct 8, 2025  </time>

  <h3>
    <a href="https://css-tricks.com/the-thing-about-contrast-color/">
      The thing about contrast-color    </a>
  </h3>

  
  <div class="author-row">
    <a href="https://css-tricks.com/author/geoffgraham/" aria-label="Author page of Geoff Graham">
      <img data-recalc-dims="1" alt='' src="https://i0.wp.com/css-tricks.com/wp-content/cache/breeze-extra/gravatars/a8e040142716a4b44d014d80fbcf99c635b1d8faabfe469b6954a8ef2f168595.jpg?resize=80%2C80&#038;ssl=1" srcset='https://css-tricks.com/wp-content/cache/breeze-extra/gravatars/a8e040142716a4b44d014d80fbcf99c635b1d8faabfe469b6954a8ef2f168595.jpg 2x' class='avatar avatar-80 photo' height="80" width="80" />    </a>
  
    <a class="author-name" href="https://css-tricks.com/author/geoffgraham/">
      Geoff Graham    </a>
  </div>

</article>
    </div>
  



<p></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/c/contrast-color/">contrast-color()</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>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392903</post-id>	</item>
		<item>
		<title>Let’s Use the Nonexistent ::nth-letter Selector Now</title>
		<link>https://css-tricks.com/using-nonexistent-nth-letter-selector-now/</link>
					<comments>https://css-tricks.com/using-nonexistent-nth-letter-selector-now/#comments</comments>
		
		<dc:creator><![CDATA[Lee Meyer]]></dc:creator>
		<pubDate>Mon, 27 Apr 2026 13:55:42 +0000</pubDate>
				<category><![CDATA[Articles]]></category>
		<category><![CDATA[pseudo elements]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?p=393324</guid>

					<description><![CDATA[<p>My shim might give the powers that be another reason to say native support isn't necessary, or if lots of people use my :nth-letter hack in the wild, the browser gods might recognize the need to implement it for real.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/using-nonexistent-nth-letter-selector-now/">Let’s Use the Nonexistent ::nth-letter Selector Now</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[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>&#8220;I think I’m done with reality.&#8221;</p>
<cite>— <a href="https://music.youtube.com/watch?v=oVyZNLx46IA&amp;si=9YtNpbZVCJiPZcp-" rel="noopener">The Seventh Circle</a> by Architects</cite></blockquote>



<p>We’ve all, at some point, had the <a href="https://medium.com/@joseph0crick/the-awful-css-language-9605534ca6e" rel="noopener">thought</a> that <a href="https://forum.level1techs.com/t/css-sucks-and-i-hate-it/171341" rel="noopener">CSS sucks</a>. Indeed, the overhyped buzz around&nbsp;<a href="https://blog.damato.design/posts/pretext-review/" target="_blank" rel="noreferrer noopener">the new pretext.js library</a>&nbsp;as a &#8220;CSS killer&#8221; reflects how much we all want to strangle CSS at times</p>



<p>Someday in the future, CSS might answer back: “No, you are <a href="https://idiallo.com/blog/learn-css" rel="noopener">the one who sucks at CSS</a>. Here’s the <a href="https://drafts.css-houdini.org/css-parser-api/" rel="noopener">CSS Parser API</a>. Go make your own styling language and <a href="https://dev.to/nombrekeff/flutter-styling-explained-in-css-llf-5-51nm" rel="noopener">see how close any alternative is to perfect</a>.”</p>



<p>Well, CSS, you’ve been teasing me since 2017 with the possibility of that API, which I hoped would let me create my own CSS syntax, but no such thing materialized.</p>



<p>And while I am venting, since 2003 we’ve asked <a href="https://annevankesteren.nl/2003/09/from-a-markover-to-pseudo-elements" rel="noopener">over</a> and <a href="https://github.com/w3c/csswg-drafts/issues/3208" rel="noopener">over</a> and <a href="https://css-tricks.com/what-else-is-on-your-css-wishlist/">over</a> for <code>::nth-letter</code>, which seems like a natural suggestion. I mean, we’ve always had <a href="https://css-tricks.com/almanac/pseudo-selectors/f/first-letter/"><code>::first-letter</code></a> to mimic print effects like <a href="https://www.nicksimson.com/posts/2024-drop-caps" rel="noopener">drop caps</a>, so we know you could do <code>::nth-letter</code> if you wanted.</p>



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



<p>You are just a tease, CSS, which means that in 2026, I still can’t write styles like Chris Coyier’s <a href="https://css-tricks.com/a-call-for-nth-everything/#aa-nth-letter-last-letter-nth-last-letter">hypothetical example</a> from back in 2011.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">h1.fancy::nth-letter(n) {
  display: inline-block;
  padding: 20px 10px;
  color: white;
}

h1.fancy::nth-letter(even) {
  transform: skewY(15deg);
  background: #C97A7A;
}

h1.fancy::nth-letter(odd) {
  transform: skewY(-15deg);
  background: #8B3F3F;
}</code></pre>


<h3 class="wp-block-heading" id="impossible-demos-of-nth-letter-">Impossible demos of <code>::nth-letter</code></h3>


<p>If you prefer to play with an interactive example, here is the invalid syntax <code>::nth-letter</code> working in CodePen.</p>



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



<p>And here’s a video demo by my eight-year-old, to demonstrate that using this syntax is child’s play.</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="nth letter polyfill" width="500" height="281" src="https://www.youtube.com/embed/Nku5thWAM8Y?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<p>If <code>::nth-letter</code> existed, we could migrate my <a href="https://css-tricks.com/spiral-scrollytelling-in-css-with-sibling-index/">text vortex scrolling effect</a> to use it, and then delete the JavaScript, as seen below. This is Chrome/Safari-only, due to the use of the new <a href="https://css-tricks.com/almanac/functions/s/sibling-index/"><code>sibling-index()</code></a> function.</p>



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



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-4-3 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="spiral scrollytelling in nth letter fork" width="500" height="375" src="https://www.youtube.com/embed/hayNFlmN7PY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<p>If we had <code>::nth-letter</code>, we could migrate Temani Afif’s amazing <a href="https://codepen.io/t_afif/embed/xbOzxyp" rel="noopener">direction-aware elastic hover</a>, then gleefully delete all the spans in the original markup around each letter. The <code>::nth-letter</code> code would be as shown in the CodePen below.</p>



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



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="nth letter fork  Direction aware Elastic hover effect" width="500" height="281" src="https://www.youtube.com/embed/n3fcB51CqgY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<p>If only <code>::nth-letter</code> existed, I might make it my mission to go around upgrading every <a href="https://letteringjs.com/" rel="noopener">typography styling</a> demo to use it.</p>



<p>Alas, the syntax to make this work is not possible with CSS and HTML. Such capabilities exist only in the wildest realms of our imagination. Article ends here.</p>


<h3 class="wp-block-heading" id="wait-what-how-do-all-those-demos-work-">Wait, what? How do all those demos work?</h3>


<p>While we’re on the topic of doing the impossible, it has been said — by Philip Walton at Google, who tried really hard in the past to make production-ready CSS polyfills — that <a href="https://philipwalton.com/articles/the-dark-side-of-polyfilling-css/" rel="noopener">it is not possible to write a reliable polyfill for CSS</a>. He gave up the idea, but I like to imagine his nickname at Google became “Polyphil,” so it wasn’t a total loss.</p>



<p>Philip also created this <a href="https://philipwalton.github.io/polyfill/" rel="noopener">abandoned framework for creating CSS polyfills</a>, which still works, although it’s so old that the examples show how to polyfill <a href="https://css-tricks.com/snippets/css/a-guide-to-flexbox/">flexbox</a>. In the decade since he stopped supporting this library, it doesn’t seem like the feasibility of perfect CSS polyfills has improved.</p>



<p>However, Philip’s findings haven’t stopped <a href="https://github.com/flackr/scroll-timeline" rel="noopener">cool CSS polyfills</a> from <a href="https://github.com/GoogleChromeLabs/container-query-polyfill" rel="noopener">existing</a>. They can be useful, even if they can’t be perfect. <a href="https://medium.com/detour-ux/perfect-is-the-enemy-of-the-good-why-perfectionism-is-killing-your-teamwork-cbfa62808263" rel="noopener">Perfect is the enemy of good.</a></p>


<h3 class="wp-block-heading" id="why-we-re-not-going-to-give-up-on-nth-letter-">Why we’re not going to give up on <code>::nth-letter</code></h3>


<p>To maintain our motivation for simulating <code>::nth-letter</code>, I note that the lack of a spec might make implementing it easier than writing a true polyfill. Anything we create in this space will technically be a <a href="https://medium.com/@aryanvania03/what-is-the-difference-between-a-shim-and-a-polyfill-551bb0011b1e" rel="noopener">shim rather than a polyfill</a>. All polyfills are shims, but not all shims are polyfills — like all cows are animals, but not the other way around.</p>



<p>We’re patching CSS to add functionality that never existed, whereas a polyfill simulates a feature that exists in certain environments, and/or at least has a formal spec. The closest we got to a draft spec was <a href="https://www.bram.us/2012/04/13/css-nth-letter/" rel="noopener">experimental work Adobe attempted in WebKit back in 2012</a>, which <a href="https://css-tricks.com/did-we-get-anywhere-on-that-nth-letter-thing/">never got anywhere</a>.</p>



<p>Having explained that, I will use the terms polyfill and shim interchangeably here, because polyfill is the more well-known term, and because I am anyhow about to play fast and loose with what words mean.</p>


<h3 class="wp-block-heading" id="defining-our-terms">Defining our terms</h3>


<p>Since nobody knows how <code>::nth-letter</code> would behave, I can make up my own answers to <a href="https://adactio.medium.com/an-nth-letter-selector-in-css-6f957e5b18b0" rel="noopener">questions like those Jeremy Keith raised about</a> how it would even work.</p>



<p>As Humpty Dumpty said, <a href="https://xkcd.com/1860/" rel="noopener">the words will mean what I want them to mean</a>.</p>


<h4 class="wp-block-heading" id="1-what-does-nth-mean-">1. What does &#8220;nth” mean?</h4>


<p>Jeremy wondered what the third letter in a paragraph would be. Take this example markup:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;p>AB&lt;span>CD&lt;/span>EF&lt;/p></code></pre>



<p>The third letter could be:</p>



<ul class="wp-block-list">
<li>“C” because that’s the third letter as it would appear when you read from left to right, regardless of the DOM structure. After all, <code>p::first-letter</code> would select “A,” even if that character was deeply nested in markup within the paragraph.</li>



<li>“E” because <a href="https://css-tricks.com/examples/nth-child-tester/">that’s what</a> <a href="https://css-tricks.com/examples/nth-child-tester/"><code>:nth-child</code> would do</a>. E is the third direct child of the paragraph element.</li>



<li>“D” or “B” if we styled the paragraph to use a <a href="https://stackoverflow.com/a/73013183" rel="noopener">right-to-left writing direction</a>. In a more probable scenario, if the paragraph above were changed to <code>&lt;p&gt;אב&lt;span&gt;קד&lt;/span&gt;פע&lt;/p&gt;</code> Hebrew characters are inherently right-to-left in Unicode — and then the answer would be different again.</li>
</ul>



<p>The answer, in the universe I created for this article, is that <code>::nth-letter</code> will behave the same as <code>:nth-child</code>, which <a href="https://css-tricks.com/almanac/pseudo-selectors/n/nth-child/">depends on the source order of the direct child of the element</a>.</p>



<p>Isn’t life simpler when the rigorous <a href="https://www.w3.org/policies/process/" rel="noopener">drafting process of the W3C</a> is replaced with the whims of a lone crackpot?</p>


<h4 class="wp-block-heading" id="2-what-does-letter-mean-">2. What does “letter” mean?</h4>


<p>We touched on how other languages would affect <code>::nth-letter</code>. Only <a href="https://en.wikipedia.org/wiki/Languages_used_on_the_Internet#Usage_statistics_of_content_languages_for_websites" rel="noopener">half of the web uses English</a>. If we are simulating a browser feature, we can’t ignore other languages, can we?</p>



<p>Not only are writing directions different in languages other than English, but <a href="https://en.wikipedia.org/wiki/Multigraph_(orthography" rel="noopener">some languages use multiple characters to represent a single letter</a>. Now, in theory, <code>::first-letter</code> selects all parts of such a letter. But the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::first-letter#browser_compatibility" rel="noopener">browser support for that is poor</a>. <code>::first-letter</code> has some other interesting edge cases I wouldn’t have expected, such as selecting punctuation together with the first letter, maybe because that’s how drop caps are normally presented.</p>



<p>At this point, I decide that any answer I give would disappoint some people if their idea of a letter isn’t what&#8217;s selected by <code>::nth-letter</code>. To circumvent this debate, let’s say <code>::nth-letter</code> is an alias for the nth <em>character</em>.</p>



<p>A bit extreme, but the examples I showed above of how people imagine <code>::nth-letter</code> don’t seem to focus on whether each character is a letter. And I think my 8-year-old would have been disappointed if the exclamation point he added to his rainbow text wasn’t colored.</p>



<p>Look, if you don’t like it, go back to your own universe where there’s no <code>::nth-letter</code> at all. Or you can tinker with the source code I will show you next.</p>


<h3 class="wp-block-heading" id="how-to-write-an-impossible-polyfill">How to write an impossible polyfill</h3>


<p>I published this <a href="https://www.npmjs.com/package/@leemeyer/nth-letter" rel="noopener">experimental library</a> on npm. That’s what the above CodePen uses via <a href="https://unpkg.com/@leemeyer/nth-letter@latest" rel="noopener">unpckg</a>. The <code>::nth-letter</code> package received 1.3k downloads in its first week without me advertising it, so that was nice.</p>



<p>Instead of trying to build a perfect polyfill, there’s a certain freedom in knowing we can’t. We’ll therefore <a href="https://ronjeffries.com/xprog/articles/practices/pracsimplest/" rel="noopener">do the simplest thing that could possibly work</a>. We rewrite the CSS and transform the DOM so the browser can do the rest. Here’s a simplified version that is 29 lines of JavaScript and works in today’s browsers. As we explore how it works, you’ll see that the brevity is achieved by leveraging what CSS can already do with minimal tampering.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">import getCssData from 'get-css-data';
import { SplitText } from 'gsap/SplitText';

getCssData({
  onComplete(cssText, cssArray, nodeArray) {
    nodeArray.forEach(e => e.remove());
    const selectors = new Set();
    const nthArgs = new Set();
    cssText = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
    // Replace ::nth-letter with :nth-child in CSS
    let rewrittenCss = cssText.replace(
      /([^,{{\r\n]+?)::?nth-letter[ \t]*\(([^\n)]*)\)/gi,
      (full, selector, args) => {
        selector = selector.trim();
        selectors.add(selector);
        nthArgs.add(args);
        // Use :nth-child instead of ::nth-letter
        return `${selector} .char:nth-child(${args})`;
      }
    );
    document.head.insertAdjacentHTML("beforeend", `&lt;style>${rewrittenCss}&lt;/style>`);
    selectors.forEach(selector => {
      document.querySelectorAll(selector).forEach(el => {
        if (el.hasAttribute('data-nth-letter')) return;
        el.setAttribute('data-nth-letter', 'attached');
        new SplitText(el, { type: 'chars', charsClass: 'char' });
      });
    });
  }
});</code></pre>



<p>A lot is going on in this small block of code, so let’s break down the phases.</p>


<h4 class="wp-block-heading" id="translating-nth-letter-into-valid-css">Translating <code>::nth-letter</code> into valid CSS</h4>


<p>Even at this first phase, we get a sense that introducing custom CSS syntax won’t be as easy as we might hope. It’s less conveniently obvious how to do it than <a href="https://en.wikipedia.org/wiki/Monkey_patch" rel="noopener">monkey patching</a> JavaScript, although <a href="https://kettanaito.com/blog/why-patching-globals-is-harmful" rel="noopener">the risks</a> are comparable to patching globals in JavaScript.</p>



<p>The way CSS is applied to a web page doesn’t provide a good opportunity to intercept standard CSS behaviors and customize them.</p>



<p>Indeed, even making the nonstandard <code>::nth-letter</code> syntax available to our JavaScript code is <a href="https://philipwalton.com/articles/the-dark-side-of-polyfilling-css/" rel="noopener">tricky</a>, because the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Syntax/Error_handling" rel="noopener">CSS parser will discard invalid CSS,</a> so if the user includes the selector <code>.rainbow::nth-letter(2n)</code>, that won’t be available to JavaScript when it accesses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/styleSheets" rel="noopener"><code>stylesheets</code> property of the DOM</a>.</p>



<p>We need to gather all raw CSS free from judgment of validity, so let’s use <a href="https://www.npmjs.com/package/get-css-data" rel="noopener"><code>get-css-data</code></a>, which concatenates the raw contents of any <code>style</code> tags in the DOM and uses <a href="https://www.javascripttutorial.net/web-apis/javascript-fetch-api/" rel="noopener"><code>fetch</code></a> to include the contents of each stylesheet imported via <code>link</code> tags.</p>



<p class="is-style-explanation"><strong>Sidenote:</strong> <code>get-css-data</code> won’t work if the CORS policy doesn’t allow it, but that is one of the inherent limitations of CSS polyfills.</p>



<p>Next, we rewrite the nonstandard CSS using regular expressions, which <a href="https://softwareengineering.stackexchange.com/questions/113237/when-you-should-not-use-regular-expressions" rel="noopener">is a bit ghetto</a>. A more rigorous approach would use something like <a href="https://postcss.org/" rel="noopener">PostCSS</a> at build time. But, we can get away with regex in this case, because we’re not doing our own parsing of CSS; we’re doing a relatively simple find-replace, which regex is good at.</p>



<p>The result of the replacement will translate the invalid CSS…</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.rainbow::nth-letter(n) {
  color: #f432a0;
}</code></pre>



<p>&#8230;into this valid CSS:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.rainbow .char:nth-child(n) {
  color: #f432a0;
}</code></pre>



<p>This <a href="https://www.youtube.com/watch?v=ZskP7cvj3WA" rel="noopener">great video</a> concludes that the least bad option for implementing a CSS polyfill is to “rewrite the CSS to target individual elements while maintaining cascade order.” Philip adds that he has “never seen a polyfill do this. I don’t recommend it, but I think it’s the best of the bad options.” Better late than never to create a polyfill using this strategy.</p>


<h4 class="wp-block-heading" id="implementing-the-translator-for-nth-letter-">Implementing the translator for <code>::nth-letter</code></h4>


<p>The shim removes the original styles from the page and replaces them with the rewritten styles, like so:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">getCssData({
  onComplete(cssText, cssArray, nodeArray) {
    nodeArray.forEach(e => e.remove());
    const selectors = new Set();
    const nthArgs = new Set();
    cssText = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
    // Replace ::nth-letter with :nth-child in CSS
    let rewrittenCss = cssText.replace(
      /([^,{{\r\n]+?)::?nth-letter[ \t]*\(([^\n)]*)\)/gi,
      (full, selector, args) => {
        selector = selector.trim();
        selectors.add(selector);
        nthArgs.add(args);
        // Use :nth-child instead of ::nth-letter
        return `${selector} .char:nth-child(${args})`;
      }
    );

    document.head.insertAdjacentHTML("beforeend", `&lt;style>${rewrittenCss}&lt;/style>`);
  }
});</code></pre>



<p>At this point, we have translated the unsupported <code>::nth-letter</code> syntax into valid CSS. But it still needs some DOM elements to style, or it won’t do anything.</p>


<h4 class="wp-block-heading" id="preparing-the-dom">Preparing the DOM</h4>


<p>Since <code>::nth-letter</code> doesn’t exist, my implementation is ultimately a convenient abstraction for what I did manually in my <a href="https://css-tricks.com/spiral-scrollytelling-in-css-with-sibling-index/">spiral scrollytelling article</a>. So, after gathering all the elements that require styling of individual characters, we split the targeted content into <code>div</code> tags, using the freely available <a href="https://gsap.com/docs/v3/Plugins/SplitText/" rel="noopener">SplitText plugin from GSAP</a>.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">selectors.forEach(selector => {
  document.querySelectorAll(selector).forEach(el => {
    if (el.hasAttribute('data-nth-letter')) return;
    el.setAttribute('data-nth-letter', 'attached');
    new SplitText(el, { type: 'chars', charsClass: 'char' });
  });
}</code></pre>



<p>It works! The auto-magically generated CSS receives an auto-magically generated DOM to style. We all live happily ever after. Article over for real this time.</p>



<p>Or is it?</p>


<h3 class="wp-block-heading" id="do-we-have-to-modify-the-dom-for-this-">Do we have to modify the DOM for this?</h3>


<p>As mentioned in a 2021 CSS-Tricks newsletter <a href="https://css-tricks.com/newsletter/253-25-years-of-css-css-font-descriptors-and-nth-letter-woes/">that lamented <code>::nth-letter</code> being “sadly still not a thing,”</a> the solution of spitting the text into separate elements per character is “pretty gross, right? It’s a shame that we have to mess up the markup to make a relatively simple aesthetic change.”</p>



<p>The same post spoke of a potential accessibility issue if you split characters into their own elements: &#8220;screen readers (some, anyway?) read each of those characters with pauses in between.&#8221; Research shows that VoiceOver <a href="https://lab.dotjay.com/tests/screen-readers/voiceover-text-breaks-workarounds/" rel="noopener">can cause this issue</a>, although it’s reported that the <a href="https://lab.dotjay.com/tests/screen-readers/voiceover-text-breaks-workarounds/#role-text-approach-2" rel="noopener"><code>role</code> attribute can now alleviate it</a>. The <a href="https://gsap.com/docs/v3/Plugins/SplitText/#screen-reader-accessibility" rel="noopener">SplitText plugin I use also automatically accounts for accessibility</a>, but it <a href="https://adrianroselli.com/2026/02/you-know-what-just-dont-split-words-into-letters.html#Videos" rel="noopener">may not work on all screenreaders</a>, and sadly, accessibility for split text is harder to get right than you’d think.</p>



<p>Also, if <code>::nth-letter</code> were a native feature, it would be a <a href="https://www.w3schools.com/css/css_pseudo_elements.asp" rel="noopener">pseudo-element</a>. It would be great if we could simulate that, knowing there is a risk we will trip over those extra elements that my library adds to the DOM.</p>



<p>A pseudo-element could give us the best of both worlds for solving the task at hand: something that is purely presentational and doesn’t pollute the DOM, but can still behave like part of the DOM for styling purposes only. Can we implement something similar to avoid polluting our DOM?</p>



<p>Yes and no.</p>



<p>The harsh truth is we may never be able to implement our own custom pseudo-elements.</p>



<p>Earlier, I expressed the hope that the CSS Parser API would someday help, but even in the unlikely event that this API materializes, the intent wouldn’t be to allow developers to implement their own CSS syntax or pseudo-elements. As you can see from this <a href="https://wicg.github.io/css-parser-api/" rel="noopener">2021 unofficial draft</a>, if we ever get this API, it would likely expose the browser’s CSS parser for programmatic use — but it probably wouldn’t help us customize how CSS is interpreted. Custom pseudo-elements would be the domain of a hypothetical CSS Renderer API, which is something my brain just came up with that nobody has even proposed.</p>



<p>Bramus from the Chrome team has a draft document outlining how a <a href="https://github.com/bramus/css-parser-extensions" target="_blank" rel="noreferrer noopener">CSS parser extensions API</a> would work, and this is closer to what I imagined the hypothetical CSS parser API might provide, but Bramus&#8217;s document doesn&#8217;t currently discuss custom psuedo-elements. There is also the <a href="https://github.com/WICG/html-in-canvas" target="_blank" rel="noreferrer noopener">HTML-in-canvas API</a> proposal which would let us customize the way elements are rendered without modifying their DOM. That&#8217;s <a href="https://frontendmasters.com/blog/the-web-is-fun-again-first-experiments-with-html-in-canvas/" target="_blank" rel="noreferrer noopener">already experimentally available in Chrome</a>, but still wouldn&#8217;t give us custom psuedo-elements we could arbitrarily style using CSS.</p>


<h3 class="wp-block-heading" id="shadow-dom-version-of-nth-letter-">Shadow DOM version of <code>::nth-letter</code></h3>


<p>If we’re stuck with manipulating the DOM, the closest we can get to custom pseudo-elements is to hide the character elements in the <a href="https://css-tricks.com/encapsulating-style-and-structure-with-shadow-dom/">shadow DOM</a> of the targeted elements, while exposing an API that lets us style selected characters from outside the target.</p>



<p>If we are determined that targeted elements of this new selector won’t pollute the <a href="https://frontendmasters.com/blog/light-dom-only/" rel="noopener">light DOM</a> with extra markup, then we have to hide that markup in the shadow DOM. If we do that, then the closest I know of to a custom pseudo-element is the <a href="https://frontendmasters.com/blog/light-dom-only/" rel="noopener"><code>::part</code></a> pseudo-element. If we use that, then by design, we can’t use:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.container::part(character):nth-child(2) {
  color: red;
}</code></pre>



<p>The reason is that the shadow DOM of my element would look like:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;div part="character">1&lt;/div>
&lt;div part="character">2&lt;/div></code></pre>



<p>A consumer of my component shouldn’t be able to know the structure of the shadow DOM from outside the component using CSS. That’s why “<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Pseudo-classes#tree-structural_pseudo-classes" rel="noopener">structural pseudo-classes</a> that match based on tree information, such as <code>:empty</code> and <code>:last-child</code>, cannot be appended“ to <code>::part</code>. Once upon a time, there was a <code>::shadow</code> pseudo-element that would have let us style <code>:nth-child</code> from outside the shadow DOM, but it was <a href="https://developer.chrome.com/blog/remove-shadow-piercing" rel="noopener">deprecated</a> a lifetime ago.</p>



<p>Actually, there is a way to still use <code>:nth-child</code> together with <code>::part</code> if you think laterally.</p>



<p>What if we populate each character’s <code>::part</code> attribute based on the <code>:nth-child</code> selectors we know we will need to support? We know what those are, since we created them when we were regex replacing the styles!</p>



<p>Then we’d have:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.rainbow::part(nth-child\(n\)) {
  color: #f432a0;
}</code></pre>



<p>And the HTML in our shadow DOM would look something like:</p>



<pre rel="HTML" class="wp-block-csstricks-code-block language-markup" data-line=""><code markup="tt">&lt;h1 class="rainbow" data-nth-letter="attached">Rainbow&lt;/h1>
Rainbow
#ShadowRoot
&lt;span aria-hidden="true" aria-label="Rainbow">
  &lt;div class="char" aria-hidden="true" part="nth-child(n) nth-child(odd)">R&lt;/div>
  &lt;!-- etc. -->
&lt;/span></code></pre>



<p>We can generate such a shadow DOM using the following slightly more complex version of the JavaScript:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">import getCssData from 'get-css-data';
import { SplitText } from 'gsap/SplitText';
getCssData({
  onComplete(cssText, cssArray, nodeArray) {
    nodeArray.forEach(e => e.remove());
    const selectors = new Set();
    const nthArgs = new Set();

    // Remove CSS comments
    cssText = cssText.replace(/\/\*[\s\S]*?\*\//g, '');

    let rewrittenCss = cssText.replace(
      /([^,{\r\n]+?)::?nth-letter[ \t]*\(([^\n)]*)\)/gi,
      (full, selector, args) => {
        selector = selector.trim();
        selectors.add(selector);
        nthArgs.add(args);
        return `${selector}::part(nth-child\\(${CSS.escape(args)}\\))`;
      }
    );

    document.head.insertAdjacentHTML("beforeend", `&lt;style>${rewrittenCss}&lt;/style>`);

    selectors.forEach(selector => {
      document.querySelectorAll(selector).forEach(el => {
        if (el.shadowRoot || el.hasAttribute('data-nth-letter')) return;

        const shadow = el.attachShadow({ mode: "closed" });
        el.setAttribute('data-nth-letter', 'attached');
        const wrapper = document.createElement("span");
        wrapper.setAttribute('aria-hidden', 'true');
        wrapper.innerHTML = el.innerHTML;
        shadow.appendChild(wrapper);
        const split = new SplitText(wrapper, { type: "chars", charsClass: "char" });

        nthArgs.forEach((arg, i) => {
          let chars = wrapper.querySelectorAll(`.char:nth-child(${arg})`);
          chars.forEach(c => {
            const prev = c.part || "";
            c.part = (prev ? prev + " " : "") + `nth-child(${arg})`;
          });
        });
      });
    });
  }
});</code></pre>



<p>By pre-calculating the <code>:nth-child</code> selectors as names of the shadow parts which match the <code>::nth-letter</code> usages our CSS has requested, we can select them from outside, without touching the light DOM, and without hitting a brick wall of the intentional limitations of shadow DOM.</p>



<p>It works! Are we there yet? Is the best answer to use shadow DOM?</p>



<p>Not really, it causes at least two big issues:</p>



<ol class="wp-block-list">
<li>This version <a href="https://medium.com/dev-channel/which-elements-support-shadow-dom-d58f5a447197" rel="noopener">won’t work on elements that don’t support attaching a shadow DOM</a>, such as <code>&lt;a&gt;</code> or <code>&lt;p&gt;</code>.</li>



<li>We can’t use the emergent <code>sibling-index()</code> function in the styles for a shadow part, because <code>sibling-index()</code> relies on knowing the structure of the DOM, just like <code>:nth-child</code> does. This prevents supporting the text styling demos I showed at the start. These demos would not work with the shadow DOM version of <code>::nth-letter</code>.</li>
</ol>



<p>I notice that <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::first-letter#allowable_properties" rel="noopener"><code>::first-letter</code> is also seriously limited in the styling it supports</a>. That’s not enough reason to knowingly cripple our implementation of <code>::nth-letter</code> when there’s an option not to. I conclude the light DOM version is better. It might be “gross” markup, but at least we are no longer the ones who need to write or maintain it. And if browsers ever support <code>::nth-letter</code> natively, the design of the shim is intended so we‘d keep the CSS as-is, delete the reference to my library, and never speak of it again.</p>


<h3 class="wp-block-heading" id="the-actual-ending">The (actual) ending</h3>


<p>Now that we have a simple basis for implementing things like <code>::nth-letter</code>, it would be feasible to add <code>::nth-word</code>, <code>::nth-last-letter</code>, and so on. Chris Coyier showed cool use cases for those in [his call for <a href="https://css-tricks.com/a-call-for-nth-everything/"><code>::nth</code> everything</a>.</p>



<p>There are still many limitations to the <code>::nth-letter</code> shim, such as:</p>



<ol class="wp-block-list">
<li>It doesn’t work if you change the DOM or the styles on the fly, although we <a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver" rel="noopener">probably could</a> support that.</li>



<li>It doesn’t work if you use <code>::nth-letter</code> in a CSS selector passed to <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll" rel="noopener"><code>querySelectorAll</code></a>, although we could monkey-patch JavaScript to make that work.</li>



<li>I am unsure how scalable it is.</li>



<li>It could lead to hard-to-diagnose bugs because it rewrites all the CSS and adds unexpected “char” divs to the DOM. I noticed that Philip Schatz’s <a href="https://github.com/philschatz/css-polyfills.js" rel="noopener">polyfill</a> for a <a href="https://www.w3.org/TR/css-content-3/" rel="noopener">crazy working draft</a> called the “CSS Generated Content Module” requires the consumer to opt-in by using special attributes on the <code>link</code> or <code>style</code> tags. That’s an interesting compromise that might limit the blast radius by only triggering the CSS rewrites where we need them, but it seems less convenient than just referencing the library and then using the new syntax.</li>



<li>External stylesheets not allowed by CORS won’t work.</li>
</ol>



<p>In summary, I’d probably use <code>::nth-letter</code> and its hypothetical friends all the time if these features were built into browsers. But I must admit that, having explored the complexity of building generic support for a design we can often adequately solve with a few lines of JavaScript, I see why the browsers are reluctant to implement and maintain such a feature.</p>



<p>My shim might give the powers that be another reason to say native support isn&#8217;t necessary, or if lots of people use my <code>::nth-letter</code> hack in the wild, the browser gods might recognize the need to implement it for real.</p>



<p>Either way, let’s never argue again, CSS. I understand now why you did what you did. I could never stay mad at you.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/using-nonexistent-nth-letter-selector-now/">Let’s Use the Nonexistent ::nth-letter Selector Now</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/using-nonexistent-nth-letter-selector-now/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">393324</post-id>	</item>
		<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/#comments</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>1</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" loading="lazy" 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="auto, (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" loading="lazy" 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="auto, (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" loading="lazy" 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="auto, (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>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392882</post-id>	</item>
		<item>
		<title>hypot()</title>
		<link>https://css-tricks.com/almanac/functions/h/hypot/</link>
		
		<dc:creator><![CDATA[Juan Diego Rodríguez]]></dc:creator>
		<pubDate>Wed, 15 Apr 2026 16:30:48 +0000</pubDate>
				<category><![CDATA[math]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=392723</guid>

					<description><![CDATA[<p>The <code>hypot()</code> function takes a list of values and returns the square root of the sum of their squares.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/h/hypot/">hypot()</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>The&nbsp;<code>hypot()</code>&nbsp;function takes a list of values and returns the square root of the sum of their squares.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.hypotenuse {
  width: hypot(30vmin, 40vmin); /* 50vmin */
}</code></pre>



<p>Most of the time, we&#8217;ll pass it two arguments:&nbsp;<code>hypot(A, B)</code>. Think of it like a way to complete the <a href="https://en.wikipedia.org/wiki/Pythagorean_theorem" rel="noopener">Pythagorean theorem</a> where we give the function the opposite and adjacent sides of a triangle, and the longest side is what it returns.</p>



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



<p>The&nbsp;<code>hypot()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-values/#funcdef-hypot" rel="noopener">CSS Values and Units Module Level 4</a>&nbsp;specification</p>


<h3 class="wp-block-heading" id="syntax">Syntax</h3>


<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">hypot(&lt;calc-sum>#)</code></pre>



<p>The&nbsp;<code>hypot()</code>&nbsp;functions takes a comma-separated list of calculations (<code>&lt;calc-sum&gt;</code>) that must resolve to a&nbsp;<code>&lt;number&gt;</code>,&nbsp;<code>&lt;dimension&gt;</code>, or&nbsp;<code>&lt;percentage&gt;</code>.</p>


<h3 class="wp-block-heading" id="arguments">Arguments</h3>


<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Takes dimensions */
width: hypot(30px, 40px); /* 50px */
width: hypot(12rem, 160px)

/* Takes percentages  */
width: hypot(60%, 80%);
width: hypot(30px, 10%)

/* Takes numbers */
hypot(9, 12) /* 15 */
hypot(15, 20) /* 25 */

/* Takes negative values */
width: hypot(-50px, 120px); /* 130px */
width: hypot(-90%, -120%); /* 150% */
width: hypot(9, 40); /* 41 */</code></pre>



<p><strong><code>&lt;calc-sum&gt;</code></strong> is a comma-separated list of calculations that resolve to either a&nbsp;<code>&lt;number&gt;</code>,&nbsp;<code>&lt;dimension&gt;</code>, or&nbsp;<code>&lt;percentage&gt;</code>.</p>



<p>Mixing&nbsp;<code>&lt;dimension&gt;</code>&nbsp;and&nbsp;<code>&lt;percentage&gt;</code>&nbsp;values is fine as long as they have a consistent type — like&nbsp;<code>25%</code>&nbsp;and&nbsp;<code>5rem</code>&nbsp;in the&nbsp;<code>width</code>&nbsp;property, or else the function is invalid.</p>



<p>Lastly, the result of&nbsp;<code>hypot()</code>&nbsp;will be of the same type as its arguments, so&nbsp;<code>hypot(&lt;number&gt;, &lt;number&gt;)</code>&nbsp;returns a&nbsp;<code>&lt;number&gt;</code>, and&nbsp;<code>hypot(&lt;dimension&gt;, &lt;dimension&gt;)</code>&nbsp;returns a&nbsp;<code>&lt;dimension&gt;</code>.</p>


<h3 class="wp-block-heading" id="three-ways-to-see-it">Three ways to see it</h3>


<p>There are three main ways we can think of&nbsp;<code>hypot()</code>:</p>



<p>1. While boring, we can think of&nbsp;<code>hypot()</code>&nbsp;as just its underlying formula, which squares each argument, sums it, and takes the square root of the result. You know, just like you learned in school: &#8220;A squared, plus B squared equals C squared.</p>



<p>This explains why it can take negative values, since they get squared into a positive value.</p>



<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mi>hypot</mi><mrow><mo>(</mo><msub><mi>a</mi><mn>1</mn></msub><mo>,</mo><msub><mi>a</mi><mn>2</mn></msub><mo>,</mo><mo>&#8230;</mo><mo>,</mo><msub><mi>a</mi><mi>n</mi></msub><mo>)</mo></mrow><mo>=</mo><msqrt><mrow><msubsup><mi>a</mi><mn>1</mn><mn>2</mn></msubsup><mo>+</mo><msubsup><mi>a</mi><mn>2</mn><mn>2</mn></msubsup><mo>+</mo><mo>&#8230;</mo><mo>+</mo><msubsup><mi>a</mi><mi>n</mi><mn>2</mn></msubsup></mrow></msqrt></mrow></math></p>



<p>2. However,&nbsp;<code>hypot()</code>&nbsp;has a whole lot more interpretation beyond a formula. If given two values,&nbsp;<code>hypot(A, B)</code>&nbsp;returns the length of the hypotenuse of a right triangle with sides A and B.</p>



<p>Instead of the sides of a triangle, we can also think of both arguments as the coordinates of a point on an XY plane. In which case,&nbsp;<code>hypot(x, y)</code>&nbsp;returns the length from the origin to that point in space.</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1920" height="1080" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/hypot_right_triangle_mcmbdv.png?resize=1920%2C1080&#038;ssl=1" alt="A right triangle labeled A, B, and hypotenuse on a grid with the formula: `hypot(A, B) = hypotenuse`" class="wp-image-393533" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/hypot_right_triangle_mcmbdv.png?w=1920&amp;ssl=1 1920w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/hypot_right_triangle_mcmbdv.png?resize=300%2C169&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/hypot_right_triangle_mcmbdv.png?resize=1024%2C576&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/hypot_right_triangle_mcmbdv.png?resize=768%2C432&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/04/hypot_right_triangle_mcmbdv.png?resize=1536%2C864&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>3. That said,&nbsp;<code>hypot()</code>&nbsp;can take more than two arguments, so we&#8217;ll extend the last definition beyond two dimensions where something like <code>hypot(a<sub>1</sub>, a<sub>2</sub>, ..., a<sub>n</sub>)</code> is the length of a point to its origin in an n-dimensional space. For example, seen in three dimensions,&nbsp;<code>hypot(x, y, z)</code>&nbsp;would look like this:</p>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1920" height="1080" src="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/hypot_length_3d_qmqbud-1.png?resize=1920%2C1080" alt="A 3D coordinate system showing a vector with components x, y, and z, labeled hypot(x, y, z) = length" class="wp-image-392858" srcset="https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/hypot_length_3d_qmqbud-1.png?w=1920&amp;ssl=1 1920w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/hypot_length_3d_qmqbud-1.png?resize=300%2C169&amp;ssl=1 300w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/hypot_length_3d_qmqbud-1.png?resize=1024%2C576&amp;ssl=1 1024w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/hypot_length_3d_qmqbud-1.png?resize=768%2C432&amp;ssl=1 768w, https://i0.wp.com/css-tricks.com/wp-content/uploads/2026/03/hypot_length_3d_qmqbud-1.png?resize=1536%2C864&amp;ssl=1 1536w" sizes="auto, (min-width: 735px) 864px, 96vw" /></figure>



<p>We&#8217;ll look at basic usage with two values to help illustrate how we&#8217;d put this into practice. Plus, CSS isn&#8217;t too keen on higher dimensions beyond 2D.</p>


<h3 class="wp-block-heading" id="basic-usage">Basic Usage</h3>


<p>Let&#8217;s start with a not-so-practical example for&nbsp;<code>hypot()</code>, just to showcase how it works. Imagine we want to cast a line from one point on the screen to our mouse position in the viewport.</p>



<p>The first step is to let CSS know the mouse&#8217;s x and y coordinates through the next JavaScript snippet, which saves each coordinate in the&nbsp;<code>--m-x</code>&nbsp;and&nbsp;<code>--m-y</code>&nbsp;variables:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">window.addEventListener("pointermove", (event) => {
  let x = event.clientX;
  let y = event.clientY;

  document.documentElement.style.setProperty("--m-x", `${Math.round(x)}px`);
  document.documentElement.style.setProperty("--m-y", `${Math.round(y)}px`);
});</code></pre>



<p>From here, we need to answer two questions: (1) <em>How long should that line be? </em> (2) <em>what angle should we rotate it?</em></p>



<p>The first question is where&nbsp;<code>hypot()</code>&nbsp;comes in, since the length of that line is the same as&nbsp;<code>hypot(x, y)</code>.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.line {
  /* Places line at the top left corner */
  position: fixed;
  top: 0;
  left: 0;

  width: hypot(var(--m-x), var(--m-y));
  height: 5px; /* Thickness of the line */
}</code></pre>



<p>With this, we have a line that gets longer as the mouse moves away from the top-left corner.</p>



<p>Now, we have to rotate it by a certain angle, so we&#8217;ll use the&nbsp;<a href="https://css-tricks.com/almanac/functions/a/atan2/"><code>atan2()</code></a>&nbsp;function. Without going too much into details,&nbsp;<code>atan2(y, x)</code>&nbsp;gives us the angle between a point in the plane and the horizontal axis.</p>



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

  transform-origin: left center;
  transform: rotate(atan2(var(--m-y), var(--m-x)));
}</code></pre>



<p class="is-style-explanation">Note that we pass first&nbsp;<code>y</code>&nbsp;and then&nbsp;<code>x</code>&nbsp;into&nbsp;<code>atan2()</code> rather than the other way around.</p>



<p>And now the line should perfectly connect to our mouse.</p>



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



<p>If we want to instead connect the line from the center to the mouse, we would have to first place our line at the center:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.line {
  position: fixed;
  top: 50%;
  left: 50%;

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



<p>Then we tweak our JavaScript a bit so that our coordinates also start from the center instead of the top-left corner. This can be done by subtracting half the window&#8217;s&nbsp;<code>innerWidth</code>&nbsp;and&nbsp;<code>innerHeight</code>&nbsp;from the respective coordinate before passing them onto CSS.</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">x = x - window.innerWidth / 2;
y = y - window.innerHeight / 2;</code></pre>



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


<h3 class="wp-block-heading" id="detecting-when-the-cursor-is-near">Detecting When the Cursor is Near</h3>


<p>So, now that we know the basics of how&nbsp;<code>hypot()</code>&nbsp;works, we can use it for a more practical situation. Not so long ago,&nbsp;<a href="https://css-tricks.com/potentially-coming-to-a-browser-near-you/">Daniel Schwarz wrote about&nbsp;<code>:near()</code></a>, a potential new pseudo-selector that would match an element when the cursor is close to it by a given distance:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">button:near(3rem) {
  /* Pointer is within 3rem of the button */
}</code></pre>



<p>This could be used to apply some effect to a button whenever the user is near it, like highlighting or scaling it a bit. Unfortunately, at the time of writing, we don&#8217;t have&nbsp;<code>:near()</code>&nbsp;on any browser, but we can already simulate it using both&nbsp;<code>hypot()</code>&nbsp;and&nbsp;<a href="https://css-tricks.com/css-container-queries/#aa-container-style-queries">style queries</a>.</p>



<p>Again, our first step is to pass the mouse coordinates to CSS via JavaScript. However, this time we&#8217;ll measure the mouse position from the center of the button. To do this, we&#8217;ll:</p>



<ol class="wp-block-list">
<li><strong>Subtract from each coordinate the button&#8217;s&nbsp;<code>offsetLeft</code>&nbsp;and&nbsp;<code>offsetTop</code></strong>, which moves the origin to the button&#8217;s top-left corner.</li>



<li><strong>Subtract half the button&#8217;s&nbsp;<code>clientWidth</code>&nbsp;and&nbsp;<code>clientHeight</code></strong>, which moves the origin right to the button&#8217;s center.</li>
</ol>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const button = document.querySelector("button");

window.addEventListener("pointermove", (event) => {
  let x = event.clientX;
  let y = event.clientY;

  x = x - button.offsetLeft - button.clientWidth / 2;
  y = y - button.offsetTop - button.clientHeight / 2;

  document.documentElement.style.setProperty("--m-x", `${Math.round(x)}px`);
  document.documentElement.style.setProperty("--m-y", `${Math.round(y)}px`);
});</code></pre>



<p>Back to CSS! Let&#8217;s define two variables: (1)&nbsp;<code><strong>--near</strong></code>&nbsp;that holds the distance from the mouse to the button, and (2)&nbsp;<code><strong>--limit</strong></code>&nbsp;that defines the limit where we consider the mouse&nbsp;<em>near</em>&nbsp;the button.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">:root {
  --near: hypot(var(--m-x), var(--m-y));
  --limit: 200px;
}</code></pre>



<p>What&#8217;s left is to create a style query that matches whenever&nbsp;<code>--near</code>&nbsp;is smaller than&nbsp;<code>--limit</code>, meaning our cursor is within the limit.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">@container style(--near &lt; var(--limit)) {
  button {
    scale: 1.025;
    box-shadow: #e07a5f 0px 6px 25px 0px;
  }
}</code></pre>



<p>Before we look at the result, it&#8217;s worth checking ion your browser supports container style queries.</p>




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="container-style-queries"></baseline-status>



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


<h3 class="wp-block-heading" id="more-than-syntactical-sugar">It&#8217;s More Than Syntactical Sugar</h3>


<p>Since&nbsp;<code>hypot()</code>&nbsp;returns the square root of a sum of squares, the biggest math nerds of us would expect that the following two expressions would be equivalent:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">hypot(A, B)</code></pre>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">sqrt(pow(A, 2) + pow(B, 2))</code></pre>



<p>However, if we try to switch the former for the latter, we&#8217;ll notice that it won&#8217;t work most times. The two formulas aren&#8217;t interchangeable whenever&nbsp;<code>hypot()</code>&nbsp;takes either a&nbsp;<code>&lt;length&gt;</code>&nbsp;or&nbsp;<code>&lt;percentage&gt;</code>, since all other exponential functions only accept&nbsp;<code>&lt;number&gt;</code>&nbsp;values. In other words, <strong>we need <code>hypot()</code> if we are working with specific types of values</strong>, notably <code>&lt;length&gt;</code>&nbsp;and&nbsp;<code>&lt;percentage</code>.</p>



<p>The main reason behind this is clashing expectations. As <a href="https://drafts.csswg.org/css-values/#exponent-funcs" rel="noopener">the spec</a> says, if in our stylesheet&nbsp;<code>1rem</code>&nbsp;equals the default&nbsp;<code>16px</code>, <code>pow(1rem, 2)</code>&nbsp;would result in&nbsp;<code>1rem</code>&nbsp;(<code>16px</code>&nbsp;again), even though they are the same value. Meanwhile,&nbsp;<code>pow(16px, 2)</code>&nbsp;results in&nbsp;<code>256px</code>. <code>hypot()</code>&nbsp;inputs and outputs are always consistent, so these kinds of unexpected results don&#8217;t occur!</p>



<p class="is-style-explanation"><strong>Note:</strong> The&nbsp;<a href="https://drafts.csswg.org/css-values/#funcdef-hypot#:~:text=Why%20does%20hypot%28%29%20allow%20dimensions%20%28values%20with%20units%29,%20but%20pow%28%29%20and%20sqrt%28%29%20only%20work%20on%20numbers?" rel="noopener">spec goes into a longer explanation</a>&nbsp;on why&nbsp;<code>hypot()</code>&nbsp;takes both dimensions and numbers.</p>


<h3 class="wp-block-heading" id="edge-cases">Edge Cases</h3>


<ul class="wp-block-list">
<li>If any of the arguments is&nbsp;<code>infinity</code>&nbsp;or&nbsp;<code>-infinity</code>, then the result is&nbsp;<code>infinity</code>.</li>



<li>If, for any reason, you want to input only one value, it will return that same value as a positive.</li>
</ul>


<h3 class="wp-block-heading" id="specification">Specification</h3>


<p>The&nbsp;<code>hypot()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/css-values/#funcdef-hypot" rel="noopener">CSS Values and Units Module Level 4</a>&nbsp;specification, which is currently in Editor&#8217;s Draft.</p>


<h3 class="wp-block-heading" id="browser-support">Browser support</h3>



<baseline-status class="wp-block-css-tricks-baseline-status" featureId="exp-functions"></baseline-status>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/h/hypot/">hypot()</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>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392723</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>saturate()</title>
		<link>https://css-tricks.com/almanac/functions/s/saturate/</link>
		
		<dc:creator><![CDATA[Gabriel Shoyombo]]></dc:creator>
		<pubDate>Wed, 08 Apr 2026 18:41:44 +0000</pubDate>
				<category><![CDATA[filter]]></category>
		<guid isPermaLink="false">https://css-tricks.com/?page_id=392485</guid>

					<description><![CDATA[<p>The <code>saturate()</code> function increases or decreases the saturation of an element.</p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/s/saturate/">saturate()</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>The CSS&nbsp;<code>saturate()</code>&nbsp;function increases or decreases an element&#8217;s color saturation level, or in other words, the intensity of the element&#8217;s color. The&nbsp;<code>saturate()</code>&nbsp;is used alongside the&nbsp;<a href="https://css-tricks.com/almanac/properties/f/filter/"><code>filter</code></a>&nbsp;or&nbsp;<a href="https://css-tricks.com/almanac/properties/b/backdrop-filter/"><code>backdrop-filter</code></a>&nbsp;properties.</p>



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



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">img {
  filter: saturate(200%);
}</code></pre>



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



<p>The CSS&nbsp;<code>saturate()</code>&nbsp;function is defined in the&nbsp;<a href="https://drafts.csswg.org/filter-effects/#funcdef-filter-saturate" rel="noopener">Filter Effects Module Level 1</a>&nbsp;specification.</p>


<h3 class="wp-block-heading" id="syntax">Syntax</h3>


<p>The&nbsp;<code>saturate()</code>&nbsp;function&#8217;s formal syntax is given as:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">&lt;saturate()> = saturate( [ &lt;number> | &lt;percentage> ]? )</code></pre>



<p>In practice, we write it as:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">filter: saturate(&lt;amount>);</code></pre>


<h3 class="wp-block-heading" id="argument">Argument</h3>


<p>The&nbsp;<code>saturate()</code>&nbsp;function takes a single argument, which can be a positive decimal or percentage value. The argument determines the new saturation for the input element, where:</p>



<ul class="wp-block-list">
<li><code>0</code>&nbsp;or&nbsp;<code>0%</code>&nbsp;dries out all color from the element, resulting in a grayscale image.</li>



<li><code>1</code>&nbsp;or&nbsp;<code>100%</code>&nbsp;leaves the element completely unchanged.</li>



<li>Values above&nbsp;<code>1</code>&nbsp;or&nbsp;<code>100%</code>&nbsp;increase the saturation linearly.</li>
</ul>



<p>Negative values aren&#8217;t allowed.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">/* Using percentages */
filter: saturate(0%); /* Completely grayscale */
filter: saturate(50%); /* Low saturation */
filter: saturate(100%); /* Unchanged */
filter: saturate(150%); /* Oversaturated by 1.5x  */

/* Decimal or percentage  */
filter: saturate(0.5);
filter: saturate(50%);

/* No argument */
filter: saturate(); /* Same as 100% or 1 */

/* Negative value */
filter: saturate(-1.5); /* Invalid */</code></pre>


<h3 class="wp-block-heading" id="basic-usage">Basic usage</h3>


<p>The&nbsp;<code>saturate()</code>&nbsp;filter is rarely used on its own. Instead, we usually couple it with other filter-related functions to produce more interesting effects. For instance, we can combine&nbsp;<code>saturate()</code>&nbsp;with&nbsp;<code>contrast()</code>&nbsp;to give elements an overly vivid, colorful effect.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.dramatic {
  filter: saturate(180%) contrast(120%);
}</code></pre>



<p>&#8230;while a slightly increased contrast and a lower saturation help enhance the effect of a mid-range sepia, giving a vintage filter effect:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.vintage {
  filter: saturate(60%) sepia(40%) contrast(110%);
}</code></pre>



<p>And in something like a background, we can use low&nbsp;<code>saturate()</code>&nbsp;and&nbsp;<code>brightness()</code>&nbsp;values to reduce the colors and brightness of the background image, along with&nbsp;<code>blur(4px)</code>&nbsp;to reduce its visibility.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.background {
  filter: saturate(50%) brightness(60%) blur(4px);
}</code></pre>



<p>See examples of each of these in the following demo:</p>



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


<h3 class="wp-block-heading" id="music-preview-background">Example: Music preview background</h3>


<p>Besides image filters alone, we can use the&nbsp;<code>saturate()</code>&nbsp;function for more practical cases. For example, we could recreate the previews of music apps like Spotify or Apple Music using&nbsp;<code>saturate()</code>&nbsp;+ other CSS filters:</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.music-bg img {
  filter: blur(30px) saturate(200%) brightness(60%);
}</code></pre>



<p>While the&nbsp;<code><a href="https://css-tricks.com/almanac/functions/b/blur/">blur()</a></code>&nbsp;and&nbsp;<code>brightness()</code>&nbsp;filters soften and darken the background, the&nbsp;<code>saturate()</code>&nbsp;filter boosts its colors so they are clearly visible.</p>



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



<p>Toggle the &#8220;vivid mode,&#8221; and you&#8217;ll notice that&nbsp;<code>saturate(200%)</code>&nbsp;is necessary to keep the background colors from looking dull and washed.</p>


<h3 class="wp-block-heading" id="movie-card-image">Example: Movie card image</h3>


<p>Imagine we&#8217;re creating a movie app. Then, we should have a section to showcase new and coming-soon movies. And to keep all movie posters around the same vivid tone, we could use the&nbsp;<code>saturate()</code>&nbsp;function to increase the purity of the poster&#8217;s color (along with some other filters) and give it more life.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.movie-card img {
  filter: contrast(130%) saturate(140%) sepia(20%);
}</code></pre>



<p>A slightly increased contrast further distinguishes the dark and light points, while a low sepia adds warmth.</p>



<p class="is-style-explanation">The browser applies <a href="https://css-tricks.com/almanac/properties/f/filter/#aa-notes">filters to an image in the same order as they are declared</a>.</p>



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



<p>Once again, we can toggle the &#8220;Boost Saturation&#8221; switch to see what the image would look like with other filters and no increased saturation.</p>


<h2 class="wp-block-heading" id="using-saturate-with-backdrop-filter-">Using <code>saturate()</code> with <code>backdrop-filter</code></h2>


<p>While the filter property applies saturation to the element itself, the <code>backdrop-filter</code> property applies the filter to the area behind it.</p>



<p>A good illustration of this combo is a &#8220;color-pop&#8221; cursor. It&#8217;s a floating element that moves with the mouse, saturating only the portion of the background image it covers.</p>



<p>To get started, we&#8217;ll need a little JavaScript to get the cursor coordinates into CSS:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">const cursor = document.getElementById("cursor");

window.addEventListener("mousemove", (e) =&gt; {
    cursor.style.left = e.clientX + "px";
    cursor.style.top = e.clientY + "px";
});</code></pre>



<p>And initially lower the background image&#8217;s saturation.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">img {
    filter: saturate(30%) brightness(80%);
}</code></pre>



<p>This allows the effect to pop when the user hovers the spotlight over a section of the background.</p>



<pre rel="CSS" class="wp-block-csstricks-code-block language-css" data-line=""><code markup="tt">.cursor {
    backdrop-filter: saturate(400%) contrast(110%);
}</code></pre>



<p>On hover, the cursor area is supersaturated, making the colors more vibrant.</p>



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


<h3 class="wp-block-heading" id="specification">Specification</h3>


<p>The CSS&nbsp;<code>saturate()</code>&nbsp;function is defined, among other filter functions, in the&nbsp;<a href="https://drafts.csswg.org/filter-effects/#funcdef-filter-saturate" rel="noopener">Filter Effects Module 1</a>&nbsp;specification, which is currently in Editor&#8217;s Draft.</p>


<h3 class="wp-block-heading" id="browser-support">Browser support</h3>



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




<baseline-status class="wp-block-css-tricks-baseline-status" featureId="backdrop-filter"></baseline-status>



<p>The&nbsp;<code>saturate()</code>&nbsp;function is currently supported by all modern browsers.</p>



<p></p>
<hr />
<p><small><a rel="nofollow" href="https://css-tricks.com/almanac/functions/s/saturate/">saturate()</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>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">392485</post-id>	</item>
	</channel>
</rss>

<!-- plugin=object-cache-pro client=phpredis metric#hits=8225 metric#misses=18 metric#hit-ratio=99.8 metric#bytes=6862601 metric#prefetches=428 metric#store-reads=34 metric#store-writes=2 metric#store-hits=436 metric#store-misses=14 metric#sql-queries=19 metric#ms-total=511.41 metric#ms-cache=37.43 metric#ms-cache-avg=1.0695 metric#ms-cache-ratio=7.3 -->
