<?xml version="1.0" encoding="UTF-8" standalone="no"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:idx="urn:atom-extension:indexing" xmlns:media="http://search.yahoo.com/mrss/" idx:index="no"><subtitle>tipy na zajímavé články</subtitle>
    <!--
Content-type: Preventing XSRF in IE.

-->
    <generator uri="https://cloud.feedly.com">feedly cloud</generator>
    <id>tag:feedly.com,2013:cloud/feed/https://feedly.com/f/rJIIpYUxxvSQIe3s6CKpJaO8</id>
    <link href="https://feedly.com/f/rJIIpYUxxvSQIe3s6CKpJaO8" rel="self" type="application/rss+xml"/>
    <link href="https://feedly.com/f/rJIIpYUxxvSQIe3s6CKpJaO8?continuation=19bea069d01:402651:9b3933e4" rel="next" type="application/rss+xml"/>
    <title>rarouš.w3b</title>
    <updated>2026-03-16T06:12:02Z</updated>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/c1+r4TpK8uaTr2UOrMQDK0/1GSCBZDCx6Gq/d2cjVgo=_19cf035cda2:e7ddef:7d8a2c4</id>
        <title type="html">Tech's empiricism problem</title>
        <published>2026-03-15T06:36:23Z</published>
        <updated>2026-03-16T06:12:02Z</updated>
        <link href="https://deadsimpletech.com/blog/tech_empiricism_problem" rel="alternate" type="text/html"/>
        <summary type="html">The tech industry has extreme difficulty integrating information that doesn't have its source in an overtly rationalist process. In practice, this means that we tend to think that if you can't give a logical chain of deductions that proves that something is the case, your information is worthless. The issue with this is that day-to-day, in the tech world and outside of it, the vast bulk of the information we use to make decisions isn't this kind of information.</summary>
        <content type="html">The tech industry has extreme difficulty integrating information that doesn't have its source in an overtly rationalist process. In practice, this means that we tend to think that if you can't give a logical chain of deductions that proves that something is the case, your information is worthless. The issue with this is that day-to-day, in the tech world and outside of it, the vast bulk of the information we use to make decisions isn't this kind of information.</content>
        <author>
            <name/>
        </author>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://deadsimpletech.com/rss</id>
            <title type="html">deadSimpleTech blog feed</title>
            <link href="https://deadsimpletech.com" rel="alternate" type="text/html"/>
            <updated>2026-03-16T06:12:02Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cc6dd7b70:3f8aa9:8275c42b</id>
        <title type="html">Nobody Gets Promoted for Simplicity</title>
        <published>2026-03-07T05:55:29Z</published>
        <updated>2026-03-07T05:55:34Z</updated>
        <link href="https://terriblesoftware.org/2026/03/03/nobody-gets-promoted-for-simplicity/" rel="alternate" type="text/html"/>
        <summary type="html">We reward complexity and ignore simplicity. In interviews, design reviews, and promotions. Here’s how to fix it.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
&lt;blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"&gt;
&lt;p class="wp-block-paragraph"&gt;&lt;em&gt;“Simplicity is a great virtue, but it requires hard work to achieve and education to appreciate. And to make matters worse, complexity sells better.”&lt;/em&gt; — Edsger Dijkstra&lt;/p&gt;
&lt;/blockquote&gt;



&lt;p class="wp-block-paragraph"&gt;I think there’s something quietly screwing up a lot of engineering teams. In interviews, in promotion packets, in design reviews: the engineer who overbuilds gets a compelling narrative, but the one who ships the simplest thing that works gets… nothing.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;This isn’t intentional, of course. Nobody sits down and says, &lt;em&gt;“let’s make sure the people who over-engineer things get promoted!”&lt;/em&gt; But that’s what can happen (and it has been, over and over again) when companies evaluate work incorrectly.&lt;/p&gt;



&lt;hr class="wp-block-separator has-alpha-channel-opacity"&gt;&lt;p class="wp-block-paragraph"&gt;Picture two engineers on the same team. Engineer A gets assigned a feature. She looks at the problem, considers a few options, and picks the simplest. A straightforward implementation, maybe 50 lines of code. Easy to read, easy to test, easy for the next person to pick up. It works. She ships it in a couple of days and moves on.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Engineer B gets a similar feature. He also looks at the problem, but he sees an opportunity to build something more “robust.” He introduces a new abstraction layer, creates a pub/sub system for communication between components, adds a configuration framework so the feature is “extensible” for future use cases. It takes three weeks. There are multiple PRs. Lots of excited emojis when he shares the document explaining all of this.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Now, promotion time comes around. Engineer B’s work practically writes itself into a promotion packet: &lt;em&gt;“Designed and implemented a scalable event-driven architecture, introduced a reusable abstraction layer adopted by multiple teams, and built a configuration framework enabling future extensibility.”&lt;/em&gt; That practically screams Staff+.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;But for Engineer A’s work, there’s almost nothing to say. &lt;em&gt;“Implemented feature X.”&lt;/em&gt; Three words. Her work was better. But it’s invisible because of how simple she made it look. You can’t write a compelling narrative about the thing you &lt;em&gt;didn’t&lt;/em&gt; build. &lt;strong&gt;Nobody gets promoted for the complexity they avoided&lt;/strong&gt;.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Complexity looks smart. Not because it is, but because our systems are set up to reward it. And the incentive problem doesn’t start at promotion time. It starts before you even get the job.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Think about interviews. You’re in a system design round, and you propose a simple solution. A single database, a straightforward API, maybe a caching layer. The interviewer is like: &lt;em&gt;“What about scalability? What if you have ten million users?”&lt;/em&gt; So you add services. You add queues. You add sharding. You draw more boxes on the whiteboard. The interviewer finally seems satisfied now.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;What you just learned is that complexity impresses people. The simple answer wasn’t wrong. It just wasn’t interesting enough. And you might carry that lesson with you into your career. To be fair, interviewers sometimes have good reasons to push on scale; they want to see how you think under pressure and whether you understand distributed systems. But when the takeaway for the candidate is “simple wasn’t enough,” something’s off.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;It also shows up in design reviews. An engineer proposes a clean, simple approach and gets hit with &lt;em&gt;“shouldn’t we future-proof this?”&lt;/em&gt; So they go back and add layers they don’t need yet, abstractions for problems that might never materialize, flexibility for requirements nobody has asked for. Not because the problem demanded it, but because the room expected it.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;I’ve &lt;a href="https://terriblesoftware.org/2025/05/28/duplication-is-not-the-enemy/"&gt;seen engineers&lt;/a&gt; (and have been one myself) create abstractions to avoid duplicating a few lines of code, only to end up with something far harder to understand and maintain than the duplication ever was. Every time, it felt like the right thing to do. The code looked more “professional.” More engineered. But the users didn’t get their feature any faster, and the next engineer to touch it had to spend half a day understanding the abstraction before they could make any changes.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Now, let me be clear: complexity is sometimes the right call. If you’re processing millions of transactions, you might need distributed systems. If you have 10 teams working on the same product, you probably need service boundaries. When the problem is complex, the solution (probably) should be too!&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;The issue isn’t complexity itself. It’s unearned complexity. There’s a difference between &lt;em&gt;“we’re hitting database limits and need to shard”&lt;/em&gt; and &lt;em&gt;“we might hit database limits in three years, so let’s shard now.”&lt;/em&gt;&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Some engineers understand this. And when you look at their code (and architecture), you think &lt;em&gt;“well, yeah, of course.”&lt;/em&gt; There’s no magic, no cleverness, nothing that makes you feel stupid for not understanding it. And that’s exactly the point.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;The &lt;a href="https://terriblesoftware.org/2025/11/25/what-actually-makes-you-senior/"&gt;actual path to seniority&lt;/a&gt; isn’t learning more tools and patterns, but learning when not to use them. &lt;strong&gt;Anyone can add complexity. It takes experience and confidence to leave it out&lt;/strong&gt;.&lt;/p&gt;



&lt;hr class="wp-block-separator has-alpha-channel-opacity"&gt;&lt;p class="wp-block-paragraph"&gt;So what do we actually do about this? Because saying “keep it simple” is easy. Changing incentive structures is harder.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;&lt;strong&gt;If you’re an engineer&lt;/strong&gt;, learn that simplicity needs to be made visible. The work doesn’t speak for itself; not because it’s not good, but because most systems aren’t designed to hear it.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Start with how you talk about your own work. “Implemented feature X” doesn’t mean much. But &lt;em&gt;“evaluated three approaches including an event-driven architecture and a custom abstraction layer, determined that a straightforward implementation met all current and projected requirements, and shipped in two days with zero incidents over six months”&lt;/em&gt;, that’s the same simple work, just described in a way that captures the judgment behind it. The decision &lt;em&gt;not&lt;/em&gt; to build something is a decision, an important one! Document it accordingly.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;In design reviews, when someone asks “shouldn’t we future-proof this?”, don’t just cave and go add layers. Try: &lt;em&gt;“Here’s what it would take to add that later if we need it, and here’s what it costs us to add it now. I think we wait.”&lt;/em&gt; You’re not pushing back, but showing you’ve done your homework. You considered the complexity and chose not to take it on.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;And yes, bring this up with your manager. Something like: &lt;em&gt;“I want to make sure the way I document my work reflects the decisions I’m making, not just the code I’m writing. Can we talk about how to frame that for my next review?”&lt;/em&gt; Most managers will appreciate this because you’re making their job easier. You’re giving them language they can use to advocate for you.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Now, if you do all of this and your team still only promotes the person who builds the most elaborate system… that’s useful information too. It tells you something about where you work. Some cultures genuinely value simplicity. Others say they do, but reward the opposite. If you’re in the second kind, you can either play the game or find a place where good judgment is actually recognized. But at least you’ll know which one you’re in.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;&lt;strong&gt;If you’re an engineering leader&lt;/strong&gt;, this one’s on you more than anyone else. You set the incentives, whether you realize it or not. And the problem is that most promotion criteria are basically designed to reward complexity, even when they don’t intend to. “Impact” gets measured by the size and scope of what someone built, which more often than not matters! But what they avoided should also matter.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;So start by changing the questions you ask. In design reviews, instead of “have we thought about scale?”, try &lt;em&gt;“what’s the simplest version we could ship, and what specific signals would tell us we need something more complex?”&lt;/em&gt; That one question changes the game: it makes simplicity the default and puts the burden of proof on complexity, not the other way around!&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;In promotion discussions, push back when someone’s packet is basically a list of impressive-sounding systems. Ask: &lt;em&gt;“Was all of that necessary? Did we actually need a pub/sub system here, or did it just look good on paper?”&lt;/em&gt; And when an engineer on your team ships something clean and simple, help them write the narrative. “Evaluated multiple approaches and chose the simplest one that solved the problem” &lt;em&gt;is&lt;/em&gt; a compelling promotion case, but only if you actually treat it like one.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;One more thing: pay attention to what you celebrate publicly. If every shout-out in your team channel is for the big, complex project, that’s what people will optimize for. Start recognizing the engineer who deleted code. The one who said “we don’t need this yet” and was right.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;At the end of the day, if we keep rewarding complexity and ignoring simplicity, we shouldn’t be surprised when that’s exactly what we get. But the fix isn’t complicated. Which, I guess, is kind of the point.&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://i0.wp.com/terriblesoftware.org/wp-content/uploads/2026/03/chjpdmf0zs9sci9pbwfnzxmvd2vic2l0zs8ymdiylta1l25zmtexndetaw1hz2uta3d2d3b1c3kuanbn.webp?fit=1024%2C680&amp;ssl=1&amp;w=640"/>
        <link href="https://i0.wp.com/terriblesoftware.org/wp-content/uploads/2026/03/chjpdmf0zs9sci9pbwfnzxmvd2vic2l0zs8ymdiylta1l25zmtexndetaw1hz2uta3d2d3b1c3kuanbn.webp?fit=1024%2C680&amp;ssl=1" rel="enclosure" type="image"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://terriblesoftware.org/feed/</id>
            <title type="html">Terrible Software</title>
            <link href="https://terriblesoftware.org" rel="alternate" type="text/html"/>
            <updated>2026-03-07T05:55:34Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbf7aa19d:105d1:68bfe759</id>
        <title type="html">The Frontend Treadmill</title>
        <published>2026-03-05T19:30:10Z</published>
        <updated>2026-03-05T19:30:14Z</updated>
        <link href="https://polotek.net/posts/the-frontend-treadmill/" rel="alternate" type="text/html"/>
        <summary type="html">A lot of frontend teams are very convinced that rewriting their frontend will lead to the promised land. And I am the bearer of bad tidings.
If you are building a product that you hope has longevity, your frontend framework is the least interesting technical decision for you to make. And all of the time you spend arguing about it is wasted energy.
I will die on this hill.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;A lot of frontend teams are very convinced that rewriting their frontend will lead to the promised land. And I am the bearer of bad tidings.&lt;/p&gt;&lt;p&gt;If you are building a product that you hope has longevity, your frontend framework is the least interesting technical decision for you to make. And all of the time you spend arguing about it is wasted energy.&lt;/p&gt;&lt;p&gt;I will die on this hill.&lt;/p&gt;&lt;p&gt;If your product is still around in 5 years, you’re doing great and you should feel successful. But guess what? Whatever framework you choose will be obsolete in 5 years. That’s just how the frontend community has been operating, and I don’t expect it to change soon. Even the popular frameworks that are still around are completely different. Because change is the name of the game. So they’re gonna rewrite their shit too and just give it a new version number.&lt;/p&gt;&lt;p&gt;Product teams that are smart are getting off the treadmill. Whatever framework you currently have, start investing in getting to know it deeply. Learn the tools until they are not an impediment to your progress. That’s the only option. Replacing it with a shiny new tool is a trap.&lt;/p&gt;&lt;p&gt;I also wanna give a piece of candid advice to engineers who are searching for jobs. If you feel strongly about what framework you want to use, please make that a criteria for your job search. Please stop walking into teams and derailing everything by trying to convince them to switch from framework X to your framework of choice. It’s really annoying and tremendously costly.&lt;/p&gt;&lt;p&gt;I always have to start with the cynical take. It’s just how I am. But I do want to talk about what I think should be happening instead.&lt;/p&gt;&lt;p&gt;Companies that want to reduce the cost of their frontend tech becoming obsoleted so often should be looking to get back to fundamentals. Your teams should be working closer to the web platform with a lot less complex abstractions. We need to relearn what the web is capable of and go back to that.&lt;/p&gt;&lt;p&gt;Let’s be clear, I’m not suggesting this is strictly better and the answer to all of your problems. I’m suggesting this as an intentional business tradeoff that I think provides more value and is less costly in the long run. I believe if you stick closer to core web technologies, you’ll be better able to hire capable engineers in the future without them convincing you they can’t do work without rewriting millions of lines of code.&lt;/p&gt;&lt;p&gt;And if you’re an engineer, you will be able to retain much higher market value over time if you dig into and understand core web technologies. I was here before react, and I’ll be here after it dies. You may trade some job marketability today. But it does a lot more for career longevity than trying to learn every new thing that gets popular. And you see how quickly they discarded us when the market turned anyway. Knowing certain tech won’t save you from those realities.&lt;/p&gt;&lt;p&gt;I couldn’t speak this candidly about this stuff when I held a management role. People can’t help but question my motivations and whatever agenda I may be pushing. Either that or I get into a lot of trouble with my internal team because they think I’m talking about them. But this is just what I’ve seen play out after doing this for 20+ years. And I feel like we need to be able to speak plainly.&lt;/p&gt;&lt;p&gt;This has been brewing in my head for a long time. The frontend ecosystem is kind of broken right now. And it’s frustrating to me for a few different reasons. New developers are having an extremely hard time learning enough skills to be gainfully employed. They are drowning in this complex garbage and feeling really disheartened. As a result, companies are finding it more difficult to do basic hiring. The bar is so high just to get a regular dev job. And everybody loses.&lt;/p&gt;&lt;p&gt;What’s even worse is that I believe a lot of this energy is wasted. People that are learning the current tech ecosystem are absolutely not learning web fundamentals. They are too abstracted away. And when the stack changes again, these folks are going to be at a serious disadvantage when they have to adapt away from what they learned. It’s a deep disservice to people’s professional careers, and it’s going to cause a lot of heartache later.&lt;/p&gt;&lt;p&gt;On a more personal note, this is frustrating to me because I think it’s a big part of why we’re seeing the web stagnate so much. I still run into lots of devs who are creative and enthusiastic about building cool things. They just can’t. They are trying and failing because the tools being recommended to them are just not approachable enough. And at the same time, they’re being convinced that learning fundamentals is a waste of time because it’s so different from what everybody is talking about.&lt;/p&gt;&lt;p&gt;I guess I want to close by stating my biases. I’m a web guy. I’ve been bullish on the web for 20+ years, and I will continue to be. I think it is an extremely capable and unique platform for delivering software. And it has only gotten better over time while retaining an incredible level of backwards compatibility. The underlying tools we have are dope now. But our current framework layer is working against the grain instead of embracing the platform.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;This is from &lt;a href="https://social.polotek.net/@polotek/112617458589147547"&gt;a recent thread I wrote on mastodon&lt;/a&gt;. Reproduced with only light editing.&lt;/p&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Marco Rogers (polotek)</name>
        </author>
        <media:content medium="image" url="https://polotek.net/js/script.min.74bf1a3fcf1af396efa4acf3e660e876b61a2153ab9cbe1893ac24ea6d4f94ee.js"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbf6d2731:80d01:4b6775c</id>
        <title type="html">A GitHub Issue Title Compromised 4,000 Developer Machines</title>
        <published>2026-03-05T19:15:27Z</published>
        <updated>2026-03-05T19:15:36Z</updated>
        <link href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another" rel="alternate" type="text/html"/>
        <summary type="html">A prompt injection in a GitHub issue triggered a chain reaction that ended with 4,000 developers getting OpenClaw installed without consent. The attack composes well-understood vulnerabilities into something new: one AI tool bootstrapping another.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;img src="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another/hero-clinejection-chain-1600x900.png" alt="The Clinejection attack chain: a prompt injection in a GitHub issue title cascades through AI triage, cache poisoning, and credential theft to silently install OpenClaw on 4,000 developer machines" tabindex="0"&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;img src="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another/hero-clinejection-chain-1600x900.png" alt="The Clinejection attack chain: a prompt injection in a GitHub issue title cascades through AI triage, cache poisoning, and credential theft to silently install OpenClaw on 4,000 developer machines"&gt;&lt;span&gt;esc to close&lt;/span&gt;&lt;/div&gt;&lt;figcaption&gt;Five steps from a GitHub issue title to 4,000 compromised developer machines. The entry point was natural language.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;On February 17, 2026, someone published &lt;code&gt;cline@2.3.0&lt;/code&gt; to npm. The CLI binary was byte-identical to the previous version. The only change was one line in &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;"postinstall": "npm install -g openclaw@latest"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the next eight hours, every developer who installed or updated Cline got OpenClaw - a separate AI agent with full system access - installed globally on their machine without consent. Approximately 4,000 downloads occurred before the package was pulled&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The interesting part is not the payload. It is how the attacker got the npm token in the first place: by injecting a prompt into a GitHub issue title, which an AI triage bot read, interpreted as an instruction, and executed.&lt;/p&gt;
&lt;h2&gt;The full chain&lt;/h2&gt;
&lt;p&gt;The attack - which Snyk named "Clinejection"&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-2" id="user-content-fnref-2"&gt;2&lt;/a&gt;&lt;/sup&gt; - composes five well-understood vulnerabilities into a single exploit that requires nothing more than opening a GitHub issue.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Prompt injection via issue title.&lt;/strong&gt; Cline had deployed an AI-powered issue triage workflow using Anthropic's &lt;code&gt;claude-code-action&lt;/code&gt;. The workflow was configured with &lt;code&gt;allowed_non_write_users: "*"&lt;/code&gt;, meaning any GitHub user could trigger it by opening an issue. The issue title was interpolated directly into Claude's prompt via &lt;code&gt;${{ github.event.issue.title }}&lt;/code&gt; without sanitisation.&lt;/p&gt;
&lt;p&gt;On January 28, an attacker created Issue #8904 with a title crafted to look like a performance report but containing an embedded instruction: install a package from a specific GitHub repository&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: The AI bot executes arbitrary code.&lt;/strong&gt; Claude interpreted the injected instruction as legitimate and ran &lt;code&gt;npm install&lt;/code&gt; pointing to the attacker's fork - a typosquatted repository (&lt;code&gt;glthub-actions/cline&lt;/code&gt;, note the missing 'i' in 'github'). The fork's &lt;code&gt;package.json&lt;/code&gt; contained a preinstall script that fetched and executed a remote shell script.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Cache poisoning.&lt;/strong&gt; The shell script deployed Cacheract, a GitHub Actions cache poisoning tool. It flooded the cache with over 10GB of junk data, triggering GitHub's LRU eviction policy and evicting legitimate cache entries. The poisoned entries were crafted to match the cache key pattern used by Cline's nightly release workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Credential theft.&lt;/strong&gt; When the nightly release workflow ran and restored &lt;code&gt;node_modules&lt;/code&gt; from cache, it got the compromised version. The release workflow held the &lt;code&gt;NPM_RELEASE_TOKEN&lt;/code&gt;, &lt;code&gt;VSCE_PAT&lt;/code&gt; (VS Code Marketplace), and &lt;code&gt;OVSX_PAT&lt;/code&gt; (OpenVSX). All three were exfiltrated&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3-2"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Malicious publish.&lt;/strong&gt; Using the stolen npm token, the attacker published &lt;code&gt;cline@2.3.0&lt;/code&gt; with the OpenClaw postinstall hook. The compromised version was live for eight hours before StepSecurity's automated monitoring flagged it - approximately 14 minutes after publication&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1-2"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;A botched rotation made it worse&lt;/h2&gt;
&lt;p&gt;Security researcher Adnan Khan had actually discovered the vulnerability chain in late December 2025 and reported it via a GitHub Security Advisory on January 1, 2026. He sent multiple follow-ups over five weeks. None received a response&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3-3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;When Khan publicly disclosed on February 9, Cline patched within 30 minutes by removing the AI triage workflows. They began credential rotation the next day.&lt;/p&gt;
&lt;p&gt;But the rotation was incomplete. The team deleted the wrong token, leaving the exposed one active&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-4" id="user-content-fnref-4"&gt;4&lt;/a&gt;&lt;/sup&gt;. They discovered the error on February 11 and re-rotated. But the attacker had already exfiltrated the credentials, and the npm token remained valid long enough to publish the compromised package six days later.&lt;/p&gt;
&lt;p&gt;Khan was not the attacker. A separate, unknown actor found Khan's proof-of-concept on his test repository and weaponised it against Cline directly&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3-4"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;The new pattern: AI installs AI&lt;/h2&gt;
&lt;p&gt;The specific vulnerability chain is interesting but not unprecedented. Prompt injection, cache poisoning, and credential theft are all documented attack classes. What makes Clinejection distinct is the outcome: one AI tool silently bootstrapping a second AI agent on developer machines.&lt;/p&gt;
&lt;p&gt;This creates a recursion problem in the supply chain. The developer trusts Tool A (Cline). Tool A is compromised to install Tool B (OpenClaw). Tool B has its own capabilities - shell execution, credential access, persistent daemon installation - that are independent of Tool A and invisible to the developer's original trust decision.&lt;/p&gt;
&lt;p&gt;OpenClaw as installed could read credentials from &lt;code&gt;~/.openclaw/&lt;/code&gt;, execute shell commands via its Gateway API, and install itself as a persistent system daemon surviving reboots&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1-3"&gt;1&lt;/a&gt;&lt;/sup&gt;. The severity was debated - Endor Labs characterised the payload as closer to a proof-of-concept than a weaponised attack&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-5" id="user-content-fnref-5"&gt;5&lt;/a&gt;&lt;/sup&gt; - but the mechanism is what matters. The next payload will not be a proof-of-concept.&lt;/p&gt;
&lt;p&gt;This is the supply chain equivalent of &lt;a href="https://en.wikipedia.org/wiki/Confused_deputy_problem"&gt;confused deputy&lt;/a&gt;: the developer authorises Cline to act on their behalf, and Cline (via compromise) delegates that authority to an entirely separate agent the developer never evaluated, never configured, and never consented to.&lt;/p&gt;
&lt;h2&gt;Why existing controls did not catch it&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;npm audit&lt;/strong&gt;: The postinstall script installs a legitimate, non-malicious package (OpenClaw). There is no malware to detect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code review&lt;/strong&gt;: The CLI binary was byte-identical to the previous version. Only &lt;code&gt;package.json&lt;/code&gt; changed, and only by one line. Automated diff checks that focus on binary changes would miss it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Provenance attestations&lt;/strong&gt;: Cline was not using OIDC-based npm provenance at the time. The compromised token could publish without provenance metadata, which StepSecurity flagged as anomalous&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1-4"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Permission prompts&lt;/strong&gt;: The installation happens in a postinstall hook during &lt;code&gt;npm install&lt;/code&gt;. No AI coding tool prompts the user before a dependency's lifecycle script runs. The operation is invisible.&lt;/p&gt;
&lt;p&gt;The attack exploited the gap between what developers think they are installing (a specific version of Cline) and what actually executes (arbitrary lifecycle scripts from the package and everything it transitively installs).&lt;/p&gt;
&lt;h2&gt;What Cline changed afterward&lt;/h2&gt;
&lt;p&gt;Cline's post-mortem&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-4" id="user-content-fnref-4-2"&gt;4&lt;/a&gt;&lt;/sup&gt; outlines several remediation steps:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Eliminated GitHub Actions cache usage from credential-handling workflows&lt;/li&gt;
&lt;li&gt;Adopted OIDC provenance attestations for npm publishing, eliminating long-lived tokens&lt;/li&gt;
&lt;li&gt;Added verification requirements for credential rotation&lt;/li&gt;
&lt;li&gt;Began working on a formal vulnerability disclosure process with SLAs&lt;/li&gt;
&lt;li&gt;Commissioned third-party security audits of CI/CD infrastructure&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;These are meaningful improvements. The OIDC migration alone would have prevented the attack - a stolen token cannot publish packages when provenance requires a cryptographic attestation from a specific GitHub Actions workflow.&lt;/p&gt;
&lt;h2&gt;The architectural question&lt;/h2&gt;
&lt;p&gt;Clinejection is a supply chain attack, but it is also an agent security problem. The entry point was natural language in a GitHub issue title. The first link in the chain was an AI bot that interpreted untrusted text as an instruction and executed it with the privileges of the CI environment.&lt;/p&gt;
&lt;p&gt;This is the same structural pattern we have written about in the context of &lt;a href="https://grith.ai/blog/mcp-servers-new-npm-packages"&gt;MCP tool poisoning&lt;/a&gt; and &lt;a href="https://grith.ai/blog/agent-skills-supply-chain"&gt;agent skill registries&lt;/a&gt; - untrusted input reaches an agent, the agent acts on it, and nothing evaluates the resulting operations before they execute.&lt;/p&gt;
&lt;p&gt;The difference here is that the agent was not a developer's local coding assistant. It was an automated CI workflow that ran on every new issue, with shell access and cached credentials. The blast radius was not one developer's machine - it was the entire project's publication pipeline.&lt;/p&gt;
&lt;p&gt;Every team deploying AI agents in CI/CD - for issue triage, code review, automated testing, or any other workflow - has this same exposure. The agent processes untrusted input (issues, PRs, comments) and has access to secrets (tokens, keys, credentials). The question is whether anything evaluates what the agent does with that access.&lt;/p&gt;
&lt;p&gt;Per-syscall interception catches this class of attack at the operation layer. When the AI triage bot attempts to run &lt;code&gt;npm install&lt;/code&gt; from an unexpected repository, the operation is evaluated against policy before it executes - regardless of what the issue title said. When a lifecycle script attempts to exfiltrate credentials to an external host, the egress is blocked.&lt;/p&gt;
&lt;p&gt;The entry point changes. The operations do not. &lt;a href="https://grith.ai/"&gt;grith&lt;/a&gt; was built to catch exactly this class of problem - evaluating every operation at the syscall layer, regardless of which agent triggered it or why.&lt;/p&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another/hero-clinejection-chain-1600x900.png"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbc9c1a2c:32fe9b:98d097d6</id>
        <title type="html">Container queries and units in action</title>
        <published>2026-03-05T06:07:52Z</published>
        <updated>2026-03-05T06:07:56Z</updated>
        <link href="https://web.dev/articles/baseline-in-action-container-queries" rel="alternate" type="text/html"/>
        <summary type="html">Learn how to use CSS container queries for adding both responsive typography and styles based on container dimensions in this guide.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;

  
    
    




&lt;p&gt;

&lt;/p&gt;

&lt;p&gt;
  Published: October 23, 2025
&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;
   
&lt;/p&gt;

&lt;p&gt;One of the goals when writing CSS is to build component parts that will adapt well to different (and unexpected) contexts. Ideally, a component can be placed inside any "container" element without it feeling broken or out of place. How can you accomplish this in a complex layout like a store where the primary component—the "product"—has to fit into a variety of list layouts, including the sidebar?&lt;/p&gt;

&lt;h2 id="responsive_typography_with_cqi_units" tabindex="-1"&gt;&lt;span&gt;Responsive typography with &lt;code dir="ltr"&gt;cqi&lt;/code&gt; units&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;The first step is to define some basic sizing variables that could be reused across the project—starting with whitespace sizes. But before creating any custom properties, the browser provides some useful named values as CSS units:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;code dir="ltr"&gt;1em&lt;/code&gt;: the current font size.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1rem&lt;/code&gt;: the font size on the &lt;code dir="ltr"&gt;:root (html)&lt;/code&gt; element.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1lh&lt;/code&gt; / &lt;code dir="ltr"&gt;1rlh&lt;/code&gt;: the current and root line heights.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1vw&lt;/code&gt;: the viewport width.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1vi&lt;/code&gt;: the viewport "inline" size (for English, this is the same as &lt;code dir="ltr"&gt;vw&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1cqi&lt;/code&gt;: the inline size of the nearest "container" (defaulting to the viewport).&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;You can think of these units as common variables provided by the browser, with a shorthand syntax for multiplication. If you wanted half of a &lt;code dir="ltr"&gt;--line-height&lt;/code&gt; custom property in CSS, you would need to write the entire calculation &lt;code dir="ltr"&gt;calc(0.5 * var(--line-height))&lt;/code&gt;, but with the &lt;code dir="ltr"&gt;lh&lt;/code&gt; unit, you can ask for &lt;code dir="ltr"&gt;0.5lh&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;Custom properties like &lt;code dir="ltr"&gt;--brand-color&lt;/code&gt; and &lt;code dir="ltr"&gt;--button-background&lt;/code&gt; have different meanings and serve a different purpose, even when they result in the same &lt;code dir="ltr"&gt;deepPink&lt;/code&gt; color (another browser-provided variable). Similarly, &lt;code dir="ltr"&gt;1em&lt;/code&gt; might sometimes be equal to &lt;code dir="ltr"&gt;16px&lt;/code&gt;, but that's not a stable relationship. Like any other variable, units should be used to express a relationship, rather than an expected value.&lt;/p&gt;

&lt;p&gt;Both &lt;code dir="ltr"&gt;1em&lt;/code&gt; and &lt;code dir="ltr"&gt;1lh&lt;/code&gt; are font-relative units that could be used for spacing, but only one of them has a reliable relationship to the current line height. If this page involved a lot of elements with prose in them—paragraphs and lists, for example—the &lt;code dir="ltr"&gt;lh&lt;/code&gt; unit would work well for spacing between them:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ul&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ol&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;margin-block&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;lh&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That will maintain a consistent baseline rhythm, without any extra work. But this page has almost no prose. Instead of spacing within a flow of text, this layout requires spacing to push text away from the edge of a card, and spacing between columns and rows in a grid or stacked layout. In this case there are several things to consider:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;Multiples (or fractions) of &lt;code dir="ltr"&gt;1lh&lt;/code&gt; may still be useful for maintaining vertical rhythm across the page.&lt;/li&gt;
&lt;li&gt;The &lt;code dir="ltr"&gt;cqi&lt;/code&gt; unit would account for the amount of &lt;em&gt;available space&lt;/em&gt; in a given context.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;By combining these two units in a &lt;code dir="ltr"&gt;round()&lt;/code&gt; function, the &lt;code dir="ltr"&gt;--gap&lt;/code&gt; variable is based primarily on the container size, but rounded up to a multiple of quarter-lines:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--gap&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;round&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;up&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;cqi&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.25&lt;/span&gt;&lt;span&gt;lh&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;If the &lt;code dir="ltr"&gt;line-height&lt;/code&gt; is &lt;code dir="ltr"&gt;20px&lt;/code&gt;, then the &lt;code dir="ltr"&gt;--&lt;/code&gt;gap will be multiples of &lt;code dir="ltr"&gt;5px&lt;/code&gt;—but the exact multiple will depend on available space. If either the &lt;code dir="ltr"&gt;line-height&lt;/code&gt; or available space change, the &lt;code dir="ltr"&gt;--gap&lt;/code&gt; variable will adapt to its new context. To make the &lt;code dir="ltr"&gt;--gap&lt;/code&gt; consistent across the entire design, you could replace the container-relative &lt;code dir="ltr"&gt;cqi&lt;/code&gt; units with viewport-relative &lt;code dir="ltr"&gt;vi&lt;/code&gt; units.&lt;/p&gt;

&lt;p&gt;The same approach is useful when establishing "fluid" font sizes that respond to available space. This &lt;code dir="ltr"&gt;--body-text&lt;/code&gt; variable is based on the user-provided font preference, with some range to adapt based on the container. In this case, &lt;code dir="ltr"&gt;clamp()&lt;/code&gt; ensures the font will be at least as large as the user's preference, can grow some as the container size increases, but will stop growing at some point:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--body-text&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clamp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.875&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;cqi&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1.25&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;&lt;li&gt;The range is clamped between &lt;code dir="ltr"&gt;1rem&lt;/code&gt; and &lt;code dir="ltr"&gt;1.25rem&lt;/code&gt;, to stay near the user-selected font size.&lt;/li&gt;
&lt;li&gt;The &lt;code dir="ltr"&gt;cqi&lt;/code&gt; value determines how &lt;em&gt;fast&lt;/em&gt; the font will grow or shrink in relation to available space, which is added to a &lt;code dir="ltr"&gt;rem&lt;/code&gt; value to offset that growth based on the user-selected size.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Keeping the central &lt;code dir="ltr"&gt;rem&lt;/code&gt; value near &lt;code dir="ltr"&gt;1&lt;/code&gt; and the added &lt;code dir="ltr"&gt;cqi&lt;/code&gt; value low ensures that users still have significant control when zooming in or out. You can adjust those two values, and then use browser zoom to see how they interact. The closer you get to &lt;code dir="ltr"&gt;100cqi&lt;/code&gt;, and the farther you fall below &lt;code dir="ltr"&gt;1rem&lt;/code&gt;, the less influence user font preferences will have—and the less font sizes will respond to zooming in or out.&lt;/p&gt;

&lt;p&gt;The &lt;code dir="ltr"&gt;--item-title&lt;/code&gt; variable is slightly larger and more responsive, and the &lt;code dir="ltr"&gt;--list-title&lt;/code&gt; size responds to the viewport size (using &lt;code dir="ltr"&gt;vi&lt;/code&gt;) rather than the immediate container size. That way item headings respond to their context, but the main list headings all match in size no matter where they show up.&lt;/p&gt;
&lt;aside&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;span&gt; Product titles in the cart sidebar might be slightly different from product titles in the main list—but the "cart" heading is always identical to the "products" list heading.&lt;/span&gt;&lt;/aside&gt;&lt;h2 id="defining_containers_to_measure_in_context" tabindex="-1"&gt;&lt;span&gt;Defining containers to measure in context&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;At this point, container query units have been used to create adaptive typography on a web page, but new containers haven't been defined.&lt;/p&gt;

&lt;p&gt;By default, &lt;code dir="ltr"&gt;1cqi&lt;/code&gt; (&lt;code dir="ltr"&gt;1/100&lt;/code&gt; container query inline size) is the same as &lt;code dir="ltr"&gt;1svi&lt;/code&gt; (&lt;code dir="ltr"&gt;1/100&lt;/code&gt; small viewport inline size) because the &lt;a href="https://web.dev/learn/css/sizing#alternative_viewport-relative_units"&gt;"small" viewport&lt;/a&gt; acts as the initial container for any web page. In order to take full advantage of the &lt;code dir="ltr"&gt;cqi&lt;/code&gt; unit, you need to define additional "containers" within the page. The primary layout containers on this page are the &lt;code dir="ltr"&gt;product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt;—so they are set to expose their &lt;code dir="ltr"&gt;inline-size&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;product-list&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;shopping-cart&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;container-type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;inline&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;aside&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;span&gt; &lt;code dir="ltr"&gt;&amp;lt;div&amp;gt;&lt;/code&gt; or &lt;code dir="ltr"&gt;&amp;lt;section&amp;gt;&lt;/code&gt; elements with &lt;code dir="ltr"&gt;.product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;.shopping-cart&lt;/code&gt; classes would also work here. Since those have no semantic meaning or functionality in HTML, it also works to define unregistered custom elements with those names. This doesn't impact the CSS, except that custom elements use a &lt;code dir="ltr"&gt;display&lt;/code&gt; of &lt;code dir="ltr"&gt;inline&lt;/code&gt; by default.&lt;/span&gt;&lt;/aside&gt;&lt;p&gt;Container query units—including &lt;code dir="ltr"&gt;cqi&lt;/code&gt;—aren't able to measure the element that they are used on. If you set the &lt;code dir="ltr"&gt;width&lt;/code&gt; of &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; to &lt;code dir="ltr"&gt;25cqi&lt;/code&gt;, it would be a paradox to determine the container's width based on its own width! Instead, the result will be based on the next ancestor container in the tree hierarchy &lt;em&gt;that contains&lt;/em&gt; &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; cards are part of a &lt;code dir="ltr"&gt;product-list&lt;/code&gt; grid that can be changed by the user. The &lt;code dir="ltr"&gt;list&lt;/code&gt; layout option displays each card at full width. In that case, referring to the parent container size is useful, but both the &lt;code dir="ltr"&gt;small-grid&lt;/code&gt; and &lt;code dir="ltr"&gt;large-grid&lt;/code&gt; options cause the card to grow and shrink depending on how many columns fit into the container. Even when the container is quite large, the cards can remain tightly packed into smaller grid cells.&lt;/p&gt;

&lt;p&gt;There's currently no way to declare those grid cells as "containers" directly. Instead, an extra element is needed to measure within each cell. That's why each &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; instance has an &lt;code dir="ltr"&gt;&amp;lt;article&amp;gt;&lt;/code&gt; element nested directly inside. &lt;code dir="ltr"&gt;product-detail &amp;gt; article&lt;/code&gt; is the card to be styled, while &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; itself is used only as a container to measure. That allows the &lt;code dir="ltr"&gt;cqi&lt;/code&gt;-based text and spacing calculations previously defined to be recalculated for the space available to each card.&lt;/p&gt;

&lt;h2 id="explicit_container_queries" tabindex="-1"&gt;&lt;span&gt;Explicit container queries&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;Container units are powerful, but sometimes it's useful to make more dramatic changes in a component layout when the available size crosses a threshold. These are often called &lt;em&gt;breakpoints&lt;/em&gt;—since the fix is applied at the point when a given layout begins to break. You may already be familiar with using &lt;code dir="ltr"&gt;@media&lt;/code&gt; to add breakpoints based on the viewport size:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grid&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'controls'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'cart'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'list'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;minmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;-content&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;

&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@media&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(width&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;30em)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'controls controls'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'list cart'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; appears above the main &lt;code dir="ltr"&gt;product-list&lt;/code&gt; on small screens—but at a certain point, there's more horizontal space, and a sidebar makes more sense. Since that shift depends on the overall viewport, a media query is used to handle the change. However, the &lt;code dir="ltr"&gt;product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; components might appear in different contexts, somewhat independent of the viewport size. When the viewport grows wider than the sidebar breakpoint, the &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; component suddenly becomes &lt;em&gt;smaller&lt;/em&gt;, providing less space for products inside. This is where container queries become necessary. The syntax is nearly identical to a media query, but uses &lt;code dir="ltr"&gt;@container&lt;/code&gt; rather than &lt;code dir="ltr"&gt;@media&lt;/code&gt; at the start of the rule:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;article&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grid&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'image'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;'title'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'summary'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'button'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@container&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(inline-size&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;40ch)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;--image-ratio&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image title'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image summary'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image button'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;minmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;container&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;inline-size&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;50ch&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image title title'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image summary button'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1fr&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;minmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50px&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;%,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;500px&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1fr&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fit-content&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;%);&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The initial goal of this post is that components should be able to respond to any context. To make that work, each component defines its own internal behavior, without explicit knowledge of the surrounding components. Container queries help us accomplish that. The &lt;code dir="ltr"&gt;product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; components don't need to be aware of why they have more or less space in a given context, they only need to know &lt;em&gt;how much space&lt;/em&gt; is currently available.&lt;/p&gt;

&lt;h2 id="transition_grid_templates_and_visibility" tabindex="-1"&gt;&lt;span&gt;Transition grid templates and visibility&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;With a layout that's changing often based on container queries, how do we smooth out all of the transitions? When using grids for layout, it's possible to animate the size of a column or row, as well as the gap between columns and rows. In this case, the cart area and gap are expanded from &lt;code dir="ltr"&gt;0&lt;/code&gt; width when the cart is opened. In order to animate grid templates like this, two things are required:&lt;/p&gt;

&lt;ol&gt;&lt;li&gt;The initial and end states must have the same number of tracks (columns or rows).&lt;/li&gt;
&lt;li&gt;The animated tracks must use comparable units.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;While it would be possible to change from a one-column grid to a two-column grid when the sidebar is hidden, the empty sidebar column is instead resized to &lt;code dir="ltr"&gt;0&lt;/code&gt;, and the column-gap is also set to &lt;code dir="ltr"&gt;0&lt;/code&gt;. When the cart is open in the sidebar, the &lt;code dir="ltr"&gt;0&lt;/code&gt;-width column transitions to &lt;code dir="ltr"&gt;calc(15em + 1cqi)&lt;/code&gt;. Since the calculation results in a normal length value, the transition can be animated from length &lt;code dir="ltr"&gt;0&lt;/code&gt;. The gap also animates from one length to another—from &lt;code dir="ltr"&gt;0&lt;/code&gt; to &lt;code dir="ltr"&gt;var(--gap)&lt;/code&gt;—which was defined earlier:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;transition&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;250&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gap&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;250&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="non-baseline_animation_enhancements" tabindex="-1"&gt;&lt;span&gt;Non-Baseline animation enhancements&lt;/span&gt;&lt;/h3&gt;

&lt;p&gt;The &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; and &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; components are also animated when hidden or shown, and this is done using two recent features that are not yet Baseline, but work as progressive enhancements for browsers that do have support:&lt;/p&gt;

&lt;ol&gt;&lt;li&gt;Applying &lt;code dir="ltr"&gt;interpolate-size: allow-keywords&lt;/code&gt; allows &lt;a href="https://developer.chrome.com/docs/css-ui/animate-to-height-auto#animate_to_and_from_intrinsic_sizing_keywords_with_interpolate-size"&gt;transitioning element dimensions from &lt;code dir="ltr"&gt;0&lt;/code&gt; to &lt;code dir="ltr"&gt;auto&lt;/code&gt;&lt;/a&gt;. This is used for transitioning the products to and from a &lt;code dir="ltr"&gt;block-size&lt;/code&gt; of &lt;code dir="ltr"&gt;0&lt;/code&gt; when they are added or removed from the cart. Since the &lt;code dir="ltr"&gt;interpolate-size&lt;/code&gt; property inherits, that only needs to be defined once on the &lt;code dir="ltr"&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element, and it is available to every other element on the page. This is only supported in Chrome-based browsers (Chrome, Edge, and others), but can be used as a progressive enhancement. The fallback works as expected, just without the animated transition.&lt;br&gt;&lt;/li&gt;
&lt;li&gt;"Discrete" properties like &lt;code dir="ltr"&gt;display&lt;/code&gt; can now be transitioned as well, even though there are no intermediate values between &lt;code dir="ltr"&gt;grid&lt;/code&gt; and &lt;code dir="ltr"&gt;none&lt;/code&gt;. Instead the transition is applied at the start or end of the duration provided. To achieve that, &lt;code dir="ltr"&gt;allow-discrete&lt;/code&gt; is added to the &lt;code dir="ltr"&gt;transition-behavior&lt;/code&gt; property. While &lt;code dir="ltr"&gt;transition-behavior&lt;/code&gt; is otherwise Baseline, animating the &lt;code dir="ltr"&gt;display&lt;/code&gt; property is not yet supported in Firefox. But again, this works well as a progressive enhancement.&lt;br&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;There are many situations where container queries and units can be used to replace media queries and viewport units, and that's great when it helps you express the intent of a design more clearly. But one is not meant to replace the other, and there's not a &lt;em&gt;better&lt;/em&gt; query or unit that will work in every situation. CSS works best when the relationships established in code match the goals and purpose of the design. When you want text and spacing that is relative to immediate context, container queries provide that functionality, but when you want consistent sizing relative to the overall viewport, media queries and viewport units are still an excellent option. Most websites will likely involve a mix of both.&lt;/p&gt;
  

  
&lt;/div&gt;

  
    
    
      
    &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://web.dev/static/articles/baseline-in-action-container-queries/image/thumbnail.png"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbc9930f7:30dbfd:20d3c2a3</id>
        <title type="html">CSS @scope: An Alternative To Naming Conventions And Heavy Abstractions</title>
        <published>2026-03-05T06:04:42Z</published>
        <updated>2026-03-05T06:04:47Z</updated>
        <link href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/" rel="alternate" type="text/html"/>
        <summary type="html">Prescriptive class name conventions are no longer enough to keep CSS maintainable in a world of increasingly complex interfaces. Can the new `@scope` rule finally give developers the confidence to write CSS that can keep up with modern front ends?</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;header&gt;&lt;h1 id="main-heading"&gt;CSS &lt;code&gt;@scope&lt;/code&gt;: An Alternative To Naming Conventions And Heavy Abstractions&lt;/h1&gt;&lt;/header&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;section&gt;Prescriptive class name conventions are no longer enough to keep CSS maintainable in a world of increasingly complex interfaces. Can the new &lt;code&gt;@scope&lt;/code&gt; rule finally give developers the confidence to write CSS that can keep up with modern front ends?&lt;/section&gt;&lt;/p&gt;&lt;p&gt;When learning the principles of basic CSS, one is taught to write modular, reusable, and descriptive styles to ensure maintainability. But when developers become involved with real-world applications, it often feels impossible to add UI features without styles leaking into unintended areas.&lt;/p&gt;&lt;p&gt;This issue often snowballs into a self-fulfilling loop; styles that are theoretically scoped to one element or class start showing up where they don’t belong. This forces the developer to create even more specific selectors to override the leaked styles, which then accidentally override global styles, and so on.&lt;/p&gt;&lt;p&gt;Rigid class name conventions, such as &lt;a href="https://getbem.com/introduction/"&gt;BEM&lt;/a&gt;, are one theoretical solution to this issue. The &lt;strong&gt;BEM (Block, Element, Modifier) methodology&lt;/strong&gt; is a &lt;a href="https://www.smashingmagazine.com/2012/04/a-new-front-end-methodology-bem/"&gt;systematic way of naming CSS classes&lt;/a&gt; to ensure reusability and structure within CSS files. Naming conventions like this can &lt;a href="https://www.smashingmagazine.com/2018/06/bem-for-beginners/"&gt;reduce cognitive load by leveraging domain language to describe elements and their state&lt;/a&gt;, and if implemented correctly, &lt;a href="https://www.smashingmagazine.com/2025/06/css-cascade-layers-bem-utility-classes-specificity-control/"&gt;can make styles for large applications easier to maintain&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;In the real world, however, it doesn’t always work out like that. Priorities can change, and with change, implementation becomes inconsistent. Small changes to the HTML structure can require many CSS class name revisions. With highly interactive front-end applications, class names following the BEM pattern can become long and unwieldy (e.g., &lt;code&gt;app-user-overview__status--is-authenticating&lt;/code&gt;), and not fully adhering to the naming rules breaks the system’s structure, thereby negating its benefits.&lt;/p&gt;&lt;p&gt;Given these challenges, it’s no wonder that developers have turned to frameworks, Tailwind being &lt;a href="https://2024.stateofcss.com/en-US/tools/"&gt;the most popular CSS framework&lt;/a&gt;. Rather than trying to fight what seems like an unwinnable specificity war between styles, it is easier to give up on the &lt;a href="https://css-tricks.com/the-c-in-css-the-cascade/"&gt;CSS Cascade&lt;/a&gt; and use tools that guarantee complete isolation.&lt;/p&gt;&lt;h2 id="developers-lean-more-on-utilities"&gt;Developers Lean More On Utilities &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#developers-lean-more-on-utilities"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;How do we know that some developers are keen on avoiding cascaded styles? It’s the rise of “modern” front-end tooling — like &lt;a href="https://www.smashingmagazine.com/2016/04/finally-css-javascript-meet-cssx/"&gt;CSS-in-JS frameworks&lt;/a&gt; — designed specifically for that purpose. Working with isolated styles that are tightly scoped to specific components can seem like a breath of fresh air. It removes the need to name things — &lt;a href="https://24ways.org/2014/naming-things/"&gt;still one of the most hated and time-consuming front-end tasks&lt;/a&gt; — and allows developers to be productive without fully understanding or leveraging the benefits of CSS inheritance.&lt;/p&gt;&lt;p&gt;But ditching the CSS Cascade comes with its own problems. For instance, composing styles in JavaScript requires heavy build configurations and often leads to styles awkwardly intermingling with component markup or HTML. Instead of carefully considered naming conventions, we allow build tools to autogenerate selectors and identifiers for us (e.g., &lt;code&gt;.jsx-3130221066&lt;/code&gt;), requiring developers to keep up with yet another pseudo-language in and of itself. (As if the cognitive load of understanding what all your component’s &lt;code&gt;useEffect&lt;/code&gt;s do weren’t already enough!)&lt;/p&gt;&lt;p&gt;Further abstracting the job of naming classes to tooling means that basic debugging is often constrained to specific application versions compiled for development, rather than leveraging native browser features that support live debugging, such as Developer Tools.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;a href="https://twitter.com/share?text=%0aIt%e2%80%99s%20almost%20like%20we%20need%20to%20develop%20tools%20to%20debug%20the%20tools%20we%e2%80%99re%20using%20to%20abstract%20what%20the%20web%20already%20provides%20%e2%80%94%20all%20for%20the%20sake%20of%20running%20away%20from%20the%20%e2%80%9cpain%e2%80%9d%20of%20writing%20standard%20CSS.%0a&amp;amp;url=https://smashingmagazine.com%2f2026%2f02%2fcss-scope-alternative-naming-conventions%2f"&gt;It’s almost like we need to develop tools to debug the tools we’re using to abstract what the web already provides — all for the sake of running away from the “pain” of writing standard CSS.&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Luckily, modern CSS features not only make writing standard CSS more flexible but also give developers like us a great deal more power to manage the cascade and make it work for us. &lt;a href="https://www.smashingmagazine.com/2022/01/introduction-css-cascade-layers/"&gt;CSS Cascade Layers&lt;/a&gt; are a great example, but there’s another feature that gets a surprising lack of attention — although that is changing now that it has recently become &lt;strong&gt;Baseline compatible&lt;/strong&gt;.&lt;/p&gt;&lt;h2 id="the-css-scope-at-rule"&gt;The CSS &lt;code&gt;@scope&lt;/code&gt; At-Rule &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#the-css-scope-at-rule"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I consider the &lt;strong&gt;CSS &lt;code&gt;@scope&lt;/code&gt; at-rule&lt;/strong&gt; to be a potential cure for the sort of style-leak-induced anxiety we’ve covered, one that does not force us to compromise native web advantages for abstractions and extra build tooling.&lt;/p&gt;&lt;blockquote&gt;“The &lt;code&gt;@scope&lt;/code&gt; CSS at-rule enables you to select elements in specific DOM subtrees, targeting elements precisely without writing overly-specific selectors that are hard to override, and without coupling your selectors too tightly to the DOM structure.”&lt;br&gt;&lt;br&gt;— &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@scope"&gt;MDN&lt;/a&gt;&lt;/blockquote&gt;&lt;p&gt;In other words, we can work with isolated styles in specific instances &lt;strong&gt;without sacrificing inheritance, cascading, or even the basic separation of concerns&lt;/strong&gt; that has been a long-running guiding principle of front-end development.&lt;/p&gt;&lt;p&gt;Plus, it has &lt;a href="https://caniuse.com/css-cascade-scope"&gt;excellent browser coverage&lt;/a&gt;. In fact, &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/146"&gt;Firefox 146&lt;/a&gt; added support for &lt;code&gt;@scope&lt;/code&gt; in December, making it &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility"&gt;Baseline compatible&lt;/a&gt; for the first time. Here is a simple comparison between a button using the BEM pattern versus the &lt;code&gt;@scope&lt;/code&gt; rule:&lt;/p&gt;&lt;p&gt;The &lt;code&gt;@scope&lt;/code&gt; rule allows for &lt;strong&gt;precision with less complexity&lt;/strong&gt;. The developer no longer needs to create boundaries using class names, which, in turn, allows them to write selectors based on native HTML elements, thereby eliminating the need for prescriptive CSS class name patterns. By simply removing the need for class name management, &lt;code&gt;@scope&lt;/code&gt; can alleviate the fear associated with CSS in large projects.&lt;/p&gt;&lt;h2 id="basic-usage"&gt;Basic Usage &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#basic-usage"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;To get started, add the &lt;code&gt;@scope&lt;/code&gt; rule to your CSS and insert a root selector to which styles will be scoped:&lt;/p&gt;&lt;p&gt;So, for example, if we were to scope styles to a &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; element, it may look something like this:&lt;/p&gt;&lt;p&gt;This, on its own, is not a groundbreaking feature. However, a second argument can be added to the scope to create a &lt;strong&gt;lower boundary&lt;/strong&gt;, effectively defining the scope’s start and end points.&lt;/p&gt;&lt;p&gt;This practice is called &lt;strong&gt;donut scoping&lt;/strong&gt;, and &lt;a href="https://css-tricks.com/solved-by-css-donuts-scopes/"&gt;there are several approaches&lt;/a&gt; one could use, including a series of similar, highly specific selectors coupled tightly to the DOM structure, a &lt;code&gt;:not&lt;/code&gt; pseudo-selector, or assigning specific class names to &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; elements within the &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; to handle the differing CSS.&lt;/p&gt;&lt;p&gt;Regardless of those other approaches, the &lt;code&gt;@scope&lt;/code&gt; method is much more concise. More importantly, it prevents the risk of broken styles if classnames change or are misused or if the HTML structure were to be modified. Now that &lt;code&gt;@scope&lt;/code&gt; is Baseline compatible, we no longer need workarounds!&lt;/p&gt;&lt;p&gt;We can take this idea further with multiple end boundaries to create a “style figure eight”:&lt;/p&gt;&lt;p&gt;Compare that to a version handled without the &lt;code&gt;@scope&lt;/code&gt; rule, where the developer has to “reset” styles to their defaults:&lt;/p&gt;&lt;p&gt;Check out the following example. Do you notice how simple it is to target some nested selectors while exempting others?&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;/p&gt;&lt;figcaption&gt;See the Pen &lt;a href="https://codepen.io/smashingmag/pen/wBWXggN"&gt;@scope example [forked]&lt;/a&gt; by &lt;a href="https://codepen.io/blakeeric"&gt;Blake Lundquist&lt;/a&gt;.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Consider a scenario where unique styles need to be applied to slotted content within &lt;a href="https://www.smashingmagazine.com/2025/07/web-components-working-with-shadow-dom/"&gt;web components&lt;/a&gt;. When slotting content into a web component, that content becomes part of the Shadow DOM, but still inherits styles from the parent document. The developer might want to implement different styles depending on which web component the content is slotted into:&lt;/p&gt;&lt;p&gt;In this example, the developer might want the &lt;code&gt;&amp;lt;user-card&amp;gt;&lt;/code&gt; to have distinct styles only if it is rendered inside &lt;code&gt;&amp;lt;team-roster&amp;gt;&lt;/code&gt;:&lt;/p&gt;&lt;h2 id="more-benefits"&gt;More Benefits &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#more-benefits"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;There are additional ways that &lt;code&gt;@scope&lt;/code&gt; can remove the need for class management without resorting to utilities or JavaScript-generated class names. For example, &lt;code&gt;@scope&lt;/code&gt; opens up the possibility to easily &lt;strong&gt;target descendants of any selector&lt;/strong&gt;, not just class names:&lt;/p&gt;&lt;p&gt;And they &lt;strong&gt;can be nested&lt;/strong&gt;, creating scopes within scopes:&lt;/p&gt;&lt;p&gt;Plus, the root scope can be easily referenced within the &lt;code&gt;@scope&lt;/code&gt; rule:&lt;/p&gt;&lt;p&gt;The &lt;code&gt;@scope&lt;/code&gt; at-rule also introduces a new &lt;strong&gt;proximity&lt;/strong&gt; dimension to CSS specificity resolution. In traditional CSS, when two selectors match the same element, the selector with the higher specificity wins. With &lt;code&gt;@scope&lt;/code&gt;, when two elements have equal specificity, the one whose scope root is closer to the matched element wins. This eliminates the need to override parent styles by manually increasing an element’s specificity, since inner components naturally supersede outer element styles.&lt;/p&gt;&lt;h2 id="conclusion"&gt;Conclusion &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#conclusion"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Utility-first CSS frameworks, such as Tailwind, work well for prototyping and smaller projects. Their benefits quickly diminish, however, when used in larger projects involving more than a couple of developers.&lt;/p&gt;&lt;p&gt;Front-end development has become increasingly overcomplicated in the last few years, and CSS is no exception. While the &lt;code&gt;@scope&lt;/code&gt; rule isn’t a cure-all, it can reduce the need for complex tooling. When used in place of, or alongside strategic class naming, &lt;code&gt;@scope&lt;/code&gt; can make it easier and more fun to write maintainable CSS.&lt;/p&gt;&lt;h3 id="further-reading"&gt;Further Reading &lt;a href="https://www.smashingmagazine.com/2026/02/css-scope-alternative-naming-conventions/#further-reading"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;img src="https://www.smashingmagazine.com/images/logo/logo--red.png" alt="Smashing Editorial"&gt;&lt;span&gt;(gg, yk)&lt;/span&gt;&lt;/div&gt;&lt;p&gt;&lt;span&gt;Explore more on&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>About The Author</name>
        </author>
        <media:content medium="image" url="https://files.smashing.media/articles/css-scope-alternative-naming-conventions/css-scope-alternative-naming-conventions.jpg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://www.smashingmagazine.com/feed/</id>
            <title type="html">Smashing Magazine</title>
            <link href="https://www.smashingmagazine.com" rel="alternate" type="text/html"/>
            <updated>2026-03-05T06:04:47Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cba61ae76:123cf3:98d097d6</id>
        <title type="html">Your skip link targets don't need tabindex=-1 to work properly</title>
        <published>2026-03-04T19:44:49Z</published>
        <updated>2026-03-04T19:44:54Z</updated>
        <link href="https://matuzo.at/blog/2026/skip-links-tabindex" rel="alternate" type="text/html"/>
        <summary type="html">I'm a frontend developer in Graz, specialized in HTML, accessibility, and CSS layout and architecture.</summary>
        <content type="html">
&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;article&gt;&lt;h1&gt;Your skip link targets don't need tabindex=-1 to work properly&lt;/h1&gt;
&lt;p&gt;
posted on &lt;time datetime="2026-03-04"&gt;04.03.2026&lt;/time&gt;&lt;/p&gt;
&lt;p&gt;
&lt;/p&gt;
&lt;p&gt;Recently, someone posted on LinkedIn that skip links are often broken because their target elements are missing a &lt;code&gt;tabindex&lt;/code&gt; attribute. I was really surprised to see that because I thought that was an issue of the past. That's why I decided to test it.&lt;/p&gt; &lt;h2&gt;The original problem&lt;/h2&gt;
&lt;p&gt;The author explained in their post that when you use a keyboard and press Enter on a skip link, the page scrolls down to the target, but focus stays on the link. When you press Tab, focus doesn't jump to the target; it jumps to the next focusable element after the skip link. He calls that a “phantom jump”.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&amp;lt;a href=&amp;quot;#content&amp;quot;&gt;Jump to content&amp;lt;/a&gt;
&amp;lt;nav&gt;
    &amp;lt;!-- A bunch of links --&gt;
&amp;lt;/nav&gt;
&amp;lt;main id=&amp;quot;content&amp;quot;&gt;
&amp;lt;/main&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They explain that the problem is that the main element is not an interactive element and thus cannot be focused. That's why their proposed solution is adding a &lt;code&gt;tabindex&lt;/code&gt; attribute.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&amp;quot;#content&amp;quot;&gt;Jump to content&amp;lt;/a&gt;
&amp;lt;nav&gt;
    &amp;lt;!-- A bunch of links --&gt;
&amp;lt;/nav&gt;
&amp;lt;main id=&amp;quot;content&amp;quot; tabindex=&amp;quot;-1&amp;quot;&gt;
&amp;lt;/main&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What the author describes was a problem… a long time ago. As &lt;a href="https://sarahmhigley.com/writing/focus-navigation-start-point/#assistive-tech-support"&gt;Sara Higley reconstructs&lt;/a&gt;, Firefox was the first browser to fix it in 2013, followed by Internet Explorer, and finally Chrome in 2016. I don't know about Safari, but it was long enough ago that I can't remember anymore.&lt;/p&gt;
&lt;h2&gt;The solution&lt;/h2&gt;
&lt;p&gt;The fix browsers implemented is a feature called &lt;a href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"&gt;“sequential focus navigation starting point (SFNSP)”&lt;/a&gt;. Rob Dodson defines it well in his post &lt;a href="https://developer.chrome.com/blog/focus-start-point"&gt;“Remove headaches from focus management”.&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The 'sequential focus navigation starting point' feature defines where we start to search for focusable elements for sequential focus navigation (Tab or Shift-Tab) when there is no focused area.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Prior to that, there were only two possible starting points. &lt;/p&gt;
&lt;ul&gt;&lt;li&gt;If there is no other starting point, it's the &lt;code&gt;document&lt;/code&gt; or, if available, the currently active &lt;code&gt;dialog&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If an element is focused, it's also the starting point &lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;With SFNSP, there can now be more starting points.&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Navigating to a page fragment, e.g., by clicking a skip link, sets the starting point.&lt;/li&gt;
&lt;li&gt;Clicking any element on the page, interactive or not, sets the starting point.&lt;/li&gt;
&lt;li&gt;If an element that was the starting point is removed from the DOM, its parent becomes the starting point.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;That was a necessary addition because it fixed several fundamental accessibility issues. As mentioned, it's been available in all major browsers for many years and works well. Nevertheless, I want to back that with actual tests. In this blog post, I want to focus only on the scenario where navigating to a page fragment sets the starting point.&lt;/p&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;p&gt;For my test, I created a &lt;a href="https://codepen.io/matuzo/pen/pvEgaEq"&gt;simple page&lt;/a&gt;. My goal was to confirm three outcomes:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;After pressing Enter on the skip link, then pressing Tab, I would land on the “Content” button using a keyboard.&lt;/li&gt;
&lt;li&gt;After pressing Enter on the skip link, then pressing Tab, I would land on the “Content” button using a keyboard with a screen reader.&lt;/li&gt;
&lt;li&gt;After pressing Enter on the skip link, I would use the virtual cursor with a screen reader to move to the next element and land on the “Content” button or the h2.&lt;/li&gt;
&lt;/ul&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;header&gt;
  &amp;lt;a href=&amp;quot;#content&amp;quot;&gt;Skip&amp;lt;/a&gt;
  &amp;lt;button&gt;1&amp;lt;/button&gt;
  &amp;lt;button&gt;2&amp;lt;/button&gt;
  &amp;lt;button&gt;3&amp;lt;/button&gt;
  &amp;lt;button&gt;4&amp;lt;/button&gt;
  &amp;lt;button&gt;5&amp;lt;/button&gt;
&amp;lt;/header&gt;

&amp;lt;main id=&amp;quot;content&amp;quot;&gt;
  &amp;lt;h2&gt;Main content&amp;lt;/h2&gt;
  &amp;lt;button&gt;Content&amp;lt;/button&gt;
&amp;lt;/main&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was able to confirm all three scenarios in all tested browsers.&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Keyboard, Firefox 157, macOS 26.3&lt;/li&gt;
&lt;li&gt;Keyboard, Chrome 145, macOS 26.3&lt;/li&gt;
&lt;li&gt;Keyboard, Safari, macOS 26.3&lt;/li&gt;
&lt;li&gt;Keyboard Windows 11, Chrome 145&lt;/li&gt;
&lt;li&gt;Keyboard Windows 11, Edge 145&lt;/li&gt;
&lt;li&gt;Keyboard, Windows 11, Firefox 147&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, VoiceOver, macOS 26.3, Safari&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, JAWS 2026, Windows 11, Chrome 145&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, NVDA 2025.3.3, Windows 11, Firefox 147&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, NVDA 2025.3.3, Windows 11, Chrome145&lt;/li&gt;
&lt;li&gt;Tab key / Virtual cursor, Narrator, Windows 11, Edge/Chrome 145&lt;/li&gt;
&lt;li&gt;Talkback, Android 16, Chrome 145&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;Styling&lt;/h2&gt;
&lt;p&gt;Navigating to a non-interactive page fragment doesn't reveal the element's focus styling, because it's not focused, it's only a starting point. We still have options to style a targeted element in CSS.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:target {
  outline: 2px solid #ccc;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Unless I'm missing something (please &lt;a href&gt;let me know&lt;/a&gt;), I would say it's safe to remove &lt;code&gt;tabindex=&amp;quot;-1&amp;quot;&lt;/code&gt; from your skip link targets.&lt;br&gt;
I didn't link to the post on LinkedIn because I didn't want to callout the author. It's not about them being wrong, but about the fact that browsers and screen readers receive frequent updates. That's why it's important that you reevaluate the best practices you've been following for years every now and then and check if they still apply. Often, things that were true a couple of years ago aren't true today.&lt;/p&gt; &lt;/article&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</content>
        <author>
            <name>Manuel Matuzović</name>
        </author>
        <media:content medium="image" url="https://res.cloudinary.com/dp3mem7or/image/upload/w_1200/articles/sm_target-tabindex.png?s=213"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cba36ebfe:100ae8:aed68aa9</id>
        <title type="html">Claude is an Electron App because we’ve lost native</title>
        <published>2026-03-04T18:58:07Z</published>
        <updated>2026-03-04T18:58:11Z</updated>
        <link href="https://tonsky.me/blog/fall-of-native/" rel="alternate" type="text/html"/>
        <summary type="html">Article argues that Claude is not an Electron app not because LLMs can’t do it, but because there are no advantages left for native</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;article&gt;&lt;h1&gt;Claude is an Electron App because we’ve lost native&lt;/h1&gt;
        &lt;p&gt;In &lt;a href="https://www.dbreunig.com/2026/02/21/why-is-claude-an-electron-app.html" target="_blank"&gt;“Why is Claude an Electron App?”&lt;/a&gt; Drew Breunig wonders:&lt;/p&gt;
        &lt;blockquote&gt;
          &lt;p&gt;Claude spent $20k on an agent swarm implementing (kinda) a C-compiler in Rust, but desktop Claude is an Electron app.&lt;/p&gt;
          &lt;p&gt;If code is free, why aren’t all apps native?&lt;/p&gt;
        &lt;/blockquote&gt;
        &lt;p&gt;And then argues that the answer is that LLMs are not good enough yet. They can do 90% of the work, so there’s still a substantial amount of manual polish, and thus, increased costs.&lt;/p&gt;
        &lt;p&gt;But I think that’s not the real reason. The real reason is: native has nothing to offer.&lt;/p&gt;
        &lt;p&gt;API-wise, native apps lost to web apps a long time ago. Native APIs are terrible to use, and OS vendors use everything in their power to make you not want to develop native apps for their platform. That explains the rise of Electron before LLM times, but it’s also a problem that LLMs solve now: if that was a real barrier to developing native apps, it doesn’t exist anymore.&lt;/p&gt;
        &lt;p&gt;Then there’re looks and consistency. Some time ago, maybe in the late 90s and 2000s, native was ahead. It used to look good, it was consistent, and it all actually worked: the more apps used native look and feel, the better user experience was across apps (which we used to call programs). &lt;/p&gt;
        &lt;p&gt;These days, though, native is as bad as the web, if not worse. Consistency is basically out the window. Anything can look like anything, buttons have no borders, contrast doesn’t exist, and neither do conventions. Apple, for example, seems to place traffic lights and corner radius by vibes rather than by any measurable guidelines.&lt;/p&gt;
        &lt;figure&gt;&lt;img src="https://tonsky.me/blog/fall-of-native/radii@2x.webp?t=1772581463"&gt;&lt;figcaption&gt;&lt;a href="https://x.com/vaxryy/status/1977175437382930662" target="_blank"&gt;Maybe the server should round the corners?&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Looks could be good, but they also can be bad, and then you are stuck with platform-consistent, but generally bad UI (Liquid Glass ahem). It changes too often, too: the app you made today will look out of place next year, when Apple decides to change look and feel yet again. There’s no native look anymore.&lt;/p&gt;
        &lt;figure&gt;&lt;img src="https://tonsky.me/blog/fall-of-native/run@2x.webp?t=1772581463"&gt;&lt;figcaption&gt;&lt;a href="https://grumpy.website/1723" target="_blank"&gt;Computer UIs also degrade over time&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Theoretically, native apps can integrate with OS on a deeper level. This sounds nice, but what does that mean in practice? There are almost no good interoperable file formats; everything is locked inside individual apps, most services moved to the web, and OSes dropped the ball for making a good shared baseline. You can integrate with OS-provided calendar, but you can’t do it with web calendar. Well, you can, of course, but it’s easier on the web; native doesn’t help with it at all.&lt;/p&gt;
        &lt;figure&gt;&lt;img src="https://tonsky.me/blog/fall-of-native/calendar@2x.webp?t=1772581463"&gt;&lt;figcaption&gt;&lt;a href="https://grumpy.website/1747" target="_blank"&gt;Web pages only lead to more web pages&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Finally, the last hope of people longing for native is performance. They feel that native apps will be faster. Well, they can, but it doesn’t mean they will. Web apps can be faster, too, but in practice, nobody cares. There’s no technical reason why &lt;a href="https://tonsky.me/blog/js-bloat/"&gt;Slack needs to load 80 MiB&lt;/a&gt; just to show 10 channel names and 3 messages on a screen. The web is not the problem here! It’s a choice to be bad. What makes you think it’ll be different once the company decides to move to native?&lt;/p&gt;
        &lt;p&gt;Don’t get me wrong: writing this brings me no joy. I don’t think web is a solution either. I just remember good times when native did a better-than-average job, and we were all better for using it, and it saddens me that these times have passed.&lt;/p&gt;
        &lt;p&gt;I just don’t think that kidding ourselves that the only problem with software is Electron and it all will be butterflies and unicorns once we rewrite Slack in SwiftUI is not productive. The real problem is a lack of care. And the slop; you can build it with any stack.&lt;/p&gt;
        
      &lt;/article&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Nikita Prokopov</name>
        </author>
        <media:content medium="image" url="https://dynogee.com/gen?id=nm509093bpj50lv&amp;title=Claude+is+an+Electron+App+because+we%E2%80%99ve+lost+native"/>
        <link href="https://dynogee.com/gen?id=24m2qx9uethuw6p&amp;title=Claude+is+an+Electron+App+because+we%E2%80%99ve+lost+native" rel="enclosure" type="image"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://tonsky.me/atom.xml</id>
            <title type="html">tonsky.me</title>
            <link href="https://tonsky.me" rel="alternate" type="text/html"/>
            <updated>2026-03-04T18:58:11Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cad6bfca5:b057c8:14c12a2</id>
        <title type="html">Your car’s tire sensors could be used to track you</title>
        <published>2026-03-02T07:21:01Z</published>
        <updated>2026-03-02T07:21:06Z</updated>
        <link href="https://networks.imdea.org/your-cars-tire-sensors-could-be-used-to-track-you/" rel="alternate" type="text/html"/>
        <summary type="html">Researchers at IMDEA Networks Institute, together with European partners, have found that tire pressure sensors in modern cars can unintentionally expose drivers to tracking. Over a ten-week study, they collected signals from more than 20,000 vehicles, revealing a hidden privacy risk and highlighting the need for stronger security measures in future vehicle sensor systems. Most...</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div id="single_title"&gt;

								&lt;h1&gt;Your car’s tire sensors could be used to track you&lt;/h1&gt;&lt;h4&gt;Researchers at IMDEA Networks show standard tire sensors can expose drivers’ movements, raising privacy concerns&lt;/h4&gt;								&lt;p&gt;&lt;span id="date"&gt;25 February 2026&lt;/span&gt;&lt;/p&gt;
								

																



								&lt;p&gt;&lt;span lang="EN-US"&gt;Researchers at IMDEA Networks Institute, together with European partners, have found that &lt;strong&gt;tire pressure sensors in modern cars can unintentionally expose drivers to tracking&lt;/strong&gt;. Over a ten-week study, they collected signals from more than 20,000 vehicles, revealing a hidden privacy risk and highlighting the need for stronger security measures in future vehicle sensor systems.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;Most modern cars are equipped with a &lt;strong&gt;Tire Pressure Monitoring System (TPMS)&lt;/strong&gt;, mandatory since the late 2000s in many countries for their contribution to road safety. This system uses small sensors in each wheel to monitor tire pressure and sends wireless signals to the car’s computer to alert the driver if a tire is underinflated.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;However, &lt;strong&gt;the researchers found that these tire sensors also send a unique ID number in clear, unencrypted wireless signals&lt;/strong&gt;, meaning that anyone nearby with a simple radio receiver can capture the signal, and recognize the same car again later. Most vehicle tracking today uses cameras that need clear visibility and line-of-sight to a car. TPMS tracking is different: tire sensors automatically send radio signals that pass through walls and vehicles, allowing small hidden wireless receivers to capture them without being seen. Because each sensor broadcasts a fixed unique ID, the same car can be recognized repeatedly without reading a license plate. &lt;strong&gt;This makes TPMS-based tracking cheaper, harder to detect&lt;/strong&gt;, and more difficult to avoid than camera-based surveillance, and therefore a stronger privacy threat. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;To test how serious this risk is, the team built a network of low-cost radio receivers, located near roads and parking areas. The necessary equipment costs only $100 per receiver. In total, they collected more than six million tire sensor messages from over 20,000 cars. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;“Our results show that these tire sensor signals can be used to follow vehicles and learn their movement patterns,” says &lt;strong&gt;Domenico Giustiniano&lt;/strong&gt;, Research Professor at IMDEA Networks Institute. “This means a network of inexpensive wireless receivers could quietly monitor the patterns of cars in real-world environments. Such information could reveal daily routines, such as work arrival times or travel habits.”&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;The researchers also &lt;strong&gt;developed methods to match signals from the four tires of a car&lt;/strong&gt;. This allowed them to increase the accuracy of specific vehicles arriving, living, or following regular schedules. The study showed that signals can be captured from moving cars and from distances greater than 50 meters, even when sensors are inside buildings or hidden locations. This makes covert tracking technically feasible. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;Additionally, TPMS signals include tire pressure readings, which may reveal the type of vehicle or whether a car or truck is carrying heavy loads. This could allow more advanced forms of surveillance.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;“As vehicles become increasingly connected, &lt;strong&gt;even safety-oriented sensors like TPMS should be designed with security in mind&lt;/strong&gt;, since data that appears passive and harmless can become a powerful identifier when collected at scale,” highlights &lt;strong&gt;Dr.&lt;/strong&gt; &lt;strong&gt;Alessio Scalingi&lt;/strong&gt;, former PhD student at IMDEA Networks and now Assistant Professor at UC3M, Madrid.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;Despite these risks, current vehicle cybersecurity regulations do not yet specifically address TPMS security. The researchers warn that without encryption or authentication, tire sensors remain an easy target for passive surveillance.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;“TPMS was designed for safety, not security,” adds &lt;strong&gt;Dr. Yago Lizarribar&lt;/strong&gt;, former PhD student at IMDEA Networks during the research study, and now Researcher at Armasuisse, Switzerland. “&lt;strong&gt;Our findings show the need for manufacturers and regulators to improve protection&lt;/strong&gt; in future vehicle sensor systems.”&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;Therefore, the research team urges the manufacturers and policymakers to strengthen cybersecurity in future cars, so that safety systems do not become tools for tracking the population. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span lang="EN-US"&gt;The paper, titled “&lt;a href="https://dspace.networks.imdea.org/handle/20.500.12761/2011" target="_blank" rel="noopener"&gt;Can’t Hide Your Stride: Inferring Car Movement Patterns from Passive TPMS Measurements&lt;/a&gt;,” has been accepted for publication at IEEE WONS 2026.&lt;/span&gt;&lt;/p&gt;



								&lt;div id="taxonomy"&gt;

									&lt;p&gt;Source(s): &lt;span&gt;IMDEA Networks Institute&lt;/span&gt;&lt;br&gt;&lt;/p&gt;

									
									
								&lt;/div&gt;
								
								
							&lt;/div&gt;

						
					&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Marta Dorado</name>
        </author>
        <media:content medium="image" url="https://networks.imdea.org/wp-content/uploads/2026/02/media-file-parking-lot-1024x576.png"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19ca0820f80:1a9cfd:f46e742f</id>
        <title type="html">Please, please, please stop using passkeys for encrypting user data</title>
        <published>2026-02-27T19:10:04Z</published>
        <updated>2026-02-27T19:10:07Z</updated>
        <link href="https://blog.timcappalli.me/p/passkeys-prf-warning/" rel="alternate" type="text/html"/>
        <summary type="html">Passkeys are the future of authentication, but using them for data encryption is a disaster waiting to happen. Overloading these credentials creates a dangerous blast radius that can lead to the irreversible loss of a user's most sacred memories and documents.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;Why am I writing this today?
Because I am deeply concerned about users losing their most sacred data.&lt;/p&gt;&lt;p&gt;Over the past year or two, I’ve seen many organizations, large and small, implement passkeys (which is great, thank you!) and use the PRF (Pseudo-Random Function) extension to derive keys to protect user data, typically to support end-to-end encryption (including backups).
I’ve also seen a number of influential folks and organizations promote the use of PRF for encrypting data.&lt;/p&gt;&lt;p&gt;The primary use cases I’ve seen implemented or promoted so far include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;encrypting message backups (including images and videos)&lt;/li&gt;&lt;li&gt;end-to-end encryption&lt;/li&gt;&lt;li&gt;encrypting documents and other files&lt;/li&gt;&lt;li&gt;encrypting and unlocking crypto wallets&lt;/li&gt;&lt;li&gt;credential manager unlocking&lt;/li&gt;&lt;li&gt;local account sign in&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Why is this a problem?&lt;/p&gt;&lt;p&gt;When you overload a credential used for authentication by also using it for encryption, the “blast radius” for losing that credential becomes immeasurably larger.&lt;/p&gt;&lt;p&gt;Imagine a user named Erika. They are asked to set up encrypted backups in their favorite messaging app because they don’t want to lose their messages and photos, especially those of loved ones who are no longer here.
Erika is prompted to use their passkey to enable these backups.&lt;/p&gt;&lt;p&gt;There is nothing in the UI that emphasizes that these backups are now tightly coupled to their passkey. Even if there were explanatory text, Erika, like most users, doesn’t typically read through every dialog box, and they certainly can’t be expected to remember this technical detail a year from now.&lt;/p&gt;&lt;p&gt;A few months pass, and Erika decides to clean up their credential manager. They don’t remember why they had a specific passkey for a messaging app and deletes it.&lt;/p&gt;&lt;p&gt;Fast forward a year: they get a new phone and set up the messaging app. They aren’t prompted to use a passkey because one no longer exists in their credential manager. Instead, they use phone number verification to recover their account. They are then guided through the “restore backup” flow and prompted for their passkey.&lt;/p&gt;&lt;p&gt;Since they no longer have it, they are informed that they cannot access their backed up data. Goodbye, memories.&lt;/p&gt;&lt;p&gt;Here’s a few examples of what a user sees when they delete a passkey:&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt="Apple Passwords delete passkey" src="https://blog.timcappalli.me/p/passkeys-prf-warning/delete-ap_hu_2f537039f66a265f.png"&gt;&lt;figcaption&gt;Example: deleting a passkey in Apple Passwords&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt="Google Password Manager delete passkey" src="https://blog.timcappalli.me/p/passkeys-prf-warning/delete-gpm_hu_999e554d404d2d82.png"&gt;&lt;figcaption&gt;Example: deleting a passkey in Google Password Manager&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure&gt;&lt;img alt="Bitwarden delete passkey" src="https://blog.timcappalli.me/p/passkeys-prf-warning/delete-bw_hu_ad31ff008fd77df7.png"&gt;&lt;figcaption&gt;Example: deleting a passkey in Bitwarden&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;How is a user supposed to understand that they are potentially blowing away photos of deceased relatives, an encrypted property deed, or their digital currency?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;We cannot, and should not, expect users to know this.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;At this point, you may be asking why PRF is part of WebAuthn in the first place.
There are some very legitimate and more durable uses of PRF in WebAuthn, specifically supporting credential managers and operating systems.&lt;/p&gt;&lt;p&gt;A passkey with PRF can make unlocking your credential manager (where all of your other passkeys and credentials are stored) much faster and more secure.
Credential managers have robust mechanisms to protect your vault data with multiple methods, such as master passwords, per-device keys, recovery keys, and social recovery keys.
Losing access to a passkey used to unlock your credential manager rarely leads to complete loss of your vault data.&lt;/p&gt;&lt;p&gt;PRF is already implemented in WebAuthn Clients and Credential Managers, so the cat is out of the bag. My asks:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;To the wider identity industry&lt;/strong&gt;: &lt;strong&gt;&lt;em&gt;please stop promoting and using passkeys to encrypt user data. I’m begging you. Let them be great, phishing-resistant authentication credentials.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;To &lt;strong&gt;credential managers&lt;/strong&gt;: please prioritize adding warnings for users when they delete a passkey with PRF (and &lt;a href="https://www.w3.org/TR/passkey-endpoints/#usage" target="_blank" rel="noreferrer"&gt;displaying the RP’s info page when available&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;To &lt;strong&gt;sites and services using passkeys&lt;/strong&gt;: if you still need to use PRF knowing these concerns, please:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;add an informational page to your support site explaining how you’re using passkeys for more than authentication&lt;/li&gt;&lt;li&gt;list that page URL in the &lt;a href="https://www.w3.org/TR/passkey-endpoints/#usage" target="_blank" rel="noreferrer"&gt;Well-Known URL for Relying Party Passkey Endpoints (&lt;code&gt;prfUsageDetails&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;&lt;li&gt;provide as much warning as possible up front to users when enabling it&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Thanks for reading! &#128591;&#127995;&lt;/p&gt;&lt;p&gt;(and thanks to &lt;a href="https://blog.millerti.me/" target="_blank" rel="noreferrer"&gt;Matthew Miller&lt;/a&gt; for reviewing and providing feedback on this post)&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Tim Cappalli</name>
        </author>
        <media:content medium="image" url="https://blog.timcappalli.me/p/passkeys-prf-warning/featured.jpg"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c94728918:7e51:7c94cf89</id>
        <title type="html">How &amp;quot;Liquid Design&amp;quot; Broke the iPhone and Forced Apple’s Great Reset</title>
        <published>2026-02-25T10:57:40Z</published>
        <updated>2026-02-25T10:57:43Z</updated>
        <link href="https://webdesignerdepot.com/how-liquid-design-broke-the-iphone-and-forced-apples-great-reset/" rel="alternate" type="text/html"/>
        <summary type="html">Apple’s &amp;quot;Liquid Glass&amp;quot; experiment has officially shattered, proving that obsession with aesthetics over usability is a billion-dollar mistake. As iOS 26 drains batteries and kills accessibility, a massive internal &amp;quot;Solid Design&amp;quot; rescue mission is underway to save the iPhone from its own ego.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;The launch of &lt;strong&gt;iOS 26&lt;/strong&gt; was heralded as a “Spatial Awakening.” Apple’s design team, led by the high-concept vision of Alan Dye, promised to bridge the gap between our physical reality and our digital tools through a language called &lt;strong&gt;Liquid Glass&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;It was marketed as an interface that breathes—a hyper-dynamic, translucent, and motion-heavy UI that used real-time refraction to make your apps look like they were floating in a physical pane of crystal.&lt;/p&gt;&lt;p&gt;But six months into the lifecycle of iOS 26, the verdict is in from power users, developers, and accessibility advocates alike: &lt;strong&gt;Liquid Glass is a usability catastrophe.&lt;/strong&gt; &lt;/p&gt;&lt;p&gt;It is the most arrogant piece of software Apple has ever shipped—a system that demands you admire its beauty while it actively hinders your ability to use your phone. It is a house of mirrors built on the bones of a once-efficient operating system, and its failure has forced the most radical leadership shakeup in Cupertino since the departure of Jony Ive.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Death of Legibility: A Low-Vision Nightmare&lt;/h2&gt;&lt;p&gt;The most fundamental job of a user interface is to be readable. Liquid Glass fails this at a level that would get a first-year design student expelled. By prioritizing “refractive realism,” Apple replaced solid containers with semi-transparent, light-bending “panes.”&lt;/p&gt;&lt;p&gt;The problem is that the real world is messy. When your notification center is refracting a high-detail photo of a forest or a bright sunset, the text “loses the fight” for your attention. This has created a &lt;strong&gt;Contrast Crisis&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Apple’s AI tries to dynamically shift text color based on the background, but it frequently fails on busy images, leading to a “muddy” effect where text becomes functionally invisible.&lt;/p&gt;&lt;p&gt;For users with visual impairments—or even just tired eyes—the constant shifting of translucent layers creates a barrier to entry. We are now in an era where “Reduce Transparency” isn’t just an accessibility option; it is a mandatory setting for anyone who wants to read their messages in direct sunlight.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Performance Penalty: GPU-Driven Ego&lt;/h2&gt;&lt;p&gt;Liquid Glass treats the iPhone UI like a AAA video game. Every time you swipe, the system calculates real-time ray-traced reflections and “chromatic aberration” on the edges of your app icons. It is a staggering waste of compute power that has introduced a level of friction we haven’t seen in years.&lt;/p&gt;&lt;p&gt;Even on the powerhouse &lt;strong&gt;iPhone 17 Pro&lt;/strong&gt;, the interface suffers from “micro-stutters.” The moment the GPU throttles due to heat or background tasks, the “liquid” illusion breaks, leaving the user with a laggy, disconnected experience.&lt;/p&gt;&lt;p&gt;Furthermore, the &lt;strong&gt;Battery Tax&lt;/strong&gt; has been devastating. Data suggests a 10–15% reduction in real-world screen-on time because the phone is effectively running a physics engine just to show you your calendar. We are using 3-nanometer chips—miracles of human engineering—to render “shimmers” on icons that were perfectly functional as flat squares.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Uncanny Valley of Physics&lt;/h2&gt;&lt;p&gt;In 2013, iOS 7 killed skeuomorphism because we no longer needed digital objects to look like leather or felt. Liquid Glass is a return to a different kind of realism—&lt;strong&gt;Digital Skeuomorphism&lt;/strong&gt;. Apple tried to make us believe we are touching floating panes of glass, but when the physics don’t match the tactile reality, it falls into an uncanny valley.&lt;/p&gt;&lt;p&gt;The “wobbly” sliders and “floaty” icons lack the weight and intent that made the original iPhone feel like a precision tool. When you move a slider in the Control Center and it stretches like digital gelatin, it feels flimsy and unserious.&lt;/p&gt;&lt;p&gt;It is “Barbie-fied” tech—glossy, plastic, and fundamentally performative. As critic John Gruber noted, Apple spent billions making the thinnest hardware in the world only to put a UI on it that looks like a Windows Vista fever dream.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Alan Dye Departure and the Internal Revolt&lt;/h2&gt;&lt;p&gt;The departure of &lt;strong&gt;Alan Dye&lt;/strong&gt; to Meta in late 2025 was the ultimate confirmation that the “Liquid” experiment had reached its dead end.&lt;/p&gt;&lt;p&gt;Reports from within Apple Park suggest that Dye’s team frequently brushed aside warnings from accessibility and performance engineers to maintain the “purity” of the aesthetic.&lt;/p&gt;&lt;p&gt;When Dye left, he left behind a UI that looked stunning in a 4K keynote presentation but felt “restless and needy” in the hands of a real person. The fact that Apple had to rush out &lt;strong&gt;iOS 26.2&lt;/strong&gt; with an “Opaque Mode” was a silent admission of failure.&lt;/p&gt;&lt;p&gt;Apple doesn’t add “undo” sliders to its core vision unless that vision is actively breaking the user experience.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;The Rescue Mission: Stephen Lemay and “Solid Design”&lt;/h2&gt;&lt;p&gt;The keys to the iPhone’s soul have now been handed to &lt;strong&gt;Stephen Lemay&lt;/strong&gt;, a 26-year veteran of the original Aqua design era. Lemay’s mission for &lt;strong&gt;iOS 27&lt;/strong&gt; is reportedly a “Snow Leopard” style intervention—a radical “taming” of the interface focused on three pillars:&lt;/p&gt;&lt;ol start="1" class="wp-block-list"&gt;&lt;li&gt;&lt;strong&gt;The Return of Opacity:&lt;/strong&gt; Moving away from high-refraction backgrounds to solid, high-contrast containers that prioritize legibility.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;GPU Idle Recovery:&lt;/strong&gt; Targeting a 40% reduction in baseline GPU activity by stripping away real-time physics from static screens.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Edge Definition:&lt;/strong&gt; Reintroducing high-contrast borders and “Physical Depth” to ensure the eye never has to “hunt” for a button.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The shift under Lemay is philosophical. Under Dye, Apple design became performative—it wanted you to look &lt;em&gt;at&lt;/em&gt; the glass. Under Lemay, the goal is for the design to disappear again.&lt;/p&gt;&lt;h2 class="wp-block-heading"&gt;Conclusion: Form Follows Nothing&lt;/h2&gt;&lt;p&gt;Liquid Glass failed because it forgot why people buy iPhones. We don’t buy them to look at a digital art gallery; we buy them to get things done. By turning the interface into an obstacle course of reflections and blurs, Apple traded &lt;strong&gt;utility&lt;/strong&gt; for &lt;strong&gt;vanity&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;It was a beautiful mistake, a technical marvel that served no master other than the ego of the designers who built it.&lt;/p&gt;&lt;p&gt;As we look toward the “Solid Design” of iOS 27, Liquid Glass will likely be remembered alongside the “butterfly keyboard”—a time when Apple got so caught up in &lt;em&gt;how&lt;/em&gt; they could build something, they forgot to ask &lt;em&gt;if&lt;/em&gt; they should.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;Noah Davis is an accomplished UX strategist with a knack for blending innovative design with business strategy. With over a decade of experience, he excels at crafting user-centered solutions that drive engagement and achieve measurable results.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Noah Davis</name>
        </author>
        <media:content medium="image" url="https://d3tamksjp7q04h.cloudfront.net/2026/02/13144445/Screenshot-2026-02-13-at-11.44.24-AM.jpeg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://webdesignerdepot.com/feed/</id>
            <title type="html">Web Designer Depot</title>
            <link href="https://webdesignerdepot.com" rel="alternate" type="text/html"/>
            <updated>2026-02-25T10:57:43Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c946c2b85:31b24:c24964f6</id>
        <title type="html">Precompressed HTML at the Edge: Eleventy Meets Cloudflare Workers</title>
        <published>2026-02-25T10:50:42Z</published>
        <updated>2026-02-25T10:50:47Z</updated>
        <link href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/" rel="alternate" type="text/html"/>
        <summary type="html">In this post, I will show you how I integrate Brotli level 11 compression directly into my 11ty build process to squeeze every possible byte out of my blog’s HTML.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#introduction"&gt;&lt;span&gt;Jump to section titled: Introduction&lt;/span&gt;&lt;/a&gt;&lt;p&gt;In 2025, I wrote a series of web performance optimisation blog posts focussing on some of the key fundamental's of Frontend Web Performance:&lt;/p&gt;&lt;h3 id="caching"&gt;Caching&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#caching"&gt;&lt;span&gt;Jump to section titled: Caching&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;a href="https://nooshu.com/blog/2025/09/02/asset-fingerprinting-and-the-preload-response-header-in-11ty/"&gt;Asset fingerprinting and the preload response header in 11ty&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The blog post describes how the I enhanced web performance on my 11ty-built site by combining asset fingerprinting with the HTTP preload hint.&lt;/p&gt;&lt;p&gt;It explains that preload tells the browser to fetch critical resources earlier, but hashed filenames make this difficult to manage manually.&lt;/p&gt;&lt;p&gt;The solution was to generate preload Link headers automatically during the 11ty build. A custom script locates the fingerprinted CSS file and injects the correct preload header into the Cloudflare Pages &lt;code&gt;_headers&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;This speeds up CSS delivery, removes the need for manual updates, and allows the use of long-lived &lt;code&gt;Cache-Control&lt;/code&gt; header values such as &lt;code&gt;max-age=31536000&lt;/code&gt; and &lt;code&gt;immutable&lt;/code&gt;.&lt;/p&gt;&lt;h3 id="compression"&gt;Compression&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#compression"&gt;&lt;span&gt;Jump to section titled: Compression&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;a href="https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/"&gt;Cranking Brotli up to 11 with Cloudflare Pro and 11ty&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The blog post explains how I improved the performance of my 11ty-powered blog after migrating to Cloudflare Pages by using Brotli compression at the highest level (11) for static assets. I also describes the difference between Brotli and gzip, outline how Cloudflare’s Pro plan typically applies a moderate Brotli level (4), and then show how to pre-compress JavaScript files to Brotli level 11. Lastly, serve them correctly both locally and via Cloudflare, and configure Cloudflare’s compression rules so that all assets benefit from the stronger compression to reduce file sizes and improve load performance.&lt;/p&gt;&lt;h3 id="concatenation"&gt;Concatenation&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#concatenation"&gt;&lt;span&gt;Jump to section titled: Concatenation&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;a href="https://nooshu.com/blog/2025/01/12/using-an-11ty-shortcode-to-craft-a-custom-css-pipeline/"&gt;Using an 11ty Shortcode to craft a custom CSS pipeline&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;While not strictly about concatenation, it covers related ideas. The post explains how I built a custom CSS pipeline for my 11ty site using a bespoke short code rather than the default bundle plugin.&lt;/p&gt;&lt;p&gt;It details how I preserved local live reload, added content-based fingerprinting for long-term caching, minified CSS with &lt;a href="https://www.npmjs.com/package/clean-css"&gt;clean-css package&lt;/a&gt;, enabled Brotli compression, and used disk and memory caching to prevent unnecessary work.&lt;/p&gt;&lt;p&gt;The build also generates hashed filenames and matching HTML link tags, ensuring production serves fully optimised, cache friendly CSS automatically.&lt;/p&gt;&lt;h2 id="more-compression"&gt;More compression&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#more-compression"&gt;&lt;span&gt;Jump to section titled: More compression&lt;/span&gt;&lt;/a&gt;&lt;p&gt;In this blog post, I’m going to take the compression a little further. I already have CSS and JavaScript Brotli compressed to the highest level (11) and served from Cloudflare Pages. But what about the third and final core technology of the web? Arguably the most important too… The HTML. In this post, I will look at how to compress your HTML to 11 during the 11ty build phase, and what modern technologies you need in order to make this work (it’s not as straightforward as I thought!)&lt;/p&gt;&lt;h2 id="why-html-brotli-matters"&gt;Why HTML Brotli matters?&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#why-html-brotli-matters"&gt;&lt;span&gt;Jump to section titled: Why HTML Brotli matters?&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Before we dive into the details, let’s discuss why Brotli compression is important for HTML. Well, to summarise Brotli compression in one sentence:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Brotli compression reduces file size by intelligently identifying repeated patterns in data and encoding them more efficiently using a combination of modern compression techniques.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This is fantastic for anything with repeating patterns as the bytes saved over the network can be huge! The great thing about HTML is that it has a tonne of repeating patterns (e.g. Markup)! This reduction in file size &lt;strong&gt;could&lt;/strong&gt; potentially equate to:&lt;/p&gt;&lt;h3 id="improved-web-performance"&gt;Improved Web Performance&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#improved-web-performance"&gt;&lt;span&gt;Jump to section titled: Improved Web Performance&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Every kilobyte saved here multiplies across your traffic volume.&lt;/p&gt;&lt;h3 id="at-scale-cost-savings-add-up"&gt;At scale, cost savings add up&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#at-scale-cost-savings-add-up"&gt;&lt;span&gt;Jump to section titled: At scale, cost savings add up&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Let’s assume you are serving a high traffic website, say the &lt;a href="https://www.bbc.co.uk"&gt;BBC&lt;/a&gt;, &lt;a href="https://www.google.com"&gt;Google&lt;/a&gt;, or &lt;a href="https://www.gov.uk"&gt;GOV.UK&lt;/a&gt;. Imagine how much bandwidth could be saved by simply compressing your HTML. Any percentage saving on millions of requests per day is going to mean:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Less bandwidth&lt;/li&gt;&lt;li&gt;Lower &lt;a href="https://www.cloudflare.com/en-gb/learning/cloud/what-are-data-egress-fees/"&gt;CDN egress&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Lower cloud costs&lt;/li&gt;&lt;li&gt;Lower carbon footprint&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This is a win both commercially and environmentally!&lt;/p&gt;&lt;h3 id="improved-performance-on-slow-and-unstable-mobile-networks"&gt;Improved performance on slow and unstable mobile networks&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#improved-performance-on-slow-and-unstable-mobile-networks"&gt;&lt;span&gt;Jump to section titled: Improved performance on slow and unstable mobile networks&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Brotli 11 improves the web for users where they need it the most:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;3G connectivity&lt;/li&gt;&lt;li&gt;High latency rural connections&lt;/li&gt;&lt;li&gt;Congested public networks&lt;/li&gt;&lt;li&gt;International access&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;HTML blocks everything else. If you shrink it aggressively, you unblock the page faster, meaning users get a better experience.&lt;/p&gt;&lt;p&gt;This is a real-world performance gain.&lt;/p&gt;&lt;h3 id="it-indirectly-improves-core-web-vitals"&gt;It indirectly improves Core Web Vitals&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#it-indirectly-improves-core-web-vitals"&gt;&lt;span&gt;Jump to section titled: It indirectly improves Core Web Vitals&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Smaller HTML means:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Faster DOM construction&lt;/li&gt;&lt;li&gt;Earlier CSS discovery&lt;/li&gt;&lt;li&gt;Earlier JS discovery&lt;/li&gt;&lt;li&gt;Reduced main thread idle gaps&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This is especially important for server rendered pages or hybrid Server-side Rendered apps.&lt;/p&gt;&lt;h3 id="compression-cost-is-paid-once-for-static-assets"&gt;Compression cost is paid once for static assets&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#compression-cost-is-paid-once-for-static-assets"&gt;&lt;span&gt;Jump to section titled: Compression cost is paid once for static assets&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Yes, level 11 compression is expensive to compute. But this cost is paid back over time. You pay for the CPU time once. Users benefit forever assuming:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;the HTML is static and cacheable at the Content Delivery Network (CDN) using long-life caching headers&lt;/li&gt;&lt;li&gt;you automate and pre-compress the HTML at build time&lt;/li&gt;&lt;/ul&gt;&lt;h3 id="compression-size-examples-v1"&gt;Compression Size Examples v1&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#compression-size-examples-v1"&gt;&lt;span&gt;Jump to section titled: Compression Size Examples v1&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Let’s have a look at a huge HTML page on the web. My go-to for this is either the &lt;a href="https://www.w3.org/TR/2011/WD-html5-20110405/Overview.html"&gt;W3C HTML5 Specification page&lt;/a&gt; OR &lt;a href="https://apod.nasa.gov/apod/archivepix.html"&gt;NASA’s Astronomy Picture of the Day Archive&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Rather than choose, let’s just compress both!&lt;/p&gt;&lt;h4 id="w3c-html5-specification-single-page"&gt;W3C HTML5 Specification (Single Page)&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#w3c-html5-specification-single-page"&gt;&lt;span&gt;Jump to section titled: W3C HTML5 Specification (Single Page)&lt;/span&gt;&lt;/a&gt;&lt;p&gt;That’s an &lt;strong&gt;88 percent reduction&lt;/strong&gt; in bytes over the network when compressing the HTML with Brotli 11. Not bad for something that takes just over 10-seconds to run.&lt;/p&gt;&lt;p&gt;And yes, that’s 4.7 MB of HTML alone. It’s an absolute beast of a page.&lt;/p&gt;&lt;p&gt;For perspective, that would take around 12 to 15 minutes to download on a 56k modem in the late 90s. Just for the HTML. I might be showing my age here &#128557;&lt;/p&gt;&lt;h4 id="nasa-s-astronomy-picture-of-the-day-archive"&gt;Nasa’s Astronomy Picture of the Day Archive&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#nasa-s-astronomy-picture-of-the-day-archive"&gt;&lt;span&gt;Jump to section titled: Nasa’s Astronomy Picture of the Day Archive&lt;/span&gt;&lt;/a&gt;&lt;p&gt;We’ve taken it from roughly a third of a megabyte down to just &lt;strong&gt;53 KB&lt;/strong&gt;. That is a pretty substantial reduction!&lt;/p&gt;&lt;p&gt;This means faster page loads, lower data usage, and a much smoother experience for anyone on a limited data plan or dealing with patchy connections. A very positive impact, right where it matters most for users.&lt;/p&gt;&lt;h2 id="how-is-this-different-from-my-other-compression-posts"&gt;How is this different from my other compression posts?&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#how-is-this-different-from-my-other-compression-posts"&gt;&lt;span&gt;Jump to section titled: How is this different from my other compression posts?&lt;/span&gt;&lt;/a&gt;&lt;p&gt;So how is this different from the Brotli compression I have mentioned in the previous blog posts?&lt;/p&gt;&lt;p&gt;In &lt;a href="https://nooshu.com/blog/2025/01/05/cranking-brotli-up-to-11-with-cloudflare-pro-and-11ty/"&gt;Cranking Brotli up to 11 with Cloudflare Pro and 11ty&lt;/a&gt; I used a combination of:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Brotli CLI (e.g.&lt;code&gt;brew install brotli&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;bash scripts (&lt;code&gt;compress.sh&lt;/code&gt;, &lt;code&gt;compress-directory.sh&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In this post, I override the Cloudflare Dashboard's "Compression Rules” (which dynamically compresses HTML at Brotli level 4). The CDN is compressing the HTML on-the-fly when the user’s browser requests it.&lt;/p&gt;&lt;p&gt;What this blog post describes is pre-compression to Brotli 11 at 11ty build time. To do this the workflow is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Apply Brotli level 11 pre-compression to HTML during the 11ty build on Cloudflare Pages.&lt;/li&gt;&lt;li&gt;&lt;code&gt;_helpers/html-compression.js&lt;/code&gt; runs after the site is written to &lt;code&gt;_site&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Each HTML file gets a matching &lt;code&gt;.br&lt;/code&gt; file&lt;/li&gt;&lt;li&gt;Use the built-in Node &lt;code&gt;zlib&lt;/code&gt; module, so no manual setup, CLI tools, scripts, Cloudflare configuration, or extra dependencies are required.&lt;/li&gt;&lt;li&gt;Take advantage of &lt;a href="https://developers.cloudflare.com/pages/functions/"&gt;Cloudflare Pages Functions&lt;/a&gt;, which run on &lt;a href="https://workers.cloudflare.com/"&gt;Cloudflare Workers&lt;/a&gt;. This is a great opportunity to use a modern, fast evolving platform that opens the door to powerful edge capabilities.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id="what-s-the-point"&gt;What’s the point?&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#what-s-the-point"&gt;&lt;span&gt;Jump to section titled: What’s the point?&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Well, that’s a great question. As some readers may know by default Cloudflare compresses HTML at compression level 4. Now, if we compare this level to the compression level 11 above, let’s have a look at the difference:&lt;/p&gt;&lt;h3 id="compression-size-examples-v2"&gt;Compression Size Examples v2&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#compression-size-examples-v2"&gt;&lt;span&gt;Jump to section titled: Compression Size Examples v2&lt;/span&gt;&lt;/a&gt;&lt;h4 id="w3c-html5-specification-single-page-2"&gt;W3C HTML5 Specification (Single Page)&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#w3c-html5-specification-single-page-2"&gt;&lt;span&gt;Jump to section titled: W3C HTML5 Specification (Single Page)&lt;/span&gt;&lt;/a&gt;&lt;h4 id="nasa-s-astronomy-picture-of-the-day-archive-2"&gt;Nasa’s Astronomy Picture of the Day Archive&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#nasa-s-astronomy-picture-of-the-day-archive-2"&gt;&lt;span&gt;Jump to section titled: Nasa’s Astronomy Picture of the Day Archive&lt;/span&gt;&lt;/a&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href="https://apod.nasa.gov/apod/archivepix.html"&gt;https://apod.nasa.gov/apod/archivepix.html&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Uncompressed size&lt;/strong&gt;: 314 KB&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 4)&lt;/strong&gt;: 66 KB (79%)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Compression Time (Level 4)&lt;/strong&gt;: 0.011 seconds&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 11)&lt;/strong&gt;: 53 KB (83% saving)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Brotli (Level 11)&lt;/strong&gt;: 0.743 seconds&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It’s worth noting that I used &lt;a href="https://paulcalvano.com/"&gt;Paul Calvano's&lt;/a&gt; fantastic &lt;a href="https://tools.paulcalvano.com/compression-tester/"&gt;Compression Tester Tool&lt;/a&gt;, to help with this basic analysis.&lt;/p&gt;&lt;p&gt;What you will likely notice is that even for large HTML files the size difference between compression level 4 and compression level 11 isn’t huge, only 12 KB in the W3C HTML5 Specification example. The real difference comes in computation time. Level 4 0.149 seconds verses 11.717 seconds! That’s a &lt;strong&gt;7764% increase&lt;/strong&gt; in time between Level 4 and Level 11! Although this is an extreme example, you can probably see why Level 11 isn’t used by Cloudflare for on-the-fly compression of HTML assets. Level 4 gives a good balence between file compression versus compression speed. I’m betting countless smart people were involved in the analysis of using Level 4 by default! When you are a company that is literally serving billions of requests per second, this decision really makes a difference in terms of processing power infrastructure and power usage!&lt;/p&gt;&lt;p&gt;Thankfully, the way that I have implemented level 11 compression on my blog, processing time doesn’t really matter. All the HTML is being compressed at 11ty build time. As I said above, the cost of this additional CPU time is paid back over time by users getting a better experience (even if only slightly). Furthermore, remember there’s a slight reduction in storage required on the CDN. From my perspective, if it is low effort after setup, it feels like the right move to implement it.&lt;/p&gt;&lt;h2 id="problems"&gt;Problems&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#problems"&gt;&lt;span&gt;Jump to section titled: Problems&lt;/span&gt;&lt;/a&gt;&lt;p&gt;As I found out during implementation it isn’t just as simple as compressing the HTML to 11 and setting a static &lt;code&gt;Content-Encoding&lt;/code&gt; header for HTML in the &lt;code&gt;_headers&lt;/code&gt; file (trust me, I tried it!)&lt;/p&gt;&lt;h3 id="problem-1-url-path-vs-actual-file-path-mismatch"&gt;Problem 1: URL Path vs Actual File Path Mismatch&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#problem-1-url-path-vs-actual-file-path-mismatch"&gt;&lt;span&gt;Jump to section titled: Problem 1: URL Path vs Actual File Path Mismatch&lt;/span&gt;&lt;/a&gt;&lt;p&gt;When serving static assets like CSS, JS, or images, there is usually a simple one-to-one relationship between the URL and the file on disk. A request to &lt;code&gt;/css/site.css&lt;/code&gt; maps directly to &lt;code&gt;_site/css/site.css&lt;/code&gt;. No extra logic is required because the URL path matches the file path exactly. I had no idea, but I soon found out that HTML pages behave differently. A request to &lt;code&gt;/&lt;/code&gt; or &lt;code&gt;/blog/post/&lt;/code&gt; does not correspond to a literal file at that path. Instead, the server applies a convention and serves &lt;code&gt;index.html&lt;/code&gt; inside that directory. So &lt;code&gt;/blog/post/&lt;/code&gt; actually maps to &lt;code&gt;blog/post/index.html&lt;/code&gt; on disk. This mapping happens automatically when Cloudflare serves uncompressed HTML through its very efficient static asset layer.&lt;/p&gt;&lt;p&gt;The problem appears when serving pre-compressed Brotli files. You cannot simply request the same URL and expect the .br file to resolve. Instead, the Cloudflare Function must manually translate the directory style URL into the real file path before appending &lt;code&gt;.br&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;For example, &lt;code&gt;/&lt;/code&gt; becomes &lt;code&gt;/index.html.br&lt;/code&gt;, &lt;code&gt;/blog/post/&lt;/code&gt; becomes &lt;code&gt;/blog/post/index.html.br&lt;/code&gt;, and &lt;code&gt;/404.html&lt;/code&gt; becomes &lt;code&gt;/404.html.br&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;In short, HTML requires explicit path translation because the browser sees a directory style URL while the actual file stored on disk is index.html. The Cloudflare Function must bridge that gap to correctly serve the Brotli 11 compressed version, rather than the uncompressed HTML version.&lt;/p&gt;&lt;p&gt;It actually makes sense now that I think about it, I’ve always just taken the automatic appending of &lt;code&gt;index.html&lt;/code&gt; to a URL Path for granted! The fact that as a user on the web doesn’t even have to think about that small detail, shows how well it works! As &lt;a href="https://en.wikipedia.org/wiki/Dieter_Rams"&gt;Dieter Rams&lt;/a&gt; once said:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Good design is as little design as possible.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id="problem-2-a-page-full-of-wingdings"&gt;Problem 2: A page full of Wingdings&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#problem-2-a-page-full-of-wingdings"&gt;&lt;span&gt;Jump to section titled: Problem 2: A page full of Wingdings&lt;/span&gt;&lt;/a&gt;&lt;p&gt;I’m showing my age again, but for readers who don’t remember early versions of Windows (e.g. &lt;a href="https://en.wikipedia.org/wiki/Windows_3.1"&gt;3.1&lt;/a&gt;), it came bundled with a font called &lt;a href="https://en.wikipedia.org/wiki/Wingdings"&gt;Wingdings&lt;/a&gt;. This True Type font contains many largely recognised shapes and gestures as well as some recognised world symbols.&lt;/p&gt;&lt;p&gt;Wingdings were an early symbol font that experimented with pictographic digital symbols, that would later lead on to ASCII emoticons like &lt;code&gt;:-)&lt;/code&gt; &amp;amp; &lt;code&gt;¯\_(ツ)_/¯&lt;/code&gt;, which in turn would progress to the modern world of &lt;a href="https://emojipedia.org/"&gt;Emoji’s&lt;/a&gt;!&lt;/p&gt;&lt;p&gt;Essentially, what was happening the Cloudflare server was serving raw Brotli compressed HTML files to the browser, expecting it to understand what these (now binary, not text) files were, and how to read and understand them. I was essentially serving the HTML without the following headers:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Content-Type: text/html; charset=UTF-8
Content-Encoding: br
Vary: Accept-Encoding
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here’s a brief explanation of these headers:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;code&gt;Content-Encoding: br&lt;/code&gt;: This is telling the browser “What I’m sending you is a Brotli compressed file, you are going to need to decode it before you understand it”.&lt;/li&gt;&lt;li&gt;&lt;code&gt;Content-Type: text/html; charset=UTF-8&lt;/code&gt;: This tells the browser what character set ('charset') it should use after decompression, this is essential as this is key to the browser parsing the HTML correctly.&lt;/li&gt;&lt;li&gt;&lt;code&gt;Vary: Accept-Encoding&lt;/code&gt; only affects caching (e.g. Content Delivery Networks). It’s basically saying to the cache to store separate versions of this file depending on the Accept-Encoding header.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The result of the above gave me a homepage that looked like the image below (interesting but not exactly readable!):&lt;/p&gt;&lt;p&gt;&lt;source type="image/webp"&gt;&lt;img alt="The garbage output of a raw Brotli encoded HTML file in the browser looks like something from the Wingdings font from the early 90’s" src="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/so0HRc7rVz-300.jpeg"&gt;&lt;/source&gt;&lt;/p&gt;&lt;h2 id="my-approach"&gt;My Approach&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#my-approach"&gt;&lt;span&gt;Jump to section titled: My Approach&lt;/span&gt;&lt;/a&gt;&lt;h3 id="step-1-build-time-compression"&gt;Step 1: Build-time compression&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#step-1-build-time-compression"&gt;&lt;span&gt;Jump to section titled: Step 1: Build-time compression&lt;/span&gt;&lt;/a&gt;&lt;p&gt;After the site is generated, it moves into a final preparation stage before going live.&lt;/p&gt;&lt;p&gt;During this stage, the system goes through all the finished HTML pages and creates highly compressed versions of them. These compressed files sit alongside the originals and are ready to be served immediately.&lt;/p&gt;&lt;p&gt;Because this happens as part of the 11ty build process, every release automatically includes freshly optimised files. This means the live site can deliver pages faster, with smaller file sizes and no need to compress anything on the fly (e.g. what Cloudflare does with its HTML compression to Brotli level 4).&lt;/p&gt;&lt;p&gt;The result is better performance for users, with no extra overhead once the site is hosted and running on Cloudflare pages.&lt;/p&gt;&lt;h3 id="step-2-cloudflare-pages-function-for-content-negotiation"&gt;Step 2: Cloudflare Pages Function for content negotiation&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#step-2-cloudflare-pages-function-for-content-negotiation"&gt;&lt;span&gt;Jump to section titled: Step 2: Cloudflare Pages Function for content negotiation&lt;/span&gt;&lt;/a&gt;&lt;p&gt;When someone visits a page on the site, a lightweight Cloudflare Function (via a Cloudflare Worker) checks whether a users browser supports modern compression.&lt;/p&gt;&lt;p&gt;If it does, the system serves the Brotli 11 pre compressed version of the page. This keeps file sizes small and pages loading quickly.&lt;/p&gt;&lt;p&gt;If the browser does not support this compression, or if a compressed version is not available, the system simply serves the standard uncompressed version of the HTML instead.&lt;/p&gt;&lt;p&gt;No extra processing happens at this stage. The edge layer (Cloudflare Function + Worker) is only deciding which version of the already prepared files to send. All optimisation work has already been done earlier in the 11ty build process.&lt;/p&gt;&lt;p&gt;The final result is fast delivery, efficient bandwidth use, and a simple, reliable build setup. Everyone wins!&lt;/p&gt;&lt;h3 id="step-3-the-eleventy-build-uses-node-js-zlib-for-seamless-integration"&gt;Step 3: The Eleventy build uses Node.js zlib, for seamless integration&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#step-3-the-eleventy-build-uses-node-js-zlib-for-seamless-integration"&gt;&lt;span&gt;Jump to section titled: Step 3: The Eleventy build uses Node.js zlib, for seamless integration&lt;/span&gt;&lt;/a&gt;&lt;p&gt;As mentioned earlier, this Brotli implementation differs from others I have used. Instead of relying on bash scripts such as &lt;code&gt;compress.sh&lt;/code&gt; or &lt;code&gt;compress-directory.sh&lt;/code&gt;, it uses Node.js’s built in &lt;code&gt;zlib&lt;/code&gt; module. Because &lt;a href="https://nodejs.org/dist/latest/docs/api/zlib.html#zlib"&gt;zlib is part of Node.js core&lt;/a&gt;, it is stable, well maintained, and requires no external dependencies. It has been available since the earliest Node.js releases, so it is a sensible default choice. I may even revisit the other build process in future and consider replacing the remaining bash scripts with a fully Node.js based approach.&lt;/p&gt;&lt;h2 id="implementation"&gt;Implementation&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#implementation"&gt;&lt;span&gt;Jump to section titled: Implementation&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Next, let’s stop the waffling and get onto the actual implementation, I assume that’s what readers are here for after all!&lt;/p&gt;&lt;h3 id="core-compression-utility"&gt;Core compression utility&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#core-compression-utility"&gt;&lt;span&gt;Jump to section titled: Core compression utility&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Here is the main compression file that is used to compress the HTML to Brotli 11:&lt;/p&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;

&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; brotliCompressSync &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'zlib'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;


&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; &lt;span&gt;BROTLI_LEVEL&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;11&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;


&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;brotliCompress&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;input&lt;span&gt;,&lt;/span&gt; level &lt;span&gt;=&lt;/span&gt; &lt;span&gt;BROTLI_LEVEL&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; buffer &lt;span&gt;=&lt;/span&gt; Buffer&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isBuffer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;input&lt;span&gt;)&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; input &lt;span&gt;:&lt;/span&gt; Buffer&lt;span&gt;.&lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;input&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;brotliCompressSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;buffer&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; level &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I appreciate that not everyone prefers heavily commented code, so there is also a version in &lt;a href="https://gist.github.com/Nooshu/0dd55a4ba67da0a6d0053dc0e2884ba8"&gt;this Gist&lt;/a&gt; with minimal comments and improved readability.&lt;/p&gt;&lt;h3 id="html-compression-post-build"&gt;HTML compression post-build&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#html-compression-post-build"&gt;&lt;span&gt;Jump to section titled: HTML compression post-build&lt;/span&gt;&lt;/a&gt;&lt;p&gt;The post-build HTML compression file:&lt;/p&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;

&lt;span&gt;import&lt;/span&gt; fs &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'fs'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; path &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'path'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; brotliCompress&lt;span&gt;,&lt;/span&gt; &lt;span&gt;BROTLI_LEVEL&lt;/span&gt; &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'./compression.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;


&lt;span&gt;function&lt;/span&gt; &lt;span&gt;findHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;dir&lt;span&gt;,&lt;/span&gt; acc &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; entries &lt;span&gt;=&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readdirSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;dir&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;withFileTypes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;true&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; entry &lt;span&gt;of&lt;/span&gt; entries&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;const&lt;/span&gt; fullPath &lt;span&gt;=&lt;/span&gt; path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;dir&lt;span&gt;,&lt;/span&gt; entry&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;const&lt;/span&gt; relPath &lt;span&gt;=&lt;/span&gt; path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;relative&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'./_site'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; fullPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;entry&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isDirectory&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			&lt;span&gt;findHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;fullPath&lt;span&gt;,&lt;/span&gt; acc&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; &lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;entry&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; entry&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;.&lt;/span&gt;&lt;span&gt;endsWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'.html'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			acc&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;relPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; acc&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;


&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;compressHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; startTime &lt;span&gt;=&lt;/span&gt; Date&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#128640; PRODUCTION POSTBUILD: Starting HTML Brotli compression'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; siteDir &lt;span&gt;=&lt;/span&gt; path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'./_site'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;existsSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;siteDir&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'⚠️  _site directory not found, skipping HTML compression'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; htmlFiles &lt;span&gt;=&lt;/span&gt; &lt;span&gt;findHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;siteDir&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;htmlFiles&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;===&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'⚠️  No HTML files found, skipping compression'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	&lt;span&gt;let&lt;/span&gt; compressedCount &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;let&lt;/span&gt; skippedCount &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;let&lt;/span&gt; totalOriginal &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;let&lt;/span&gt; totalCompressed &lt;span&gt;=&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; errors &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;for&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;const&lt;/span&gt; relPath &lt;span&gt;of&lt;/span&gt; htmlFiles&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;const&lt;/span&gt; inputPath &lt;span&gt;=&lt;/span&gt; path&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;siteDir&lt;span&gt;,&lt;/span&gt; relPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;const&lt;/span&gt; outputPath &lt;span&gt;=&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;inputPath&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.br&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

		&lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			
			&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;existsSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;outputPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
				&lt;span&gt;const&lt;/span&gt; inputStats &lt;span&gt;=&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;statSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;inputPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				&lt;span&gt;const&lt;/span&gt; outputStats &lt;span&gt;=&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;statSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;outputPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;outputStats&lt;span&gt;.&lt;/span&gt;mtime &lt;span&gt;&amp;gt;=&lt;/span&gt; inputStats&lt;span&gt;.&lt;/span&gt;mtime&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
					skippedCount&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
					&lt;span&gt;continue&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				&lt;span&gt;}&lt;/span&gt;
			&lt;span&gt;}&lt;/span&gt;

			&lt;span&gt;const&lt;/span&gt; fileContent &lt;span&gt;=&lt;/span&gt; fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;readFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;inputPath&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;const&lt;/span&gt; originalSize &lt;span&gt;=&lt;/span&gt; fileContent&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;;&lt;/span&gt;

			&lt;span&gt;const&lt;/span&gt; brotliBuffer &lt;span&gt;=&lt;/span&gt; &lt;span&gt;brotliCompress&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;fileContent&lt;span&gt;,&lt;/span&gt; &lt;span&gt;BROTLI_LEVEL&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;const&lt;/span&gt; compressedSize &lt;span&gt;=&lt;/span&gt; brotliBuffer&lt;span&gt;.&lt;/span&gt;length&lt;span&gt;;&lt;/span&gt;

			fs&lt;span&gt;.&lt;/span&gt;&lt;span&gt;writeFileSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;outputPath&lt;span&gt;,&lt;/span&gt; brotliBuffer&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			compressedCount&lt;span&gt;++&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			totalOriginal &lt;span&gt;+=&lt;/span&gt; originalSize&lt;span&gt;;&lt;/span&gt;
			totalCompressed &lt;span&gt;+=&lt;/span&gt; compressedSize&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			errors&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;❌ &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;relPath&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;error&lt;span&gt;.&lt;/span&gt;message&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; totalTime &lt;span&gt;=&lt;/span&gt; Date&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;-&lt;/span&gt; startTime&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; savedPercent &lt;span&gt;=&lt;/span&gt; totalOriginal &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt; &lt;span&gt;-&lt;/span&gt; totalCompressed &lt;span&gt;/&lt;/span&gt; totalOriginal&lt;span&gt;)&lt;/span&gt; &lt;span&gt;*&lt;/span&gt; &lt;span&gt;100&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;'0'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;✅ Compressed &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;compressedCount&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; HTML file(s) (&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;skippedCount&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; skipped, up-to-date)&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;compressedCount &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
			&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;totalOriginal &lt;span&gt;/&lt;/span&gt; &lt;span&gt;1024&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; KB → &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;totalCompressed &lt;span&gt;/&lt;/span&gt; &lt;span&gt;1024&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt; KB (&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;savedPercent&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;% smaller)&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
		&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;   Finished in &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;totalTime&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;ms (&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;totalTime &lt;span&gt;/&lt;/span&gt; &lt;span&gt;1000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toFixed&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;s)&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;errors&lt;span&gt;.&lt;/span&gt;length &lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\nErrors:'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		errors&lt;span&gt;.&lt;/span&gt;&lt;span&gt;forEach&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;e&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;e&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with minimal comments and much improved readability is available in a &lt;a href="https://gist.github.com/Nooshu/568f44fe571d11660bc3e4e281f6329b"&gt;Gist here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id="cloudflare-pages-function-for-content-negotiation"&gt;Cloudflare Pages Function for content negotiation&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#cloudflare-pages-function-for-content-negotiation"&gt;&lt;span&gt;Jump to section titled: Cloudflare Pages Function for content negotiation&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Here we have the Cloudflare Function file that runs in the Cloudflare Worker. It is located in the &lt;code&gt;/functions&lt;/code&gt; directory in the root of the repository so it can be detected when Cloudflare Pages builds.&lt;/p&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;

&lt;span&gt;async&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;handleHtmlWithBrotli&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;span&gt;,&lt;/span&gt; env&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; url &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;URL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;.&lt;/span&gt;url&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; pathname &lt;span&gt;=&lt;/span&gt; url&lt;span&gt;.&lt;/span&gt;pathname&lt;span&gt;;&lt;/span&gt;

	
	
	
	
	&lt;span&gt;const&lt;/span&gt; isHtmlDocument &lt;span&gt;=&lt;/span&gt; pathname &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'/'&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; pathname&lt;span&gt;.&lt;/span&gt;&lt;span&gt;endsWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'/'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; pathname &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'/404.html'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;isHtmlDocument&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt; env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ASSETS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; acceptsBrotli &lt;span&gt;=&lt;/span&gt; request&lt;span&gt;.&lt;/span&gt;headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Accept-Encoding'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?.&lt;/span&gt;&lt;span&gt;includes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'br'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;??&lt;/span&gt; &lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;acceptsBrotli&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt; env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ASSETS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	
	
	
	&lt;span&gt;const&lt;/span&gt; brPath &lt;span&gt;=&lt;/span&gt;
		pathname &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'/'&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;'/index.html.br'&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; pathname &lt;span&gt;===&lt;/span&gt; &lt;span&gt;'/404.html'&lt;/span&gt; &lt;span&gt;?&lt;/span&gt; &lt;span&gt;'/404.html.br'&lt;/span&gt; &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;pathname&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;index.html.br&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;const&lt;/span&gt; brUrl &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;URL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;brPath&lt;span&gt;,&lt;/span&gt; url&lt;span&gt;.&lt;/span&gt;origin&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;const&lt;/span&gt; brResponse &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ASSETS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;brUrl&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;brResponse&lt;span&gt;.&lt;/span&gt;ok&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt; env&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ASSETS&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	
	
	&lt;span&gt;const&lt;/span&gt; headers &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Headers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;brResponse&lt;span&gt;.&lt;/span&gt;headers&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Content-Encoding'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;'br'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Content-Type'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;'text/html; charset=UTF-8'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Vary'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;'Accept-Encoding'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	
	headers&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'Cache-Control'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;'public, max-age=31536000, no-transform'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;brResponse&lt;span&gt;.&lt;/span&gt;body&lt;span&gt;,&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; brResponse&lt;span&gt;.&lt;/span&gt;status&lt;span&gt;,&lt;/span&gt;
		headers&lt;span&gt;,&lt;/span&gt;
		
		
		&lt;span&gt;encodeBody&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;'manual'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; &lt;span&gt;onRequestGet&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt; request&lt;span&gt;,&lt;/span&gt; env &lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;handleHtmlWithBrotli&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;,&lt;/span&gt; env&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;export&lt;/span&gt; &lt;span&gt;const&lt;/span&gt; &lt;span&gt;onRequestHead&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt; request&lt;span&gt;,&lt;/span&gt; env &lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;handleHtmlWithBrotli&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;request&lt;span&gt;,&lt;/span&gt; env&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with minimal comments and much better readability is available in a &lt;a href="https://gist.github.com/Nooshu/5176f0d3446e05629501130912f09570"&gt;Gist here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id="build-lifecycle-wiring"&gt;Build lifecycle wiring&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#build-lifecycle-wiring"&gt;&lt;span&gt;Jump to section titled: Build lifecycle wiring&lt;/span&gt;&lt;/a&gt;&lt;p&gt;This is the main build file I use to build my 11ty blog for production on Cloudflare Pages.&lt;/p&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;

&lt;span&gt;import&lt;/span&gt; env &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_data/env.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; clearCssBuildCache &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/css-manipulation.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; generatePreloadHeaders &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/header-generator.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; compressHtmlFiles &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/html-compression.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; compressJavaScriptFiles &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/js-compression.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;import&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; minifyJavaScriptFiles &lt;span&gt;}&lt;/span&gt; &lt;span&gt;from&lt;/span&gt; &lt;span&gt;'../_helpers/js-minify.js'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;


&lt;span&gt;export&lt;/span&gt; &lt;span&gt;function&lt;/span&gt; &lt;span&gt;registerBuildEvents&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;eleventyConfig&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;isLocal&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	
	
	eleventyConfig&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'eleventy.before'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;clearCssBuildCache&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

	eleventyConfig&lt;span&gt;.&lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'eleventy.after'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;async&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\n═══════════════════════════════════════════════════════════════════════════════'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#128640; PRODUCTION POSTBUILD PHASE: Beginning postbuild operations'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'═══════════════════════════════════════════════════════════════════════════════\n'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;generatePreloadHeaders&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;await&lt;/span&gt; &lt;span&gt;minifyJavaScriptFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;compressJavaScriptFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;compressHtmlFiles&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'\n═══════════════════════════════════════════════════════════════════════════════'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'✅ PRODUCTION POSTBUILD PHASE: All postbuild operations completed'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'═══════════════════════════════════════════════════════════════════════════════\n'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

		
		
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#128295; Forcing cleanup of HTTP connections and timers...'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;await&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;resolve&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;resolve&lt;span&gt;,&lt;/span&gt; &lt;span&gt;100&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

		&lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			&lt;span&gt;const&lt;/span&gt; http &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;import&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'http'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;const&lt;/span&gt; https &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;import&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'https'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

			&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;http&lt;span&gt;.&lt;/span&gt;globalAgent&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
				http&lt;span&gt;.&lt;/span&gt;globalAgent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;destroy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'✅ Destroyed HTTP global agent'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;}&lt;/span&gt;
			&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;https&lt;span&gt;.&lt;/span&gt;globalAgent&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
				https&lt;span&gt;.&lt;/span&gt;globalAgent&lt;span&gt;.&lt;/span&gt;&lt;span&gt;destroy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'✅ Destroyed HTTPS global agent'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;}&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt; &lt;span&gt;catch&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;error&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'⚠️  Could not destroy HTTP agents:'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; error&lt;span&gt;.&lt;/span&gt;message&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt;

		&lt;span&gt;await&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;resolve&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;resolve&lt;span&gt;,&lt;/span&gt; &lt;span&gt;50&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'✅ HTTP connection cleanup completed'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

		
		
		&lt;span&gt;if&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;env&lt;span&gt;.&lt;/span&gt;isProd&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#127937; Production build complete - forcing process exit in 2 seconds...'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
				console&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;'&#128075; Forcing clean exit now'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
				process&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
			&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2000&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A version with minimal comments and much improved readability is available in a &lt;a href="https://gist.github.com/Nooshu/a6299b047d6f88a586de9b126216b49e"&gt;Gist here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id="other-technical-details-worth-mentioning"&gt;Other Technical details worth mentioning&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#other-technical-details-worth-mentioning"&gt;&lt;span&gt;Jump to section titled: Other Technical details worth mentioning&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Here are 4 other small technical details worth explaining as part of the implementation:&lt;/p&gt;&lt;h3 id="why-encodebody-manual-is-required"&gt;Why encodeBody: 'manual' is required&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#why-encodebody-manual-is-required"&gt;&lt;span&gt;Jump to section titled: Why encodeBody: 'manual' is required&lt;/span&gt;&lt;/a&gt;&lt;p&gt;The content we send back to the user's browser is already compressed using Brotli. It is being loaded from a file that has already been compressed in advance.&lt;/p&gt;&lt;p&gt;If we don't tell Cloudflare to leave it alone, it may assume the content is not compressed and try to compress it again. Compressing something that is already compressed can cause problems and may result in a broken or unreadable output, plus it is a waste of CPU time and resources.&lt;/p&gt;&lt;p&gt;By setting &lt;code&gt;encodeBody: ‘manual'&lt;/code&gt;, we are telling Cloudflare to send the content exactly as it is, without changing it. This ensures the pre-compressed file is delivered correctly to the user's browser.&lt;/p&gt;&lt;h3 id="why-vary-accept-encoding-and-cache-control-no-transform-matter"&gt;Why Vary: Accept-Encoding and Cache-Control: no-transform matter&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#why-vary-accept-encoding-and-cache-control-no-transform-matter"&gt;&lt;span&gt;Jump to section titled: Why Vary: Accept-Encoding and Cache-Control: no-transform matter&lt;/span&gt;&lt;/a&gt;&lt;p&gt;These 2 response headers are critical for making the pre-compression work. I've given details as to why that is below:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;&lt;/strong&gt;: This notifies browsers and CDNs that the response can change depending on what kind of compression the browser supports.&lt;/p&gt;&lt;p&gt;For example, if a browser says it supports Brotli, the cache will store and return the Brotli version for those requests. If another browser doesn't support Brotli, the cache will store and return a different version, such as an uncompressed version. This prevents the wrong format being sent to the wrong browser.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;Cache-Control: no-transform&lt;/code&gt;&lt;/strong&gt;: This informs caches and other systems between the server and the user's browser not to modify the content. It asserts that the response should not be compressed again or altered in any way.&lt;/p&gt;&lt;p&gt;Without this setting, a proxy might try to compress the content again, which can cause errors and waste processing power. With this header in place, the already compressed file is stored and delivered exactly as intended.&lt;/p&gt;&lt;h3 id="incremental-build-optimisation-runtime-check-to-skip-unchanged-files"&gt;Incremental build optimisation (runtime check to skip unchanged files)&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#incremental-build-optimisation-runtime-check-to-skip-unchanged-files"&gt;&lt;span&gt;Jump to section titled: Incremental build optimisation (runtime check to skip unchanged files)&lt;/span&gt;&lt;/a&gt;&lt;p&gt;After the Eleventy build finishes, the post-build step recursively scans through the output folder, such as the &lt;code&gt;_site&lt;/code&gt; directory, and checks each HTML file.&lt;/p&gt;&lt;p&gt;Before compressing a file, it checks whether a matching &lt;code&gt;.br&lt;/code&gt; file already exists and whether it is up-to-date. If the &lt;code&gt;.br&lt;/code&gt; file is the same age or newer than the original file, it is skipped. If the page is new or has been updated, a fresh compressed version is created.&lt;/p&gt;&lt;p&gt;This avoids pages that have not changed being needlessly recompressed, keeping the post build step fast. When only a few pages are updated, the need for recompression is limited.&lt;/p&gt;&lt;h3 id="why-we-need-a-cloudflare-function-instead-of-just-the-headers-file-for-html-brotli"&gt;Why we need a Cloudflare Function instead of just the &lt;code&gt;_headers&lt;/code&gt; file for HTML Brotli?&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#why-we-need-a-cloudflare-function-instead-of-just-the-headers-file-for-html-brotli"&gt;&lt;span&gt;Jump to section titled: Why we need a Cloudflare Function instead of just the _headers file for HTML Brotli?&lt;/span&gt;&lt;/a&gt;&lt;h4 id="1-headers-can-only-change-headers-not-the-file-itself"&gt;1. &lt;code&gt;_headers&lt;/code&gt; can only change headers, not the file itself&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#1-headers-can-only-change-headers-not-the-file-itself"&gt;&lt;span&gt;Jump to section titled: 1. _headers can only change headers, not the file itself&lt;/span&gt;&lt;/a&gt;&lt;p&gt;The &lt;code&gt;_headers&lt;/code&gt; file lets us add or modify (but not remove) response headers. It doesn't control which file is actually sent back to the browser.&lt;/p&gt;&lt;p&gt;So, when someone visits &lt;code&gt;/&lt;/code&gt; or &lt;code&gt;/blog/post/&lt;/code&gt;, Cloudflare Pages automatically serves &lt;code&gt;index.html&lt;/code&gt; or &lt;code&gt;blog/post/index.html&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;If we want to serve the Brotli version of the HTML, we need to send &lt;code&gt;index.html.br&lt;/code&gt; instead. But the &lt;code&gt;_headers&lt;/code&gt; file has no way to switch the file being served.&lt;/p&gt;&lt;h4 id="2-setting-the-header-alone-is-not-enough"&gt;2. Setting the header alone is not enough&lt;/h4&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#2-setting-the-header-alone-is-not-enough"&gt;&lt;span&gt;Jump to section titled: 2. Setting the header alone is not enough&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Even if you add &lt;code&gt;Content-Encoding: br&lt;/code&gt; in the static &lt;code&gt;_headers&lt;/code&gt; file, the actual file being sent would still be the normal uncompressed version of the HTML.&lt;/p&gt;&lt;p&gt;The browser would see this Brotli header and try to decompress the response. Since the content being sent isn't compressed, it would simply fail and the page would break.&lt;/p&gt;&lt;h2 id="results"&gt;Results&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#results"&gt;&lt;span&gt;Jump to section titled: Results&lt;/span&gt;&lt;/a&gt;&lt;p&gt;Looking at my build logs I can now see that compressing the HTML to Brotli 11 has had the following results:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&#128202; HTML Brotli 11 total savings: 123.4 KB (75.2% reduction)&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;That’s not too bad a saving considering how simple it is to set up and integrate into the 11ty build process! Thankfully, now that it’s done I can just “set it and forget it!”. Let's examine the results from the DevTools Network panel below:&lt;/p&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#devtools-before"&gt;&lt;span&gt;Jump to section titled: DevTools Before&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;source type="image/webp"&gt;&lt;img alt="The Firefox DevTools panel before implementation, no Brotli compression in the Content-Encoding response header Network panel column" src="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/NnpcUW5tX8-300.png"&gt;&lt;/source&gt;&lt;/p&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#devtools-after"&gt;&lt;span&gt;Jump to section titled: DevTools After&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;source type="image/webp"&gt;&lt;img alt="The Firefox DevTools panel before implementation, Brotli compression can be seen in the Content-Encoding response header Network panel column" src="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/16NWb1-HYF-300.png"&gt;&lt;/source&gt;&lt;/p&gt;&lt;h3 id="devtools-after-page-reload"&gt;DevTools After Page Reload&lt;/h3&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#devtools-after-page-reload"&gt;&lt;span&gt;Jump to section titled: DevTools After Page Reload&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;source type="image/webp"&gt;&lt;img alt="The Firefox DevTools panel on page reload, no Brotli compression in the Content-Encoding response header Network panel column can be seen" src="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/VCyv4pYajj-300.png"&gt;&lt;/source&gt;&lt;/p&gt;&lt;p&gt;Curiously, when reloading the page with DevTools open, the HTTP status code changes from 200 to 304 and the Brotli compression in the &lt;code&gt;Content-Encoding: br&lt;/code&gt; disappears. The reason for this is because either:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The reload request doesn’t contain an &lt;code&gt;Accept-Encoding: br&lt;/code&gt; header so the Cloudflare Worker is simply returning the uncompressed version of the HTML file as is expected.&lt;/li&gt;&lt;li&gt;The 304 has no response body, so there’s nothing to show as being compressed.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;&lt;a href="https://nooshu.com/blog/2026/02/21/precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers/#summary"&gt;&lt;span&gt;Jump to section titled: Summary&lt;/span&gt;&lt;/a&gt;&lt;p&gt;By pre-compressing this blog’s HTML during the 11ty build phase, I have reduced the number of bytes sent on each page load. While the savings are small for a low traffic site like mine, at scale across millions of users and billions of requests per day, this approach could deliver meaningful bandwidth reductions and incremental performance improvements. This is especially true where network speed and stability vary globally.&lt;/p&gt;&lt;p&gt;Thank you for reading, I hope you found it useful. &lt;a href="https://en.wikipedia.org/wiki/Edge_computing"&gt;Edge Workers&lt;/a&gt; are an incredibly powerful technology. I genuinely look forward to using them again in the future.&lt;/p&gt;&lt;p&gt;I always open to feedback and corrections. If you spot anything that needs fixing or is incorrect, please &lt;a href="https://nooshu.com/contact/"&gt;let me know&lt;/a&gt;. I will credit you in the post changelog below.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Post changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;21/02/26: Initial post published.&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Matt Hobbs</name>
        </author>
        <media:content medium="image" url="https://nooshu.com/og-images/blog-2026-02-21-precompressed-html-at-the-edge-eleventy-meets-cloudflare-workers.png"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://nooshu.com/feed/feed-rss.xml</id>
            <title type="html">Nooshu</title>
            <link href="https://nooshu.com" rel="alternate" type="text/html"/>
            <updated>2026-02-25T10:50:47Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c89249bb5:92bfde:d7f2ecc9</id>
        <title type="html">Research is leadership, and code can help (but only in the right places)</title>
        <published>2026-02-23T06:16:43Z</published>
        <updated>2026-02-23T06:16:51Z</updated>
        <link href="https://productpicnic.beehiiv.com/p/research-is-leadership-and-code-can-help-but-only-in-the-right-places" rel="alternate" type="text/html"/>
        <summary type="html">Code was never the blocker in delivering customer value — and the easier writing code becomes, the more it distracts from the work we must do to unblock productivity.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Open your favorite thought leadership page and scroll for a few seconds. Chances are, you’ll quickly find some screed about how the reality of software development is changing and we have to adapt to it.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Well, I have news for you. The reality of software development changed &lt;/span&gt;&lt;span&gt;&lt;strong&gt;decades ago&lt;/strong&gt;&lt;/span&gt;&lt;span&gt;, and we &lt;/span&gt;&lt;span&gt;&lt;em&gt;still &lt;/em&gt;&lt;/span&gt;&lt;span&gt;haven’t adapted. Today’s teams are using LLMs to push black-box code they don’t understand into production — but between plentiful open-source libraries and StackOverflow copy-pasting, they were &lt;/span&gt;&lt;span&gt;&lt;em&gt;already &lt;/em&gt;&lt;/span&gt;&lt;span&gt;doing that.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Code hasn’t been the real limit on productivity &lt;/span&gt;&lt;span&gt;&lt;a href="https://laughingmeme.org/2026/02/09/code-has-always-been-the-easy-part.html?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;any time in this century&lt;/a&gt;&lt;/span&gt;&lt;span&gt;, and yet all of our work processes are structured as though it is.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;p id="the-social-systems-of-work-are-comi"&gt;&lt;/p&gt;&lt;h1&gt;&lt;span&gt;The social systems of work are coming apart&lt;/span&gt;&lt;/h1&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;What was (is) the limit? Getting signal from customers. &lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;You don’t need high fidelity to learn that you are &lt;/span&gt;&lt;span&gt;&lt;a href="https://www.linkedin.com/posts/jai-toor_its-easier-than-ever-to-build-things-no-activity-7424949922514329601-iXQ4/?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;barking up the wrong tree&lt;/a&gt;&lt;/span&gt;&lt;span&gt;. The easier coding becomes — and the more you produce before showing it to a user — the more effort you end up investing into being wrong. It’s no surprise that while HBR has found that &lt;/span&gt;&lt;span&gt;&lt;a href="https://hbr.org/2026/02/ai-doesnt-reduce-work-it-intensifies-it?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;AI intensifies work&lt;/a&gt;&lt;/span&gt;&lt;span&gt; instead of reducing it, execs forecast an anemic &lt;/span&gt;&lt;span&gt;&lt;a href="https://www.linkedin.com/posts/tante_firm-data-on-ai-activity-7429104180197412864-qBg8/?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;1.4% productivity growth&lt;/a&gt;&lt;/span&gt;&lt;span&gt; over the next 3 years.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Alas! It turns out that &lt;/span&gt;&lt;span&gt;&lt;strong&gt;all the other people in the office with you aren’t merely decorative&lt;/strong&gt;&lt;/span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;&lt;a href="https://productpicnic.beehiiv.com/p/skipping-alignment-leads-to-zero-impact-ux" target="_blank" rel=""&gt;Work is a social system&lt;/a&gt;&lt;/span&gt;&lt;span&gt; and productivity improvements localized to a part of the workflow that was &lt;/span&gt;&lt;span&gt;&lt;em&gt;already&lt;/em&gt;&lt;/span&gt;&lt;span&gt; completely unblocked are not going to reflect in the bottom line.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Rather than help, the presence of AI is actually making it worse.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;❝&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;If there is a productivity crisis in the knowledge economy, it is the fault of management for failing to retain mid-level people in positions where they might feel consistently supported to help make the projects they sponsor do well.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p id="an-atomized-team-is-the-antithesis-"&gt;&lt;/p&gt;&lt;h1&gt;&lt;span&gt;An atomized team is the antithesis of UX&lt;/span&gt;&lt;/h1&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;It’s easy to pretend that continuity isn’t important, as long as you can get your deliverables over the line every two weeks. But is that really what you want to write in your promo doc, or on your resume? No one cares about your story point velocity. Managers want to see impact and ownership. And without engaging with &lt;/span&gt;&lt;span&gt;&lt;a href="https://www.linkedin.com/posts/erikahall_often-the-most-important-design-research-activity-7427096500708597761-NoF6/?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;work-as-a-system&lt;/a&gt;&lt;/span&gt;&lt;span&gt;, you will never achieve either of those.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;❝&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;If you don't understand the structural incentives, the social context of decision-making, and the individual perspectives, you will be continuously confused by watching your organization make obviously bad choices over and over and over while ignoring your recommendations. &lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Long before AI rolled onto the scene, the “build to learn” ideology had already done irreparable harm to people’s ability to understand this system, through the simple means of convincing them to pretend that the system does not exist. &lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;But no matter how hard you try to ignore it, the system is there. You &lt;/span&gt;&lt;span&gt;&lt;a href="https://www.linkedin.com/posts/rsnyder1_what-is-minimum-essential-for-product-market-share-7429883252343152640-h2d9/?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;can’t just build your way into PMF&lt;/a&gt;&lt;/span&gt;&lt;span&gt;. You have to do all the uncomfortable, squishy work &lt;/span&gt;&lt;span&gt;&lt;em&gt;around&lt;/em&gt;&lt;/span&gt;&lt;span&gt; the software. Like research.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Unfortunately, research means talking to people, which means that research can only ever happen at a &lt;/span&gt;&lt;span&gt;&lt;a href="https://bsky.app/profile/acuity.design/post/3mfeg2mts6s2w?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;human pace.&lt;/a&gt;&lt;/span&gt;&lt;span&gt; It can be tempting to skip that research by relying on heuristics (for example assuming that efficiency is always good, and optimizing for that) but that approach is always going to &lt;/span&gt;&lt;span&gt;&lt;a href="https://bsky.app/profile/amyhoy.bsky.social/post/3mfdj6hj3hk2e?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;burn you&lt;/a&gt;&lt;/span&gt;&lt;span&gt;. The state of the art &lt;/span&gt;&lt;span&gt;&lt;a href="https://www.linkedin.com/posts/steveportigal_my-reaction-to-this-article-about-micro-optimizers-activity-7348736606998097920-mCUS/?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;moved past simple time-and-motion optimization&lt;/a&gt;&lt;/span&gt;&lt;span&gt; in the 1960s, and it’s time to get on board.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;p id="good-user-research-not-output-veloc"&gt;&lt;/p&gt;&lt;h1&gt;&lt;span&gt;Good user research, not output velocity, unblocks productivity&lt;/span&gt;&lt;/h1&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;It’s easy to assume that someone else already talked to users, and figured out what they wanted. Even the Agile manifesto carefully excludes the work required to actually compile requirements. And in the decades since it was written, the situation has only gotten worse: tooling has helped us &lt;/span&gt;&lt;span&gt;&lt;em&gt;deliver&lt;/em&gt;&lt;/span&gt;&lt;span&gt; more quickly, but it has done nothing to help us &lt;/span&gt;&lt;span&gt;&lt;em&gt;learn what to deliver&lt;/em&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;This perverse incentive has led a lot of people to foolishly &lt;/span&gt;&lt;span&gt;&lt;a href="https://sawtoothsoftware.com/resources/events/webinars/synthetic-survey-data-its-not-data?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;use AI to counterfeit research data&lt;/a&gt;&lt;/span&gt;&lt;span&gt; (which is often sold to low-maturity teams under the moniker of “synthetic” research) just so they can get back to shipping deliverables, which is easier and smoother and less complicated. And also provides zero actual value.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;There are &lt;/span&gt;&lt;span&gt;&lt;a href="https://jonyablonski.com/articles/2025/user-research-myths/?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;many excuses&lt;/a&gt;&lt;/span&gt;&lt;span&gt; for not doing real user research. But if you want to anchor your work in real data instead of sparkling assumptions, you’re going to have to get over those excuses. Once you’re ready to do so, Stephanie Walter has made it easy with a &lt;/span&gt;&lt;span&gt;&lt;a href="https://stephaniewalter.design/blog/the-expert-guide-to-user-interviews/?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;comprehensive guide to user interviews&lt;/a&gt;&lt;/span&gt;&lt;span&gt; that will get you started even if you’re not an expert.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Which brings us back to the thing about code. Why &lt;/span&gt;&lt;span&gt;&lt;em&gt;doesn’t&lt;/em&gt;&lt;/span&gt;&lt;span&gt; the ability to reach high fidelity faster accelerate our learning? Because the “&lt;/span&gt;&lt;span&gt;&lt;a href="https://bsky.app/profile/acuity.design/post/3mdapnn4c2s2h?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com" target="_blank" rel=""&gt;blockiness&lt;/a&gt;&lt;/span&gt;&lt;span&gt;” of our research artifacts is actually a beneficial property. Good research isn’t looking for a yes or no; it’s creating a dialogue with the participant, and low fidelity leaves the possibility space open as wide as possible.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;And when you’re finally ready to write working code? We’ve known for ten years that &lt;/span&gt;&lt;span&gt;&lt;a href="https://hackernoon.com/the-mvp-is-dead-long-live-the-rat-233d5d16ab02?utm_campaign=research-is-leadership-and-code-can-help-but-only-in-the-right-places&amp;amp;utm_medium=referral&amp;amp;utm_source=productpicnic.beehiiv.com#.1su5holjw" target="_blank" rel=""&gt;testing by launching a viable product is foolish&lt;/a&gt;&lt;/span&gt;&lt;span&gt;. Good research will not only give you answers, but also let you develop a sense of what data is &lt;/span&gt;&lt;span&gt;&lt;em&gt;convincing enough&lt;/em&gt;&lt;/span&gt;&lt;span&gt;, and which assumptions actually warrant testing in prod.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;— Pavel at the Product Picnic&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>The Product Picnic</name>
        </author>
        <media:content medium="image" url="https://beehiiv-images-production.s3.amazonaws.com/uploads/asset/file/332e19e5-aacb-44d9-abb7-12d37cfb2348/57.png?t=1771793068"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c4d8cd4be:45c174:7ea8bc72</id>
        <title type="html">A Broken Heart</title>
        <published>2026-02-11T16:33:21Z</published>
        <updated>2026-02-11T16:33:24Z</updated>
        <link href="https://allenpike.com/2026/a-broken-heart/" rel="alternate" type="text/html"/>
        <summary type="html">Or, getting a 100x speedup with one dumb line of code.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
  Or, getting a 100x speedup with one dumb line of code.
&lt;/p&gt;

&lt;div&gt;
  &lt;p&gt;You always know it’s a good bug when your first reaction is, “&lt;a href="https://allenpike.com/2018/the-great-bug-hunt/"&gt;How could this even happen?&lt;/a&gt;”&lt;/p&gt;
&lt;p&gt;The other day, I was refining the dashboard of a web app we’re working on – as you do – and I noticed it was taking &lt;em&gt;forever&lt;/em&gt; to load. Like, it had been loading in a single second, but now it was taking ten seconds. Something fishy was going on.&lt;/p&gt;
&lt;p&gt;Naturally, I blamed React.&lt;/p&gt;
&lt;p&gt;I mean, sure, in a modern web app there are many potential causes of a performance problem: third-party JavaScript, overburdened servers, bloated assets, missing database indexes – a list as long as your arm. But decades of building for the web told me that this was a frontend problem. I could just smell it. The page looked janky while loading. And despite being the least-bad approach for web frontends today, the React ecosystem is lousy with ways for a codebase to get tangled, slow, and fishy.&lt;/p&gt;
&lt;p&gt;So to prove my theory, I explained to Claude that the dashboard was loading slowly, that it surely had some React problems, and to analyze it and rank them from most to least serious. And sure enough, Claude found a bunch of React fishiness – unnecessary re-renders, missing memoizations. We still weren’t on React Compiler, which I hadn’t realized. So I had Claude do a first pass on the easiest and most serious React issues, and…&lt;/p&gt;
&lt;p&gt;It made almost no difference? Maybe it wasn’t React after all.&lt;/p&gt;
&lt;p&gt;So, I rolled up my sleeves, and started investigating properly.&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;Maybe the server really is slow? A little, but it’s not blocking the frontend.&lt;/li&gt;
&lt;li&gt;Was the problem in all browsers? No. It was somehow specific to Safari?&lt;/li&gt;
&lt;li&gt;Ah, it must be third-party JavaScript then. Intercom? No. PostHog? No.&lt;/li&gt;
&lt;li&gt;Okay, let’s really dig in to the performance timeline.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Now, the Safari performance inspector has diverged from the Chromium one over the years, and has gotten (on this page at least) rather flaky. But it painted a pretty clear picture: It wasn’t spending 7+ seconds parsing JavaScript, calculating styles, or loading from the network. It was using 94% of an M1 Max CPU on… Layout?&lt;/p&gt;

&lt;p&gt;Digging into the details, it showed multiple Layout passes taking more than 1600ms each. For reference, that’s roughly 100x slower than it should be, so something was seriously wrong with how our page was being laid out. Flexbox can get a little slow, but not &lt;em&gt;this&lt;/em&gt; slow.&lt;/p&gt;
&lt;h2 id="time-to-tear-things-apart" tabindex="-1"&gt;Time to tear things apart&lt;/h2&gt;
&lt;p&gt;At that point, I reached for an age-old tool that has gotten more useful in the modern age: binary search. That is, you explain the symptom to your coding agent. Then you have it repeatedly remove stuff from your code that might be causing the problem, and see if that fixes it. When you find &lt;em&gt;something&lt;/em&gt; that fixes it, you iteratively re-add everything until you have a minimal change that indicates the underlying issue, and thus a workaround.&lt;/p&gt;
&lt;p&gt;This is especially fast if the agent can see the problem itself, but I didn’t have a command-line Safari perf analysis tool on hand. Still, after only 10 minutes of telling Claude whether a given change did or didn’t fix the issue, coaching it through how to think about what we’d just learned with each step, we’d found the culprit!&lt;/p&gt;
&lt;p&gt;A heart emoji. ❤️&lt;/p&gt;
&lt;p&gt;If we removed the emoji in our Send Feedback button (which I’d recently added), then Safari could lay the page out in 2 milliseconds. If we re-added it, the page took 1600ms for each layout, of which there were multiple.&lt;/p&gt;
&lt;p&gt;Now, I like to use emoji in early prototype interfaces. They’re trivial to add, and load faster than images. Right? A single character of a font should not take 100x longer to render (or, according to Safari, “Layout”) than the rest of a dynamic React web app. Seems like I’d hit a Safari bug.&lt;/p&gt;
&lt;p&gt;This is generally the “okay now I need a drink” moment in a Weird Bug Hunt. But it was still before noon, so I went to get another coffee instead.&lt;/p&gt;
&lt;p&gt;When you find something that looks to be a bug in the browser, you want to submit that bug. To submit a bug, though, you can’t just attach your whole project. “Hey if you run this whole production app, Safari has a problem.” You’re meant to produce a minimal reproduction case: “Here’s a simple file that triggers the issue. Load it and see.” Even better, this means you’ll fully understand the problem, and can likely find a better workaround than “never use emoji in this app again”.&lt;/p&gt;
&lt;p&gt;Making a minimal repro sample is a huge barrier for submitters, though, since fully boiling a full-fledged app containing proprietary stuff down to the minimal repro case is a ton of tedium.&lt;/p&gt;
&lt;p&gt;Or, it used to be a ton of tedium! Related to their bug-isolating capabilities, coding agents are also particularly well-suited to producing minimal test cases. They can edit a lot of code at once, and it usually doesn’t take much creativity – you’re just iteratively removing as much as you can without making the bug stop triggering.&lt;/p&gt;
&lt;p&gt;So, before long, I had a very simple repro case for the Safari team. And, in looking at the minimal repro code, it became very clear what the real culprit was. On my Mac, Safari 26.2 takes 1600ms to “Layout” the following HTML.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji" rel="stylesheet"&amp;gt;
  &amp;lt;style&amp;gt;
    body { font-family: "Noto Color Emoji"; }
  &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &#128148;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Curse you, &lt;strong&gt;Noto Color Emoji&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="fonts-can-have-colors-now" tabindex="-1"&gt;Fonts can have colors now!&lt;/h2&gt;
&lt;p&gt;Traditionally, fonts were just shapes. It would display in black, or white, or whatever color you chose. But in 2008, Apple shipped &lt;a href="https://typographica.org/typeface-reviews/apple-color-emoji/"&gt;Apple Color Emoji&lt;/a&gt; in iPhone OS 2.2, and brought it to Mac OS X in 2011. Emoji’s rise in popularity led to demand for fonts that have intrinsic color.&lt;/p&gt;
&lt;p&gt;Originally, Apple’s Color Emoji were basically a hack. They just stuck PNG images in a font, which was neither standardized nor resolution-independent. Outside a certain size range, they looked like butt. This led to four competing color font standards (from Apple, Mozilla, Google, and Microsoft) all being submitted to &lt;a href="https://en.wikipedia.org/wiki/OpenType"&gt;OpenType&lt;/a&gt; 1.7. According to Wikipedia, Microsoft and Apple added support for these different approaches, and that’s that.&lt;/p&gt;
&lt;p&gt;But that – as it so often is – was not that.&lt;/p&gt;
&lt;p&gt;You see, Noto Color Emoji is a Google font that is helpful in that it gives you consistent emoji rendering across platforms. We’d included it earlier to get decent emoji rendering on Linux (where we do some HTML-to-video rendering in the cloud, a technique that sounds horrifying but can be pretty useful). However, the font relies on &lt;a href="https://developer.chrome.com/blog/colrv1-fonts"&gt;COLRv1&lt;/a&gt;, a spec Google advises will make your apps load faster because it results in smaller emoji than bitmaps – and can fall back to supplying SVG for other browsers.&lt;/p&gt;
&lt;p&gt;“Other browsers”, in this case, is Safari. And, I guess, “falling back to SVG” means spending 1600ms of “Layout” for a single character. If you’d like to see what this looks like scaled up, you can try loading &lt;a href="https://fonts.google.com/noto/specimen/Noto+Color+Emoji"&gt;the Google Fonts page that attempts to showcase all of the Noto Color Emoji&lt;/a&gt; glyphs on iPhone. (As of iOS 26.2, it goes poorly.)&lt;/p&gt;
&lt;img src="https://allenpike.com/images/2026/iphone-emoji-error.png" alt="iPhone showing an error when trying to load Noto Color Emoji."&gt;&lt;p&gt;After I mentioned the bug in a Slack, Daniel Jalkut &lt;a href="https://bugs.webkit.org/show_bug.cgi?id=305636"&gt;filed it in the Safari bug tracker&lt;/a&gt;, and Simon Fraser on the webkit team has already commented, noting the slowness seems to be within CoreSVG. Chances are this will get fixed!&lt;/p&gt;
&lt;p&gt;In the meantime, I’d like to contribute this humble finding to the search corpus: don’t use Noto Color Emoji on Apple platforms – list “Apple Color Emoji” first. At least, until the bug is fixed and the resulting Safari release is widespread.&lt;/p&gt;
&lt;p&gt;I’d also like to come clean on a little secret. As profoundly helpful as Claude was in debugging this – I surely fixed this problem 10x faster than I would have without it – I must admit, it was Claude that tipped us off to the existence of Noto Color Emoji in the first place. I suspect that, without the coding agent, we would have solved the Linux emoji problem in a more boring way (using an icon library) and not ended up with a weird slow emoji implementation.&lt;/p&gt;
&lt;p&gt;It seems more true every month: these coding agents are very much like a power saw. Profoundly useful, and proportionately dangerous.&lt;/p&gt;
&lt;p&gt;So, I suppose, here’s to Claude. The cause of – and solution to – all of startups’ problems.&lt;/p&gt;


&lt;/div&gt;

&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://allenpike.com/images/2026/profile-banner.jpg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://allenpike.com/feed/</id>
            <title type="html">Allen Pike</title>
            <link href="https://allenpike.com" rel="alternate" type="text/html"/>
            <updated>2026-02-11T16:33:24Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c4613af8d:21f7c6:721de4d4</id>
        <title type="html">AI Makes the Easy Part Easier and the Hard Part Harder for Developers</title>
        <published>2026-02-10T05:43:41Z</published>
        <updated>2026-02-10T05:43:45Z</updated>
        <link href="https://www.blundergoat.com/articles/ai-makes-the-easy-part-easier-and-the-hard-part-harder" rel="alternate" type="text/html"/>
        <summary type="html">AI handles writing code but leaves the hard work: investigation, context, validation. Why vibe coding has limits and AI assistance can backfire.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;A friend of mine recently attended an open forum panel about how engineering orgs can better support their engineers. The themes that came up were not surprising:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sacrificing quality makes it hard to feel proud of the work. No acknowledgement of current velocity. If we sprint to deliver, the expectation becomes to keep sprinting, forever.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've been hearing variations of this for a while now, but now I'm also hearing and agreeing with "AI doesn't always speed us up".&lt;/p&gt;
&lt;h2 id="user-content-ai-did-it-for-me"&gt;"AI did it for me"&lt;/h2&gt;
&lt;p&gt;Developers used to google things. You'd read a StackOverflow answer, or an article, or a GitHub issue. You did some research, verified it against your own context, and came to your own conclusion. Nobody said "Google did it for me" or "it was the top result so it must be true."&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Now I'm starting to hear "AI did it for me."&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's either overhyping what happened, or it means the developer didn't come to their own conclusion. Both are bad. If someone on my team ever did say Google wrote their code because they copied a StackOverflow answer, I'd be worried about the same things I'm worried about now with AI: did you actually understand what you pasted?&lt;/p&gt;
&lt;h2 id="user-content-vibe-coding-has-a-ceiling"&gt;Vibe coding has a ceiling&lt;/h2&gt;
&lt;p&gt;Vibe coding is fun. At first. For prototyping or low-stakes personal projects, it's useful. But when the stakes are real, every line of code has consequences.&lt;/p&gt;
&lt;p&gt;On a personal project, I asked an AI agent to add a test to a specific file. The file was 500 lines before the request and 100 lines after. I asked why it deleted all the other content. It said it didn't. Then it said the file didn't exist before. I showed it the git history and it apologised, said it should have checked whether the file existed first. (Thank you git).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Now imagine that in a healthcare codebase instead of a side project.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI assistance can cost more time than it saves. That sounds backwards, but it's what happened here. I spent longer arguing with the agent and recovering the file than I would have spent writing the test myself.&lt;/p&gt;
&lt;p&gt;Using AI as an investigation tool, and not jumping straight to AI as solution provider, is a step that some people skip. AI-assisted investigation is an underrated skill that's not easy, and it takes practice to know when AI is wrong. Using AI-generated code can be effective, but if we give AI more of the easy code-writing tasks, we can fall into the trap where AI assistance costs more time than it saves.&lt;/p&gt;
&lt;h2 id="user-content-hard-part-gets-harder"&gt;Hard part gets harder&lt;/h2&gt;
&lt;p&gt;Most people miss this about AI-assisted development. Writing code is the easy part of the job. It always has been. The hard part is investigation, understanding context, validating assumptions, and knowing why a particular approach is the right one for this situation. When you hand the easy part to AI, you're not left with less work. You're left with only the hard work. And if you skipped the investigation because AI already gave you an answer, you don't have the context to evaluate what it gave you.&lt;/p&gt;
&lt;p&gt;Reading and understanding other people's code is much harder than writing code. AI-generated code is other people's code. So we've taken the part developers are good at (writing), offloaded it to a machine, and left ourselves with the part that's harder (reading and reviewing), but without the context we'd normally build up by doing the writing ourselves.&lt;/p&gt;
&lt;h2 id="user-content-sprint-expectations-and-burnout"&gt;Sprint expectations and burnout&lt;/h2&gt;
&lt;p&gt;My friend's panel raised a point I keep coming back to: if we sprint to deliver something, the expectation becomes to keep sprinting. Always. Tired engineers miss edge cases, skip tests, ship bugs. More incidents, more pressure, more sprinting. It feeds itself.&lt;/p&gt;
&lt;p&gt;This is a management problem, not an engineering one. When leadership sees a team deliver fast once (maybe with AI help, maybe not), that becomes the new baseline. The conversation shifts from "how did they do that?" to "why can't they do that every time?"&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My friend was saying:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When people claim AI makes them 10x more productive, maybe it's turning them from a 0.1x engineer to a 1x engineer. So technically yes, they've been 10x'd. The question is whether that's a productivity gain or an exposure of how little investigating they were doing before.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Burnout and shipping slop will eat whatever productivity gains AI gives you. You can't optimise your way out of people being too tired to think clearly.&lt;/p&gt;
&lt;h2 id="user-content-senior-skill-junior-trust"&gt;Senior skill, junior trust&lt;/h2&gt;
&lt;p&gt;I've used the phrase "AI is senior skill, junior trust" to explain how AI coding agents work in practice. They're highly skilled at writing code but we have to trust their output like we would a junior engineer. The code looks good and probably works, but we should check more carefully because they don't have the experience.&lt;/p&gt;
&lt;p&gt;Another way to look at it: an AI coding agent is like a brilliant person who reads really fast and just walked in off the street. They can help with investigations and could write some code, but they didn't go to that meeting last week to discuss important background and context.&lt;/p&gt;
&lt;h2 id="user-content-ownership-still-matters"&gt;Ownership still matters&lt;/h2&gt;
&lt;p&gt;Developers need to take responsible ownership of every line of code they ship. Not just the lines they wrote, the AI-generated ones too.&lt;/p&gt;
&lt;p&gt;If you're cutting and pasting AI output because someone set an unrealistic velocity target, you've got a problem 6 months from now when a new team member is trying to understand what that code does. Or at 2am when it breaks. "AI wrote it" isn't going to help you in either situation.&lt;/p&gt;
&lt;h2 id="user-content-how-can-ai-make-the-hard-part-easier"&gt;How can AI make the hard part easier?&lt;/h2&gt;
&lt;p&gt;The other day there was a production bug. A user sent an enquiry to the service team a couple of hours after a big release. There was an edge case timezone display bug. The developer who made the change had 30 minutes before they had to leave to teach a class, and it was late enough for me to already be at home. So I used AI to help investigate, letting it know the bug must be based on recent changes and explaining how we could reproduce. Turned out some deprecated methods were taking priority over the current timezone-aware ones, so the timezone was never converting correctly. Within 15 minutes I had the root cause, a solution idea, and investigation notes in the GitHub issue. The developer confirmed the fix, others tested and deployed, and I went downstairs to grab my DoorDash dinner.&lt;/p&gt;
&lt;p&gt;No fire drill. No staying late. AI did the investigation grunt work, I provided the context and verified, the developer confirmed the solution. That's AI helping with the hard part.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Matthew Hansen</name>
        </author>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c32d57e81:75c22b:c646b007</id>
        <title type="html">untitled</title>
        <published>2026-02-06T12:02:58Z</published>
        <updated>2026-02-06T12:03:04Z</updated>
        <link href="https://allthingssmitty.com/2026/02/02/explicit-resource-management-in-javascript/" rel="alternate" type="text/html"/>
        <content type="html">&lt;div lang="en" data-lt-installed="true"&gt;
&lt;div&gt;
&lt;div&gt;
&lt;header&gt;
&lt;div&gt;
&lt;a href="#main-content"&gt;Skip to main content&lt;/a&gt;
&lt;/div&gt;
&lt;nav&gt;
&lt;h1&gt;
&lt;a href="/"&gt;Matt Smith&lt;/a&gt;
&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="/about"&gt;About&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="/contact"&gt;Contact&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/nav&gt;
&lt;div&gt;Web Dev | Front-End Engineer | UX Designer&lt;/div&gt;
&lt;/header&gt;
&lt;main&gt;&lt;article&gt;
&lt;div&gt;
&lt;header&gt;
&lt;h2&gt;Explicit resource management in JavaScript&lt;/h2&gt;
&lt;div&gt;
&lt;span&gt;
&lt;time&gt;
&lt;span data-long="February 2, 2026" data-short="Feb 2, 2026"&gt;&lt;/span&gt;
&lt;/time&gt;
&lt;/span&gt;
&lt;div&gt;
3 min read
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
684
views
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/header&gt;
&lt;div&gt;
&lt;p&gt;Writing JavaScript that opens something (a file, a stream, a lock, a database connection) also means remembering to clean it up. And if we’re being honest, that cleanup doesn’t always happen. I know I’ve missed it more than once.&lt;/p&gt;
&lt;p&gt;JavaScript has always made this our problem. We reach for &lt;code&gt;try / finally&lt;/code&gt;, tell ourselves we’ll be careful, and hope we didn’t miss an edge case. It usually works, but it’s noisy and easy to get subtly wrong. It also scales poorly once you’re juggling more than one resource.&lt;/p&gt;
&lt;p&gt;That’s finally starting to change. &lt;strong&gt;Explicit resource management&lt;/strong&gt; gives JavaScript a first-class, language-level way to say, “This thing needs cleanup, and the runtime will guarantee it happens.”&lt;/p&gt;
&lt;p&gt;Not as a convention or a pattern, but as part of the language.&lt;/p&gt;
&lt;h2&gt;We’re bad at cleanup (and the language doesn’t help)&lt;/h2&gt;
&lt;p&gt;This pattern should look familiar:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;const&lt;/span&gt; file &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;openFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;quot;data.txt&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// do something with file&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;finally&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; file&lt;span&gt;.&lt;/span&gt;&lt;span&gt;close&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is fine, but also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Verbose&lt;/li&gt;
&lt;li&gt;Repetitive&lt;/li&gt;
&lt;li&gt;Easy to mess up as complexity grows, especially during refactors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now add &lt;em&gt;another&lt;/em&gt; resource:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;const&lt;/span&gt; file &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;openFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;quot;data.txt&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; lock &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;acquireLock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// work with file and lock&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;finally&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; lock&lt;span&gt;.&lt;/span&gt;&lt;span&gt;release&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; file&lt;span&gt;.&lt;/span&gt;&lt;span&gt;close&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now order matters. Error paths matter. You &lt;em&gt;can&lt;/em&gt; reason through all of this, but the mental overhead keeps creeping up. And once it’s there, bugs tend to follow.&lt;/p&gt;
&lt;p&gt;Other languages solved this years ago. JavaScript is (slowly) catching up.&lt;/p&gt;
&lt;aside&gt;
&lt;div&gt;&#128204; Dive further into async&lt;/div&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; in loops isn’t always what you think. Here’s where &lt;a href="/2025/10/20/rethinking-async-loops-in-javascript/"&gt;things start to break down&lt;/a&gt;.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2&gt;&lt;code&gt;using&lt;/code&gt;: cleanup, but make it the runtime’s job&lt;/h2&gt;
&lt;p&gt;At a high level, &lt;code&gt;using&lt;/code&gt; declares a resource that will be &lt;strong&gt;automatically cleaned up when it goes out of scope&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;using file &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;openFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;quot;data.txt&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// do something with file&lt;/span&gt;

&lt;span&gt;// file is automatically closed at the end of this scope&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No &lt;code&gt;try&lt;/code&gt;. No &lt;code&gt;finally&lt;/code&gt;. No “did I remember to close this?”&lt;/p&gt;
&lt;p&gt;The key shift is that cleanup is tied to &lt;strong&gt;lifetime&lt;/strong&gt;, not control flow.&lt;/p&gt;
&lt;h2&gt;How cleanup actually works&lt;/h2&gt;
&lt;p&gt;Resources opt in by implementing a well-known symbol:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Symbol.dispose&lt;/code&gt; for synchronous cleanup&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol.asyncDispose&lt;/code&gt; for asynchronous cleanup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;FileHandle&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;async&lt;/span&gt; &lt;span&gt;write&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;/* ... */&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  async &lt;span&gt;[&lt;/span&gt;Symbol&lt;span&gt;.&lt;/span&gt;asyncDispose&lt;span&gt;]&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;await&lt;/span&gt; &lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;close&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once a value has one of these methods, it can be used with &lt;code&gt;using&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And importantly, &lt;code&gt;using&lt;/code&gt; &lt;strong&gt;doesn’t magically close files&lt;/strong&gt;, it just standardizes cleanup instead of every library inventing its own.&lt;/p&gt;
&lt;h2&gt;When you need &lt;code&gt;await using&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;If cleanup is asynchronous, you’ll typically use &lt;code&gt;await using&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;await&lt;/span&gt; using file &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;openFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;quot;data.txt&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// async work with file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the scope ends, JavaScript will &lt;em&gt;await&lt;/em&gt; disposal before continuing.&lt;/p&gt;
&lt;p&gt;Synchronous resources (locks, in-memory structures) can use plain &lt;code&gt;using&lt;/code&gt;. It may feel odd at first, but it matches how JavaScript already draws the line between sync and async elsewhere. What matters is that cleanup happens at scope exit.&lt;/p&gt;
&lt;aside&gt;
&lt;div&gt;&#128204; Designing for async&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Array.fromAsync()&lt;/code&gt; is one sign JavaScript is finally treating async as a first-class concern: &lt;a href="/2025/07/14/modern-async-iteration-in-javascript-with-array-fromasync/"&gt;modern async iteration in JavaScript&lt;/a&gt;.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2&gt;Stacking resources without the headache&lt;/h2&gt;
&lt;p&gt;This is where things really improve.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;const&lt;/span&gt; file &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;openFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;quot;data.txt&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; lock &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;acquireLock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;try&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// work&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt; &lt;span&gt;finally&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; lock&lt;span&gt;.&lt;/span&gt;&lt;span&gt;release&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; file&lt;span&gt;.&lt;/span&gt;&lt;span&gt;close&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You write:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;await&lt;/span&gt; using file &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;openFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;quot;data.txt&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
using lock &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;acquireLock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// work&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cleanup happens automatically, &lt;strong&gt;in reverse order&lt;/strong&gt;, like a stack:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;lock&lt;/code&gt; is released&lt;/li&gt;
&lt;li&gt;&lt;code&gt;file&lt;/code&gt; is closed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No extra syntax. Errors don’t short-circuit disposal, and cleanup happens in a defined order.&lt;/p&gt;
&lt;h2&gt;Scope is the point&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;using&lt;/code&gt; declaration is scoped just like &lt;code&gt;const&lt;/code&gt; or &lt;code&gt;let&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;await&lt;/span&gt; using file &lt;span&gt;=&lt;/span&gt; &lt;span&gt;await&lt;/span&gt; &lt;span&gt;openFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;quot;data.txt&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;// file is valid here&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// file is disposed here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pushes you toward tighter scopes and makes lifetimes explicit, something JavaScript has historically been bad at expressing. Once you start seeing lifetimes in the code itself, it’s hard to unsee.&lt;/p&gt;
&lt;h2&gt;When &lt;code&gt;using&lt;/code&gt; isn’t enough&lt;/h2&gt;
&lt;p&gt;Not every resource fits neatly into a block. Sometimes acquisition is conditional, or you’re refactoring older code and don’t want to introduce new scopes everywhere.&lt;/p&gt;
&lt;p&gt;That’s where &lt;code&gt;DisposableStack&lt;/code&gt; and &lt;code&gt;AsyncDisposableStack&lt;/code&gt; come in:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;const&lt;/span&gt; stack &lt;span&gt;=&lt;/span&gt; &lt;span&gt;new&lt;/span&gt; &lt;span&gt;AsyncDisposableStack&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;const&lt;/span&gt; file &lt;span&gt;=&lt;/span&gt; stack&lt;span&gt;.&lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt; &lt;span&gt;openFile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;quot;data.txt&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;const&lt;/span&gt; lock &lt;span&gt;=&lt;/span&gt; stack&lt;span&gt;.&lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt; &lt;span&gt;acquireLock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;// work with file and lock&lt;/span&gt;

&lt;span&gt;await&lt;/span&gt; stack&lt;span&gt;.&lt;/span&gt;&lt;span&gt;disposeAsync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get the same safety as &lt;code&gt;using&lt;/code&gt;, with more flexibility. If &lt;code&gt;using&lt;/code&gt; is the clean, declarative case, stacks are the escape hatch.&lt;/p&gt;
&lt;h2&gt;This isn’t just a back-end feature&lt;/h2&gt;
&lt;p&gt;At first glance this can feel like a server-side concern, but it applies just as much on the front end and in platform code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web Streams&lt;/li&gt;
&lt;li&gt;&lt;code&gt;navigator.locks&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Observers and subscriptions&lt;/li&gt;
&lt;li&gt;IndexedDB transactions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anyone who’s written &lt;code&gt;subscribe()&lt;/code&gt; / &lt;code&gt;unsubscribe()&lt;/code&gt; or &lt;code&gt;open()&lt;/code&gt; / &lt;code&gt;close()&lt;/code&gt;, this should at least make you pause.&lt;/p&gt;
&lt;p&gt;This isn’t just about correctness. It’s about &lt;strong&gt;making lifetimes visible in the code&lt;/strong&gt; instead of hiding them in conventions and comments.&lt;/p&gt;
&lt;h2&gt;What’s the catch?&lt;/h2&gt;
&lt;p&gt;As of early 2026, Chrome 123+ and Firefox 119+ support all of these features. Node.js 20.9+, too. &lt;strong&gt;Safari support is still pending&lt;/strong&gt;, but it’s on their radar.&lt;/p&gt;
&lt;p&gt;For now, it’s something to experiment with and maybe start designing APIs around, especially if you maintain libraries or platform-level abstractions. Even if you’re not using &lt;code&gt;using&lt;/code&gt; tomorrow, the model it introduces is worth paying attention to.&lt;/p&gt;
&lt;h2&gt;A better default for cleanup&lt;/h2&gt;
&lt;p&gt;Explicit resource management doesn’t replace &lt;code&gt;try / finally&lt;/code&gt;. You’ll still use it when you need fine-grained control.&lt;/p&gt;
&lt;p&gt;What it does give us is a better default: less boilerplate, fewer leaks, clearer intent, and code that just reads better. As JavaScript takes on more systems-like responsibilities, features like this feel less like nice-to-haves and more like table stakes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="/tags/javascript"&gt;JavaScript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;footer&gt;
&lt;/footer&gt;
&lt;/div&gt;
&lt;/article&gt;
&lt;div&gt;
&lt;a href="/2026/01/12/stop-turning-everything-into-arrays-and-do-less-work-instead/"&gt;Previous post&lt;/a&gt;
&lt;/div&gt;
&lt;div&gt;&lt;iframe src="https://disqus.com/embed/comments/?base=default&amp;f=allthingssmitty-com&amp;t_u=https%3A%2F%2Fallthingssmitty.com%2F2026%2F02%2F02%2Fexplicit-resource-management-in-javascript%2F&amp;t_d=Explicit%20resource%20management%20in%20JavaScript%20-%20Matt%20Smith&amp;t_t=Explicit%20resource%20management%20in%20JavaScript%20-%20Matt%20Smith&amp;s_o=default#version=2ad8ee8902760829d9e04b8b01f3a1b6" tabindex="0" title="Disqus" name="dsq-app4442" width="100%"&gt;&lt;/iframe&gt;&lt;iframe tabindex="0" name="indicator-north" title="Disqus"&gt;&lt;/iframe&gt;&lt;iframe tabindex="0" name="indicator-south" title="Disqus"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;div class="feedlyNoScript"&gt;
Please enable JavaScript to view the
&lt;a rel="nofollow" href="https://disqus.com/?ref_noscript"&gt;comments powered by Disqus.&lt;/a&gt;
&lt;/div&gt;
&lt;/main&gt;
&lt;footer&gt;
&lt;div&gt;&lt;div&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://twitter.com/allthingssmitty"&gt;
&lt;div&gt;
&lt;div&gt;Twitter&lt;/div&gt;
&lt;/div&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://linkedin.com/in/allthingssmitty"&gt;
&lt;div&gt;
&lt;div&gt;LinkedIn&lt;/div&gt;
&lt;/div&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/allthingssmitty"&gt;
&lt;div&gt;
&lt;div&gt;GitHub&lt;/div&gt;
&lt;/div&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://codepen.io/allthingssmitty"&gt;
&lt;div&gt;
&lt;div&gt;CodePen&lt;/div&gt;
&lt;/div&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
© 2026 Matt Smith. All rights reserved.
&lt;/div&gt;

&lt;/footer&gt;
&lt;/div&gt;


&lt;iframe&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;/div&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://disqus.com/embed/comments/?base=default&amp;f=allthingssmitty-com&amp;t_u=https%3A%2F%2Fallthingssmitty.com%2F2026%2F02%2F02%2Fexplicit-resource-management-in-javascript%2F&amp;t_d=Explicit%20resource%20management%20in%20JavaScript%20-%20Matt%20Smith&amp;t_t=Explicit%20resource%20management%20in%20JavaScript%20-%20Matt%20Smith&amp;s_o=default#version=2ad8ee8902760829d9e04b8b01f3a1b6"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://allthingssmitty.com/atom.xml</id>
            <title type="html">allthingssmitty.com</title>
            <link href="https://allthingssmitty.com" rel="alternate" type="text/html"/>
            <updated>2026-02-06T12:03:04Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c1d4ce7f2:9cbb6a:7986450</id>
        <title type="html">Anti-frameworkism: Choosing native web APIs over frameworks</title>
        <published>2026-02-02T07:41:45Z</published>
        <updated>2026-02-02T07:41:49Z</updated>
        <link href="https://blog.logrocket.com/anti-frameworkism-native-web-apis/" rel="alternate" type="text/html"/>
        <summary type="html">Modern browsers can handle more than you think. Learn when native web APIs are enough—and when frameworks actually make sense.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
    
    &lt;p&gt;&lt;/p&gt;
    
&lt;p&gt;Today’s browsers can handle most of the problems that frontend frameworks were originally created to solve. &lt;a href="https://blog.logrocket.com/web-components-adoption-guide/"&gt;Web Components&lt;/a&gt; provide encapsulation, ES modules manage dependencies,&lt;a href="https://blog.logrocket.com/what-should-modern-css-boilerplate-look-like/"&gt; modern CSS&lt;/a&gt; features like Grid and container queries enable complex layouts, and the Fetch API covers network requests.&lt;/p&gt;&lt;img src="https://blog.logrocket.com/wp-content/uploads/2026/01/anti-frameworkism.png" alt=""&gt;&lt;p&gt;Despite this, developers still default to React, Angular, Vue, or another JavaScript framework to address problems the browser already handles natively. That default often trades real user costs –page weight, performance, and SEO – for developer convenience.&lt;/p&gt;
&lt;p&gt;In this article, we’ll explore when frameworks are genuinely necessary, when native web APIs are enough, and how often we actually need a framework at all today.&lt;/p&gt;
&lt;div id="replay-signup"&gt;
    
    &lt;h3&gt;&lt;img alt="&#128640;" src="https://s.w.org/images/core/emoji/17.0.2/svg/1f680.svg"&gt; Sign up for The Replay newsletter&lt;/h3&gt;
    &lt;p&gt;&lt;a href="https://blog.logrocket.com/the-replay-archive/"&gt; &lt;strong&gt;The Replay&lt;/strong&gt;&lt;/a&gt;  is a weekly newsletter for dev and engineering leaders.&lt;/p&gt;
    &lt;p&gt;Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.&lt;/p&gt;
    
&lt;/div&gt;

&lt;h2 id="frameworkism-vs-anti-frameworkism"&gt;&lt;strong&gt;Frameworkism vs anti-frameworkism&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;“Frameworkism” and “anti-frameworkism” aren’t formal terms or established movements. They’re shorthand for two competing defaults in how developers approach new projects. At its core, the divide is about where you choose to start.&lt;/p&gt;
&lt;p&gt;Frameworkism follows a framework-first mindset. A framework – most often React – is selected upfront and treated as the baseline. This approach assumes fast devices and reliable networks, starts heavy by default, and relies on optimization later if performance issues show up.&lt;/p&gt;
&lt;p&gt;Anti-frameworkism takes the opposite approach. It starts with zero dependencies and adds only what’s necessary. Frameworks are tools for specific problems, not the default solution. Developers lean on native browser capabilities first and introduce frameworks only when they hit real limitations. This mindset makes fewer assumptions about user conditions, accounting for slower devices and imperfect networks from the start.&lt;/p&gt;
&lt;h2 id="technical-comparison"&gt;&lt;strong&gt;A technical comparison&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Cross-browser compatibility is largely a solved problem today. Internet Explorer is no longer in the picture, and even Safari supports most modern web APIs, with only a few caveats. Many of the platform gaps that originally drove the adoption of frameworks like React have since been closed.&lt;/p&gt;
&lt;p&gt;The web platform has evolved significantly over the past few years. For a large number of everyday use cases, vanilla JavaScript is more than sufficient.&lt;/p&gt;
&lt;p&gt;Below are some of the most important capabilities the platform provides.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Web Components&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Web Components are built on three core technologies:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Custom Elements for defining new HTML elements&lt;/li&gt;
&lt;li&gt;Shadow DOM for encapsulation&lt;/li&gt;
&lt;li&gt;Template elements for reusable markup&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Together, these APIs provide a component-based architecture without relying on a framework. Below is a simple example of a Web Component (and here are some &lt;a href="https://blog.logrocket.com/web-components-adoption-guide/"&gt;more advanced ones&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;TodoItem&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;HTMLElement&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;
  &lt;/span&gt;&lt;span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;
    &lt;span&gt;super&lt;/span&gt;&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;span&gt;
    &lt;/span&gt;&lt;span&gt;&lt;span&gt;this&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;attachShadow&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt; &lt;span&gt;mode&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;'open'&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;span&gt;
  &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;

  connectedCallback&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;
    &lt;/span&gt;&lt;span&gt;&lt;span&gt;this&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;shadowRoot&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;innerHTML &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;style&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;host &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; display&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; block&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; padding&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;10px&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;completed &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; text&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;decoration&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; line&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;through&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;style&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;div &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;"&lt;span&gt;${&lt;span&gt;this&lt;/span&gt;.hasAttribute(&lt;span&gt;'completed'&lt;/span&gt;) ? &lt;span&gt;'completed'&lt;/span&gt; : &lt;span&gt;''&lt;/span&gt;}&lt;/span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;slot&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;slot&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;div&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;;&lt;/span&gt;&lt;span&gt;
  &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;

customElements&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;define&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;'todo-item'&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TodoItem&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;The component above has encapsulated styles and lifecycle methods (&lt;code&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/code&gt; creates the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot"&gt;shadow root&lt;/a&gt;, while &lt;code&gt;&lt;span&gt;connectedCallback&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/code&gt; runs when the element is added to the DOM).&lt;/p&gt;
&lt;p&gt;You can add it to your HTML page like a regular HTML tag:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt; 
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;span&gt;todo-item&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Learn Web Components&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;span&gt;todo-item&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; 

&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt; 
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;span&gt;todo-item&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;completed&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Pick UX over DX&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;span&gt;todo-item&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;There’s no transpilation step (such as converting JSX to JavaScript), no virtual DOM abstraction, and no required build step—though you can still use build tools if you want. The code runs directly in the browser and starts instantly because there’s no framework to initialize.&lt;/p&gt;
&lt;p&gt;On the other hand, Web Components don’t provide built-in reactivity. Unlike frameworks such as React, where you describe the UI and the framework automatically handles updates, Web Components are imperative. You need to manage DOM updates and respond to attribute or state changes manually, unless you introduce a library like &lt;a href="https://lit.dev/"&gt;Lit&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Ideal use case: Leaf components&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Self-contained leaf components – such as emoji pickers, date selectors, and color pickers—work particularly well as Web Components. They live at the edges of the component tree and don’t contain nested children. Because of that, they avoid much of the complexity around server-side rendering and passing content across multiple shadow &lt;a href="https://blog.logrocket.com/exploring-essential-dom-methods-frontend-development/"&gt;DOM&lt;/a&gt; boundaries.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Static content with some interactivity&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Native approaches shine when dealing with mostly static content that needs a small amount of interactivity. Most websites are still just HTML documents with occasional dynamic behavior, which can be added through &lt;a href="https://blog.logrocket.com/understanding-progressive-enhancement/"&gt;progressive enhancement&lt;/a&gt;using vanilla JavaScript.&lt;/p&gt;
&lt;p&gt;For example, here’s how you might enhance a basic contact form with asynchronous submission using the &lt;a href="https://blog.logrocket.com/fetch-api-javascript/"&gt;Fetch API&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;&lt;span&gt;document&lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;querySelector&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;'form'&lt;/span&gt;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;'submit'&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;async&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;
  e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;preventDefault&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;span&gt;
  &lt;/span&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;/span&gt;&lt;span&gt; formData &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;new&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FormData&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;target&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;span&gt;

  &lt;/span&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;/span&gt;&lt;span&gt; response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;await&lt;/span&gt;&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;'/api/submit'&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;
    &lt;span&gt;method&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;'POST'&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;
    &lt;span&gt;body&lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; formData
  &lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;span&gt;

  &lt;/span&gt;&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;
    e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;target&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;innerHTML &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;'&amp;lt;p&amp;gt;Thanks, your message has been sent!&amp;lt;/p&amp;gt;'&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;
  &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;
&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;Other web platform features&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Beyond Web Components and basic interactivity, the web platform now covers most of the responsibilities frameworks were originally designed to handle. Native ES modules and dynamic imports support dependency management and code splitting, import maps simplify working with third-party libraries, and the Fetch API handles network requests. On the styling side, modern CSS features such as animations, container queries, and custom properties make complex, responsive layouts possible without JavaScript-heavy abstractions.&lt;/p&gt;

&lt;h3&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Long-term maintenance tends to favor native approaches. Vanilla JavaScript, written a decade ago, still runs today, while framework-based applications often require substantial rewrites with each major release. Native web APIs don’t depend on external tooling or package ecosystems, and they maintain backward compatibility far more consistently. As a result, the knowledge you invest in the platform stays relevant for years, not just for a few release cycles.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Server-side rendering&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://blog.logrocket.com/csr-ssr-pre-rendering-which-rendering-technique-choose/"&gt;Server-side rendering&lt;/a&gt; once depended heavily on frameworks, but the web platform has made significant progress here as well. &lt;a href="https://web.dev/articles/declarative-shadow-dom"&gt;Declarative Shadow DOM&lt;/a&gt;, now supported across all major browsers, lets you define shadow roots directly in HTML using the &lt;code&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#shadowrootmode"&gt;&lt;span&gt;shadowrootmode&lt;/span&gt;&lt;/a&gt;&lt;/code&gt; attribute, without relying on JavaScript.&lt;/p&gt;
&lt;p&gt;In the example below, the component renders immediately instead of waiting for JavaScript to execute, improving initial paint times. The CSS also responds to attributes – the &lt;code&gt;&lt;span&gt;completed&lt;/span&gt;&lt;/code&gt; state applies a strikethrough purely through the &lt;code&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;host&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;completed&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/code&gt; selector.&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;span&gt;todo-item&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;completed&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
  &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;span&gt;template&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;shadowrootmode&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;"open"&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
    &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;span&gt;style&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;host&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;span&gt;display&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; block&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;span&gt;padding&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;10px&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;host&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;completed&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;]&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;span&gt;div&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;span&gt;text&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;decoration&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; line&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;through&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;span&gt;style&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
    &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;span&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;span&gt;slot&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;span&gt;slot&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;span&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
  &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;span&gt;template&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;
  Pick UX over DX
&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;span&gt;todo-item&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;

&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;span&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;TodoItem&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;span&gt;extends&lt;/span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;HTMLElement&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; 
  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;constructor&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; 
    &lt;span&gt;super&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; 

customElements&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;define&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;'todo-item'&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;TodoItem&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;span&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;
&lt;h2 id="ai-defaults-to-frameworks"&gt;&lt;strong&gt;AI defaults to frameworks&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;AI coding tools reinforce the framework-first default by generating React and other popular framework-based solutions, often paired with tools like Tailwind. This isn’t because these are always the best choices, but because framework-heavy code dominates their training data. Paul Kinlan described this AI-driven effect as &lt;a href="https://aifoc.us/dead-framework-theory/"&gt;dead framework theory&lt;/a&gt;. While his claim that React has permanently won and alternatives are “dead on arrival” may be overly pessimistic, the core observation holds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;An experiment: what it reveals about vibe coding, output quality, and framework weight?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To see whether this theory shows up in practice, I ran a small experiment using Claude to generate a memory game. Different tools may behave differently, so this should be treated as a single data point rather than a universal result.&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;With a very generic prompt (“create a memory game”), Claude defaulted to React and Tailwind&lt;/li&gt;
&lt;li&gt;With slightly more specificity (“create a simple memory game”), it still chose React, but switched from Tailwind to vanilla CSS written inside the component&lt;/li&gt;
&lt;li&gt;With a highly specific prompt that defined the technology (“create a simple HTML memory game”), Claude relied only on native web platform features&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;In the table below, you can see the key findings:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;
&lt;th&gt;V1 (React + Tailwind)&lt;/th&gt;
&lt;th&gt;V2 (React)&lt;/th&gt;
&lt;th&gt;V3 (No framework)&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;“Create a memory game”&lt;/td&gt;
&lt;td&gt;“Create a simple memory game”&lt;/td&gt;
&lt;td&gt;“Create a simple HTML memory game”&lt;/td&gt;
&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Prompt specificity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low (minimal guidance)&lt;/td&gt;
&lt;td&gt;Medium (no tech specified)&lt;/td&gt;
&lt;td&gt;High (HTML specified)&lt;/td&gt;
&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Functionality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Card matching, flip animations, move counter, win detection, reset&lt;/td&gt;
&lt;td&gt;Card matching, flip animations, move counter, win detection, reset&lt;/td&gt;
&lt;td&gt;Card matching, flip animations, move counter, win detection, reset&lt;/td&gt;
&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Frameworks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React, ReactDOM, Tailwind CSS, lucide-react&lt;/td&gt;
&lt;td&gt;React, ReactDOM, lucide-react&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Fonts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Fonts (Space Mono)&lt;/td&gt;
&lt;td&gt;Google Fonts (Fredoka)&lt;/td&gt;
&lt;td&gt;System font (Arial)&lt;/td&gt;
&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Icons&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;lucide-react (UI icons: Sparkles, RotateCcw) + UTF-8 emoji for cards&lt;/td&gt;
&lt;td&gt;lucide-react (8 card icons: Sparkles, Star, Heart, Zap, Sun, Moon, Cloud, Flame)&lt;/td&gt;
&lt;td&gt;UTF-8 emoji (&lt;img alt="&#127918;" src="https://s.w.org/images/core/emoji/17.0.2/svg/1f3ae.svg"&gt;&lt;img alt="&#127919;" src="https://s.w.org/images/core/emoji/17.0.2/svg/1f3af.svg"&gt;&lt;img alt="&#127912;" src="https://s.w.org/images/core/emoji/17.0.2/svg/1f3a8.svg"&gt;&lt;img alt="&#127917;" src="https://s.w.org/images/core/emoji/17.0.2/svg/1f3ad.svg"&gt;&lt;img alt="&#127914;" src="https://s.w.org/images/core/emoji/17.0.2/svg/1f3aa.svg"&gt;&lt;img alt="&#127928;" src="https://s.w.org/images/core/emoji/17.0.2/svg/1f3b8.svg"&gt;&lt;img alt="&#127930;" src="https://s.w.org/images/core/emoji/17.0.2/svg/1f3ba.svg"&gt;&lt;img alt="&#127931;" src="https://s.w.org/images/core/emoji/17.0.2/svg/1f3bb.svg"&gt;)&lt;/td&gt;
&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Core bundle (min + gzip)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;65.62 KB&lt;/td&gt;
&lt;td&gt;63.63 KB&lt;/td&gt;
&lt;td&gt;2.16 KB&lt;/td&gt;
&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Demo links&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.annalytic.com/memory-game-tests/v1/dist/index.html"&gt;&lt;br&gt;
Demo v1&lt;br&gt;&lt;/a&gt;&lt;br&gt;
(React + Tailwind)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.annalytic.com/memory-game-tests/v2/dist/index.html"&gt;&lt;br&gt;
Demo v2&lt;br&gt;&lt;/a&gt;&lt;br&gt;
(React)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.annalytic.com/memory-game-tests/v3/dist/index.html"&gt;&lt;br&gt;
Demo v3&lt;br&gt;&lt;/a&gt;&lt;br&gt;
(No framework)&lt;/td&gt;
&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Screenshot of demo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://blog.logrocket.com/wp-content/uploads/2026/01/image11.png" alt=""&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://blog.logrocket.com/wp-content/uploads/2026/01/image12.png" alt=""&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://blog.logrocket.com/wp-content/uploads/2026/01/image13.png" alt=""&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Bundle size (VSCodium)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://blog.logrocket.com/wp-content/uploads/2026/01/image7.png" alt=""&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://blog.logrocket.com/wp-content/uploads/2026/01/image10.png" alt=""&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://blog.logrocket.com/wp-content/uploads/2026/01/image1.png" alt=""&gt;&lt;/td&gt;
&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;Google Fonts load at runtime (~ 15-30 KB). They’re not included in the core bundle, but they show up in the Network tab in DevTools&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;See &lt;a href="https://github.com/azaleamollis/memory-game-tests"&gt;GitHub repo&lt;/a&gt; for reference&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;As you can see above, the memory game functionality is identical across all versions, and the aesthetics are just slightly better in the framework-based versions, thanks to React Lucide (however, note that you can add &lt;a href="https://lucide.dev/icons/"&gt;Lucide icons as inline SVG&lt;/a&gt; without React). Yet v1 and v2 are both about 30× larger than the vanilla one, with 95% of the bundle being framework overhead. React and Tailwind didn’t make the output noticeably better; they just made it heavier.&lt;/p&gt;
&lt;p&gt;This also demonstrates dead framework theory in action. Without specific guidance, AI defaults to React indeed. You can fight this by increasing prompt specificity, but since most people using AI tools won’t do that, this remains a huge win for frameworkism.&lt;/p&gt;
&lt;h2 id="dx-vs-ux-tradeoff"&gt;&lt;strong&gt;The DX vs UX trade-off&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Frameworks introduce real performance overhead. In the memory game example above, the React versions are roughly 30× larger than the native JavaScript implementation.&lt;/p&gt;
&lt;p&gt;In larger production applications, that overhead grows well beyond the initial ~60 KB as routing, state management, UI libraries, and other utilities are added. A typical React application with a full ecosystem can easily ship 150–300 KB of framework-related code.&lt;/p&gt;
&lt;p&gt;Vanilla applications also grow in size as complexity increases, which makes proportional comparisons harder. Even so, frameworks add a fixed layer of overhead that exists regardless of application scale.&lt;/p&gt;
&lt;p&gt;Lower page weight and faster load times are clear UX wins for an anti-frameworkist approach, but they come with trade-offs in developer experience. Writing Web Components by hand can be verbose. Passing props, handling events, and managing updates requires more manual work than in most frameworks, and state management across many components becomes difficult without established patterns.&lt;/p&gt;&lt;div&gt;
&lt;hr&gt;&lt;h3&gt;More great articles from LogRocket:&lt;/h3&gt;

&lt;hr&gt;&lt;/div&gt;

&lt;p&gt;This is where lighter alternatives can help bridge the gap between vanilla verbosity (a DX cost) and framework bloat (aUX cost):&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;a href="https://htmx.org/"&gt;HTMX&lt;/a&gt; (~14 KB) enhances HTML without JavaScript-heavy frontends&lt;/li&gt;
&lt;li&gt;&lt;a href="https://alpinejs.dev/"&gt;Alpine.js&lt;/a&gt; (~15 KB) provides reactivity directly in HTML&lt;/li&gt;
&lt;li&gt;&lt;a href="https://preactjs.com/"&gt;Preact&lt;/a&gt; (~3 KB) offers a React-compatible API in a tiny package&lt;em&gt;, &lt;/em&gt;perfect as a drop-in React replacement&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lit.dev/"&gt;Lit&lt;/a&gt; (~5 KB) is &lt;a href="https://lit.dev/articles/lit-for-polymer-users/"&gt;Polymer’s successor&lt;/a&gt;; it provides Web Components with reactive templates and scoped styles&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.solidjs.com/"&gt;Solid&lt;/a&gt; (~7 KB) delivers React-like syntax without virtual DOM overhead&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;These libraries improve developer experience compared to fully vanilla JavaScript while keeping bundle sizes in check. That said, users don’t care about your DX. They care that the site loads quickly on their device and connection, and that it does what they came for.&lt;/p&gt;
&lt;p&gt;For small to medium applications, the DX benefits of heavyweight frameworks like React rarely justify the UX costs they introduce. They can still make sense for enterprise-scale, highly interactive dashboards built by large teams, but those cases represent a small minority of the websites and applications that use them today.&lt;/p&gt;
&lt;h2 id="prioritize-users-stay-in-reality"&gt;&lt;strong&gt;What’s next? Prioritize users, stay in reality&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Web platform features work at scale. When &lt;a href="https://medium.com/dev-channel/a-netflix-web-performance-case-study-c0bcde26a9d9"&gt;Netflix&lt;/a&gt; replaced React with vanilla JavaScript on its landing page, load time and &lt;a href="https://web.dev/articles/tti"&gt;Time to Interactive&lt;/a&gt; dropped by more than 50 percent, and the JavaScript bundle shrank by roughly 200 KB. &lt;a href="https://github.blog/engineering/how-we-use-web-components-at-github/"&gt;GitHub&lt;/a&gt;relies heavily on custom Web Components through &lt;a href="https://github.github.io/catalyst/"&gt;Catalyst&lt;/a&gt;, its open-source library for reducing boilerplate. &lt;a href="https://web.dev/articles/ps-on-the-web"&gt;Adobe moved Photoshop&lt;/a&gt; to the web using Lit-based Web Components. These aren’t edge cases – they show that the web platform is production-ready.&lt;/p&gt;
&lt;p&gt;The job market, however, is messier. &lt;a href="https://arxiv.org/abs/2101.12703"&gt;Resume-driven&lt;/a&gt; development is a well-documented reality, and React still dominates job listings. To stay employable, you’ll almost certainly need to learn React. But the job market reflects what companies already use, not what’s technically best for new projects. To become excellent – not just employable – you need to understand native web APIs and know when each approach makes sense.&lt;/p&gt;
&lt;p&gt;Anti-frameworkism isn’t about rejecting tools. It’s about starting from the problem instead of the trend, weighing real-world impact before convenience, and choosing the technology that best serves your users.&lt;/p&gt;


&lt;/div&gt;
            

            &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Anna Monus</name>
        </author>
        <media:content medium="image" url="https://blog.logrocket.com/wp-content/uploads/2026/01/anti-frameworkism.png"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19c178f4faa:5fd687:a933d714</id>
        <title type="html">Reducing FOUC with Web Components</title>
        <published>2026-02-01T04:56:33Z</published>
        <updated>2026-02-01T04:56:37Z</updated>
        <link href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh" rel="alternate" type="text/html"/>
        <summary type="html">Learn practical techniques for reducing the flash of unstyled content (FOUC) as your web components/custom elements are defined to improve your users' experience</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div id="article-body"&gt;
                &lt;h2&gt;
  &lt;a name="the-problem" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#the-problem"&gt;
  &lt;/a&gt;
  The Problem
&lt;/h2&gt;

&lt;p&gt;One of the best things about web components is that they are &lt;em&gt;wicked&lt;/em&gt; fast, but one of the common complaints I hear is that, because they are a client-side technology, there can be a delay between when the page renders and the components are defined and rendered. This can result in a flash of unstyled content (FOUC) and page content shifts - that jarring moment when your carefully crafted layout suddenly jumps around before your users' eyes. Not exactly the first impression we're going for.&lt;/p&gt;

&lt;h2&gt;
  &lt;a name="the-old-solution" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#the-old-solution"&gt;
  &lt;/a&gt;
  The Old Solution
&lt;/h2&gt;

&lt;p&gt;Here's the solution that's been making the rounds for ages:&lt;br&gt;&lt;/p&gt;

&lt;div&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;:not&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;:defined&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;visibility&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;hidden&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
    &lt;svg&gt;Enter fullscreen mode
    
&lt;/svg&gt;&lt;svg&gt;Exit fullscreen mode
    
&lt;/svg&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  &lt;a name="whats-happening" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#whats-happening"&gt;
  &lt;/a&gt;
  What's happening?
&lt;/h3&gt;

&lt;p&gt;This CSS selector targets any custom element that hasn't been defined yet and visually hides it from view. Once the JavaScript defines the custom element, the browser marks it as &lt;code&gt;:defined&lt;/code&gt;, the selector no longer matches, and the component becomes visible.&lt;/p&gt;

&lt;h3&gt;
  &lt;a name="pros" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#pros"&gt;
  &lt;/a&gt;
  Pros
&lt;/h3&gt;

&lt;ul&gt;&lt;li&gt;
&lt;strong&gt;Performant and simple&lt;/strong&gt;: Just drop it in a CSS reset or theme file, and you're done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero setup required&lt;/strong&gt;: Developers using your components don't need to do anything special - it just works out of the box.&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;
  &lt;a name="cons" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#cons"&gt;
  &lt;/a&gt;
  Cons
&lt;/h3&gt;

&lt;ul&gt;&lt;li&gt;
&lt;strong&gt;Layout shifts&lt;/strong&gt;: Because this only hides the component visually (the element still takes up space in the layout). The element initially renders as an unstyled &lt;code&gt;HTMLUnknownElement&lt;/code&gt;, which has no intrinsic size. As components load and apply their styles, it can result in content jumps as the page reflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures&lt;/strong&gt;: If a component fails to define (network issues, JavaScript errors, etc.), it will never be shown. This might be acceptable in some cases, but it's problematic if you're using custom elements for progressive enhancement or mixing defined and undefined custom elements in your application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility issues&lt;/strong&gt;: Using &lt;code&gt;visibility: hidden;&lt;/code&gt; can remove the content from the accessibility tree, which means screen readers and other assistive technologies can't access it. Your content is invisible to both sighted users and accessibility tools, which can affect initial focus.&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;
  &lt;a name="solving-it-with-javascript" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#solving-it-with-javascript"&gt;
  &lt;/a&gt;
  Solving it with JavaScript
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://dev.to/stuffbreaker/do-you-need-to-ssr-your-web-components-2oao#but-what-about-fouc"&gt;previous article I wrote&lt;/a&gt;, I show how to use native JavaScript APIs to improve upon the original solution:&lt;br&gt;&lt;/p&gt;

&lt;div&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span&gt;/* Visually block initial render as components get defined */&lt;/span&gt;
  &lt;span&gt;body&lt;/span&gt;&lt;span&gt;:not&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;.wc-loaded&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span&gt;&amp;lt;script &lt;/span&gt;&lt;span&gt;type=&lt;/span&gt;&lt;span&gt;"module"&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;
  &lt;span&gt;(()&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;// Select all undefined custom elements on the page and wait for them to be loaded.&lt;/span&gt;
    &lt;span&gt;// Once they are all loaded, add the `wc-loaded` class to the body to make it visible again.&lt;/span&gt;
    &lt;span&gt;Promise&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;allSettled&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
      &lt;span&gt;[...&lt;/span&gt;&lt;span&gt;document&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;querySelectorAll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;:not(:defined)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)]&lt;/span&gt;
        &lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;component&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;return&lt;/span&gt; &lt;span&gt;customElements&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;whenDefined&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;component&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;localName&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
        &lt;span&gt;})&lt;/span&gt;
    &lt;span&gt;).&lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;span&gt;(()&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;document&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;classList&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;wc-loaded&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;

    &lt;span&gt;// Add fallback to add the `wc-loaded` class to the body to make it visible after 200ms. &lt;/span&gt;
    &lt;span&gt;// This prevents the user from being blocked from using your application in case a component fails to load&lt;/span&gt;
    &lt;span&gt;// or other undefined elements are being used on the page.&lt;/span&gt;
    &lt;span&gt;setTimeout&lt;/span&gt;&lt;span&gt;(()&lt;/span&gt; &lt;span&gt;=&amp;gt;&lt;/span&gt; &lt;span&gt;document&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;classList&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;wc-loaded&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;),&lt;/span&gt; &lt;span&gt;200&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
  &lt;span&gt;})();&lt;/span&gt;
&lt;span&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
    &lt;svg&gt;Enter fullscreen mode
    
&lt;/svg&gt;&lt;svg&gt;Exit fullscreen mode
    
&lt;/svg&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  &lt;a name="whats-happening" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#whats-happening"&gt;
  &lt;/a&gt;
  What's happening?
&lt;/h3&gt;

&lt;p&gt;This approach waits for all custom elements to be defined before revealing the page. Here's how it works:&lt;/p&gt;

&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hide the body&lt;/strong&gt;: We set &lt;code&gt;opacity: 0&lt;/code&gt; on the body until it gets the &lt;code&gt;wc-loaded&lt;/code&gt; class. Unlike &lt;code&gt;visibility: hidden&lt;/code&gt;, this keeps content in the accessibility tree - screen readers can still access it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Query for undefined elements&lt;/strong&gt;: &lt;code&gt;document.querySelectorAll(":not(:defined)")&lt;/code&gt; finds all custom elements that haven't been defined yet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wait for definition&lt;/strong&gt;: For each undefined element, we use &lt;code&gt;customElements.whenDefined()&lt;/code&gt;, which returns a Promise that resolves when that custom element gets defined.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wait for all components&lt;/strong&gt;: &lt;code&gt;Promise.allSettled()&lt;/code&gt; waits for all those Promises to complete (whether they succeed or fail), then adds the &lt;code&gt;wc-loaded&lt;/code&gt; class to make everything visible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fallback timeout&lt;/strong&gt;: The &lt;code&gt;setTimeout&lt;/code&gt; ensures that even if something goes wrong, users aren't staring at a blank page forever. After 200ms, we show the page regardless.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Here is a &lt;a href="https://break-stuff.github.io/wc-fouc-test/" target="_blank" rel="noopener noreferrer"&gt;demo&lt;/a&gt; you can play with.&lt;/p&gt;

&lt;h3&gt;
  &lt;a name="pros" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#pros"&gt;
  &lt;/a&gt;
  Pros
&lt;/h3&gt;

&lt;ul&gt;&lt;li&gt;
&lt;strong&gt;Accessibility-friendly&lt;/strong&gt;: By using &lt;code&gt;opacity&lt;/code&gt; instead of &lt;code&gt;visibility: hidden&lt;/code&gt;, the page content remains available to assistive technologies. Users with screen readers aren't left in the dark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful degradation&lt;/strong&gt;: Provides a fallback in case components fail to load or if undefined custom elements are also being used on the page. Your users will see &lt;em&gt;something&lt;/em&gt; rather than nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Very performant&lt;/strong&gt;: Modern browsers handle opacity changes efficiently, and the JavaScript overhead is minimal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future-proof&lt;/strong&gt;: By adding a class instead of directly manipulating styles, you avoid issues where new components added after page load might cause the opacity to flash in and out.&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;
  &lt;a name="cons" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#cons"&gt;
  &lt;/a&gt;
  Cons
&lt;/h3&gt;

&lt;ul&gt;&lt;li&gt;
&lt;strong&gt;Setup required&lt;/strong&gt;: Depending on your architecture, this might need to be included in your base template or injected into every page. It's not quite as "set it and forget it" as a CSS-only solution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript dependency&lt;/strong&gt;: Requires JavaScript to execute on the page before content is visible. If JS is disabled or fails to load, users see nothing (though the timeout helps mitigate this).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module timing&lt;/strong&gt;: The script needs to run as a module, which means it's deferred and runs after the DOM is loaded. This can introduce a small delay compared to inline scripts or CSS-only solutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Potential failure point&lt;/strong&gt;: If JavaScript is disabled or fails to load the script, it could prevent the page from loading properly.&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;
  &lt;a name="solving-it-with-css" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#solving-it-with-css"&gt;
  &lt;/a&gt;
  Solving it with CSS
&lt;/h2&gt;

&lt;p&gt;What if we could have the best of both worlds - a solution that's easy to set up, lightweight, doesn't require additional JavaScript to run, &lt;em&gt;and&lt;/em&gt; provides a fallback if components fail to load? Here's a CSS-only approach:&lt;br&gt;&lt;/p&gt;

&lt;div&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;body&lt;/span&gt;&lt;span&gt;:has&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;:not&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;:defined&lt;/span&gt;&lt;span&gt;))&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;var&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;--wc-loaded&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
  &lt;span&gt;animation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;showBody&lt;/span&gt; &lt;span&gt;0s&lt;/span&gt; &lt;span&gt;linear&lt;/span&gt; &lt;span&gt;100ms&lt;/span&gt; &lt;span&gt;forwards&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;@keyframes&lt;/span&gt; &lt;span&gt;showBody&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;to&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;--wc-loaded&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
    &lt;svg&gt;Enter fullscreen mode
    
&lt;/svg&gt;&lt;svg&gt;Exit fullscreen mode
    
&lt;/svg&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  &lt;a name="whats-happening" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#whats-happening"&gt;
  &lt;/a&gt;
  What's happening?
&lt;/h3&gt;

&lt;p&gt;This solution uses modern CSS features to handle component loading. Here's the breakdown:&lt;/p&gt;

&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;:has()&lt;/code&gt; selector&lt;/strong&gt;: This checks if the body contains any elements matching &lt;code&gt;:not(:defined)&lt;/code&gt;. If it finds undefined custom elements, the styles apply.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Initial hiding&lt;/strong&gt;: &lt;code&gt;opacity: 0&lt;/code&gt; makes the body invisible while undefined components are present.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Animation-based timeout&lt;/strong&gt;: We set up an animation with 0 seconds duration but a 100ms delay. This animation's purpose is to set &lt;code&gt;opacity: 1&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic reveal&lt;/strong&gt;: As soon as all custom elements become defined, the &lt;code&gt;:has(:not(:defined))&lt;/code&gt; selector no longer matches, the animation is removed, and the browser resets to the default &lt;code&gt;opacity: 1&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Built-in fallback&lt;/strong&gt;: If components fail to load or take too long, the animation completes after 100ms anyway, forcing &lt;code&gt;opacity: 1&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prevent flashes&lt;/strong&gt;: The &lt;code&gt;--wc-loaded&lt;/code&gt; sets the state moving forward, which will prevent the opacity from flashing when new undefined components are added to the page.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Here is a &lt;a href="https://break-stuff.github.io/css-reduced-web-component-fouc/" target="_blank" rel="noopener noreferrer"&gt;demo&lt;/a&gt; you can play with.&lt;/p&gt;

&lt;h3&gt;
  &lt;a name="pros" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#pros"&gt;
  &lt;/a&gt;
  Pros
&lt;/h3&gt;

&lt;ul&gt;&lt;li&gt;
&lt;strong&gt;Pure CSS solution&lt;/strong&gt;: No JavaScript required! This means it works even in environments where JS is disabled or fails to load, and it executes immediately without waiting for the DOM or module loading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic fallback&lt;/strong&gt;: The animation delay provides a built-in timeout. If components fail to define, the page still becomes visible after 100ms. No need for manual &lt;code&gt;setTimeout&lt;/code&gt; calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight&lt;/strong&gt;: Just a few lines of CSS - no extra scripts, no DOM queries, no Promise wrangling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reactive&lt;/strong&gt;: The &lt;code&gt;:has()&lt;/code&gt; selector automatically updates when elements become defined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to customize&lt;/strong&gt;: Want a longer timeout? Change the animation delay. Want to target specific containers instead of the whole body? Adjust the selector. Timeouts and animation timing can be made configurable using CSS variables. It's flexible without being complicated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility-friendly&lt;/strong&gt;: Like the JS solution, this uses &lt;code&gt;opacity&lt;/code&gt; rather than &lt;code&gt;visibility&lt;/code&gt;, keeping content available to assistive technologies.&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;
  &lt;a name="cons" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#cons"&gt;
  &lt;/a&gt;
  Cons
&lt;/h3&gt;

&lt;ul&gt;&lt;li&gt;
&lt;strong&gt;Browser support&lt;/strong&gt;: The &lt;code&gt;:has()&lt;/code&gt; selector is relatively new. It's supported in all modern browsers, but may not be supported in older versions. The good news is that if something is not supported, it will gracefully fail rather than crash your app.&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;
  &lt;a name="wrapping-up" href="https://dev.to/stuffbreaker/reducing-fouc-with-web-components-1jnh#wrapping-up"&gt;
  &lt;/a&gt;
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;These solutions will continue to evolve, but FOUC doesn't have to be a necessary evil of using web components. With these techniques in your toolkit, you can provide smooth, accessible experiences for your users and boost those Core Web Vitals scores without sacrificing the benefits of web components.&lt;/p&gt;


            &lt;/div&gt;

              &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://media2.dev.to/dynamic/image/width=1000,height=500,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcvd0gntbnjfkf1ezk6if.jpeg"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19be576c745:21579:56c6ba3f</id>
        <title type="html">My Opinionated CSS Reset</title>
        <published>2026-01-22T11:28:45Z</published>
        <updated>2026-01-23T09:51:26Z</updated>
        <link href="https://vale.rocks/posts/css-reset" rel="alternate" type="text/html"/>
        <summary type="html">* { all: unset; }</summary>
        <content type="html">
&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
&lt;p&gt;As years have stretched on, browser user-agent styles have grown somewhat estranged from how many developers use the web platform. I am no exception to this rule and find my own needs at odds with the predefined user-agent stylesheets of major browsers:&lt;/p&gt;
&lt;p&gt;As such, I, like many others&lt;sup&gt;&lt;/sup&gt;, have a &lt;abbr&gt;CSS&lt;/abbr&gt; reset that I apply to many projects as part of an effort to ensure comfortable development. ‘Reset’ is perhaps not quite the correct diction, as much of this is opinionated and not purely returning to a clean slate. We’re mostly past the days of rogues like Internet Explorer, and browsers are &lt;em&gt;mostly&lt;/em&gt;&lt;sup&gt;&lt;/sup&gt; consistent with their styling.&lt;/p&gt;
&lt;p&gt;Despite ‘reset’ not being the most accurate term, the more correct title of ‘Preferred &lt;abbr&gt;CSS&lt;/abbr&gt; defaults and user-agent overrides’ just doesn’t come with quite the same panache.&lt;/p&gt;
&lt;p&gt;Here is my complete unabridged reset:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;@layer&lt;/span&gt; {
	*,
	*&lt;span&gt;::before&lt;/span&gt;,
	*&lt;span&gt;::after&lt;/span&gt; {
		&lt;span&gt;box-sizing&lt;/span&gt;: border-box;
		&lt;span&gt;background-repeat&lt;/span&gt;: no-repeat;
	}

	* {
		&lt;span&gt;padding&lt;/span&gt;: &lt;span&gt;0&lt;/span&gt;;
		&lt;span&gt;margin&lt;/span&gt;: &lt;span&gt;0&lt;/span&gt;;
	}

	&lt;span&gt;html&lt;/span&gt; {
		-webkit-&lt;span&gt;text-size-adjust&lt;/span&gt;: none;
		&lt;span&gt;text-size-adjust&lt;/span&gt;: none;
		&lt;span&gt;line-height&lt;/span&gt;: &lt;span&gt;1.5&lt;/span&gt;;
		-webkit-&lt;span&gt;font-smoothing&lt;/span&gt;: antialiased;
        &lt;span&gt;hanging-punctuation&lt;/span&gt;: first allow-end last;
	}

	&lt;span&gt;body&lt;/span&gt; {
		&lt;span&gt;min-block-size&lt;/span&gt;: &lt;span&gt;100%&lt;/span&gt;;
	}

	&lt;span&gt;img&lt;/span&gt;,
	&lt;span&gt;iframe&lt;/span&gt;,
	&lt;span&gt;audio&lt;/span&gt;,
	&lt;span&gt;video&lt;/span&gt;,
	&lt;span&gt;canvas&lt;/span&gt; {
		&lt;span&gt;display&lt;/span&gt;: block;
		&lt;span&gt;max-inline-size&lt;/span&gt;: &lt;span&gt;100%&lt;/span&gt;;
	}

	&lt;span&gt;svg&lt;/span&gt; {
		&lt;span&gt;max-inline-size&lt;/span&gt;: &lt;span&gt;100%&lt;/span&gt;;
	}

	&lt;span&gt;svg&lt;/span&gt;&lt;span&gt;:not&lt;/span&gt;(&lt;span&gt;[fill]&lt;/span&gt;) {
		&lt;span&gt;fill&lt;/span&gt;: currentColor;
	}

	&lt;span&gt;input&lt;/span&gt;,
	&lt;span&gt;button&lt;/span&gt;,
	&lt;span&gt;textarea&lt;/span&gt;,
	&lt;span&gt;select&lt;/span&gt; {
		&lt;span&gt;font&lt;/span&gt;: inherit;
	}

	&lt;span&gt;textarea&lt;/span&gt; {
		&lt;span&gt;resize&lt;/span&gt;: vertical;
	}

	&lt;span&gt;fieldset&lt;/span&gt;,
	&lt;span&gt;iframe&lt;/span&gt; {
		&lt;span&gt;border&lt;/span&gt;: none;
	}

	&lt;span&gt;p&lt;/span&gt;,
	&lt;span&gt;h1&lt;/span&gt;,
	&lt;span&gt;h2&lt;/span&gt;,
	&lt;span&gt;h3&lt;/span&gt;,
	&lt;span&gt;h4&lt;/span&gt;,
	&lt;span&gt;h5&lt;/span&gt;,
	&lt;span&gt;h6&lt;/span&gt; {
		&lt;span&gt;overflow-wrap&lt;/span&gt;: break-word;
	}

	&lt;span&gt;p&lt;/span&gt; {
		&lt;span&gt;text-wrap&lt;/span&gt;: pretty;
		&lt;span&gt;font-variant-numeric&lt;/span&gt;: proportional-nums;
	}

	&lt;span&gt;h1&lt;/span&gt;,
	&lt;span&gt;h2&lt;/span&gt;,
	&lt;span&gt;h3&lt;/span&gt;,
	&lt;span&gt;h4&lt;/span&gt;,
	&lt;span&gt;h5&lt;/span&gt;,
	&lt;span&gt;h6&lt;/span&gt; {
		&lt;span&gt;font-variant-numeric&lt;/span&gt;: lining-nums;
	}


	&lt;span&gt;input&lt;/span&gt;,
	&lt;span&gt;label&lt;/span&gt;,
	&lt;span&gt;button&lt;/span&gt;,
	&lt;span&gt;h1&lt;/span&gt;,
	&lt;span&gt;h2&lt;/span&gt;,
	&lt;span&gt;h3&lt;/span&gt;,
	&lt;span&gt;h4&lt;/span&gt;,
	&lt;span&gt;h5&lt;/span&gt;,
	&lt;span&gt;h6&lt;/span&gt; {
        &lt;span&gt;line-height&lt;/span&gt; &lt;span&gt;1.1&lt;/span&gt;;
    }

	math,
	&lt;span&gt;time&lt;/span&gt;,
	&lt;span&gt;table&lt;/span&gt; {
		&lt;span&gt;font-variant-numeric&lt;/span&gt;: tabular-nums lining-nums slashed-zero;
	}

	&lt;span&gt;code&lt;/span&gt; {
		&lt;span&gt;font-variant-numeric&lt;/span&gt;: slashed-zero;
	}

	&lt;span&gt;table&lt;/span&gt; {
		&lt;span&gt;border-collapse&lt;/span&gt;: collapse;
	}

	&lt;span&gt;abbr&lt;/span&gt; {
		&lt;span&gt;font-variant-caps&lt;/span&gt;: all-small-caps;
		&lt;span&gt;text-decoration&lt;/span&gt;: none;

		&amp;amp;&lt;span&gt;[title]&lt;/span&gt; {
			&lt;span&gt;cursor&lt;/span&gt;: help;
			&lt;span&gt;text-decoration&lt;/span&gt;: underline dotted;
		}
	}

	&lt;span&gt;sup&lt;/span&gt;,
	sub {
		&lt;span&gt;line-height&lt;/span&gt;: &lt;span&gt;0&lt;/span&gt;;
	}

	&lt;span&gt;:disabled&lt;/span&gt; {
		&lt;span&gt;opacity&lt;/span&gt;: &lt;span&gt;0.8&lt;/span&gt;;
		&lt;span&gt;cursor&lt;/span&gt;: not-allowed;
	}

	&lt;span&gt;:focus-visible&lt;/span&gt; {
		&lt;span&gt;outline-offset&lt;/span&gt;: &lt;span&gt;0.2rem&lt;/span&gt;;
	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Breakdown&lt;/h2&gt;
&lt;p&gt;The first thing you may notice in my reset is that it is entirely contained within an anonymous layer. Placing the reset on an anonymous cascade layer using the &lt;code&gt;@layer&lt;/code&gt; at-rule &lt;a href="https://www.matuzo.at/blog/2026/lowering-specificity-of-multiple-rules"&gt;gives every declaration a low specificity&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*,
*&lt;span&gt;::before&lt;/span&gt;,
*&lt;span&gt;::after&lt;/span&gt; {
	&lt;span&gt;box-sizing&lt;/span&gt;: border-box;
	&lt;span&gt;background-repeat&lt;/span&gt;: no-repeat;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I find &lt;code&gt;content-box&lt;/code&gt; to be unintuitive and confusing. I much prefer &lt;code&gt;border-box&lt;/code&gt;’s inclusion of an element’s padding and border in the width and height as a default.&lt;/p&gt;
&lt;p&gt;Backgrounds repeating has always seemed to me like an unreasonable default that is overwritten more often than not, so I disable it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* {
	&lt;span&gt;padding&lt;/span&gt;: &lt;span&gt;0&lt;/span&gt;;
	&lt;span&gt;margin&lt;/span&gt;: &lt;span&gt;0&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When working with custom &lt;abbr&gt;CSS&lt;/abbr&gt;, I find browser default margins and paddings to be a hindrance rather than a help. They provide somewhat of a reasonable default when working with unstyled documents&lt;sup&gt;&lt;/sup&gt; but get in the way more often than not when writing my own styling.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;html&lt;/span&gt; {
	-webkit-&lt;span&gt;text-size-adjust&lt;/span&gt;: none;
	&lt;span&gt;text-size-adjust&lt;/span&gt;: none;
	&lt;span&gt;line-height&lt;/span&gt;: &lt;span&gt;1.5&lt;/span&gt;;
	-webkit-&lt;span&gt;font-smoothing&lt;/span&gt;: antialiased;
	&lt;span&gt;hanging-punctuation&lt;/span&gt;: first allow-end last;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;text-size-adjust&lt;/code&gt; stops mobile browsers from trying to adjust text sizes for mobile, which is more harm than help when already designing with mobile in mind. Kilian Valkhof covers it nicely in &lt;a href="https://kilianvalkhof.com/2022/css-html/your-css-reset-needs-text-size-adjust-probably/"&gt;Your &lt;abbr&gt;CSS&lt;/abbr&gt; reset needs text-size-adjust (probably)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The user-agent default line-height is just too small. It makes text cramped and difficult to read.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-webkit-font-smoothing&lt;/code&gt; is a &lt;a href="https://dbushell.com/2024/11/05/webkit-font-smoothing/"&gt;very specific fix to the way macOS renders fonts&lt;/a&gt;. Adding it stops type from appearing thicker than it should on macOS.&lt;/p&gt;
&lt;p&gt;Hanging punctuation just looks better. At time of writing no browsers support it, but they will one day, and I’ll be ready.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;body&lt;/span&gt; {
	&lt;span&gt;min-block-size&lt;/span&gt;: &lt;span&gt;100%&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Stops the body from collapsing, which is useful if the content is less than it takes to fill the viewport.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;img&lt;/span&gt;,
&lt;span&gt;iframe&lt;/span&gt;,
&lt;span&gt;audio&lt;/span&gt;,
&lt;span&gt;video&lt;/span&gt;,
&lt;span&gt;canvas&lt;/span&gt; {
	&lt;span&gt;display&lt;/span&gt;: block;
	&lt;span&gt;max-inline-size&lt;/span&gt;: &lt;span&gt;100%&lt;/span&gt;;
}

&lt;span&gt;svg&lt;/span&gt; {
	&lt;span&gt;max-inline-size&lt;/span&gt;: &lt;span&gt;100%&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The vast majority of the time when using media, I don’t intend for it to display inline (&lt;abbr&gt;SVG&lt;/abbr&gt;s being the exception). I also very rarely wish for content to be larger than the container, so a &lt;code&gt;max-inline-size&lt;/code&gt; is a reasonable method of addressing inline overflows.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;svg&lt;/span&gt;&lt;span&gt;:not&lt;/span&gt;(&lt;span&gt;[fill]&lt;/span&gt;) {
	&lt;span&gt;fill&lt;/span&gt;: currentColor;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It is reasonable for the fill colour of an &lt;abbr&gt;SVG&lt;/abbr&gt; to default to the current colour rather than black if there is no fill already defined.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;input&lt;/span&gt;,
&lt;span&gt;button&lt;/span&gt;,
&lt;span&gt;textarea&lt;/span&gt;,
&lt;span&gt;select&lt;/span&gt; {
	&lt;span&gt;font&lt;/span&gt;: inherit;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Input elements should not use different font styling by default. It makes them immediately feel out of place.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;textarea&lt;/span&gt; {
	&lt;span&gt;resize&lt;/span&gt;: vertical;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;textarea&lt;/code&gt;s usually only need vertical resizing, not horizontal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;fieldset&lt;/span&gt;,
&lt;span&gt;iframe&lt;/span&gt; {
	&lt;span&gt;border&lt;/span&gt;: none;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Fieldsets are good for grouping, but the border is ugly. I personally always opt for another method of visual clustering, so I default to removing the border. I used to manually remove the border on a case-by-case basis but eventually realised that cases of me keeping it were outliers.&lt;/p&gt;
&lt;p&gt;iframes are usually presented to integrate with a page, not stand out. Thus, I think no border is a more reasonable default.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;p&lt;/span&gt;,
&lt;span&gt;h1&lt;/span&gt;,
&lt;span&gt;h2&lt;/span&gt;,
&lt;span&gt;h3&lt;/span&gt;,
&lt;span&gt;h4&lt;/span&gt;,
&lt;span&gt;h5&lt;/span&gt;,
&lt;span&gt;h6&lt;/span&gt; {
	&lt;span&gt;overflow-wrap&lt;/span&gt;: break-word;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A common cause of horizontal overflows – especially given the rave popularity of large font sizes – it is only reasonable to break them.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;p&lt;/span&gt; {
	&lt;span&gt;text-wrap&lt;/span&gt;: pretty;
	&lt;span&gt;font-variant-numeric&lt;/span&gt;: proportional-nums;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Not very well supported, but I’m a typography snob, and any improvement helps.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proportional-nums&lt;/code&gt; enables numerals whose widths vary naturally instead of all taking up the same fixed width.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;h1&lt;/span&gt;,
&lt;span&gt;h2&lt;/span&gt;,
&lt;span&gt;h3&lt;/span&gt;,
&lt;span&gt;h4&lt;/span&gt;,
&lt;span&gt;h5&lt;/span&gt;,
&lt;span&gt;h6&lt;/span&gt; {
	&lt;span&gt;font-variant-numeric&lt;/span&gt;: lining-nums;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I like to make sure that I don’t have &lt;code&gt;oldstyle-nums&lt;/code&gt; in my headings, as they always look out of place. As an aside, I really am looking forward to &lt;code&gt;:heading&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I don’t set any further rules on my headings as I configure them on a per-project basis. &lt;code&gt;text-wrap: balance;&lt;/code&gt; is a common addition.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;input&lt;/span&gt;,
&lt;span&gt;label&lt;/span&gt;,
&lt;span&gt;button&lt;/span&gt;,
&lt;span&gt;h1&lt;/span&gt;,
&lt;span&gt;h2&lt;/span&gt;,
&lt;span&gt;h3&lt;/span&gt;,
&lt;span&gt;h4&lt;/span&gt;,
&lt;span&gt;h5&lt;/span&gt;,
&lt;span&gt;h6&lt;/span&gt; {
    &lt;span&gt;line-height&lt;/span&gt; &lt;span&gt;1.1&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Headings and inputs should have smaller line heights. Headings so that they don’t appear split, and inputs because there is such a small amount of text it offends legibility. While this is a reasonable default, if large ascenders/descenders are present on a heading font, it may need to be re-evaluated.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;math,
&lt;span&gt;time&lt;/span&gt;,
&lt;span&gt;table&lt;/span&gt; {
	&lt;span&gt;font-variant-numeric&lt;/span&gt;: tabular-nums lining-nums slashed-zero;
}

&lt;span&gt;code&lt;/span&gt; {
	&lt;span&gt;font-variant-numeric&lt;/span&gt;: slashed-zero;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When clarity is necessary, such as with times, maths, or code, some typographical changes are always necessary. &lt;code&gt;tabular-nums&lt;/code&gt; and &lt;code&gt;lining-nums&lt;/code&gt; keep numbers aligned and consistent, making data easier to read.&lt;/p&gt;
&lt;p&gt;Slashed zeros (&lt;span&gt;0&lt;/span&gt;) remove visual ambiguity.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;table&lt;/span&gt; {
	&lt;span&gt;border-collapse&lt;/span&gt;: collapse;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Non-collapsed borders feel very 90s and are visually overwhelming.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;abbr&lt;/span&gt; {
	&lt;span&gt;font-variant-caps&lt;/span&gt;: all-small-caps;
	&lt;span&gt;text-decoration&lt;/span&gt;: none;

	&amp;amp;&lt;span&gt;[title]&lt;/span&gt; {
		&lt;span&gt;cursor&lt;/span&gt;: help;
		&lt;span&gt;text-decoration&lt;/span&gt;: underline dotted;
	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;&amp;lt;abbr&gt;&lt;/code&gt; is an odd element, really. The &lt;code&gt;title&lt;/code&gt; attribute aspect isn’t well exposed and is &lt;a href="https://adrianroselli.com/2024/01/using-abbr-element-with-title-attribute.html"&gt;only really usable with a pointer&lt;/a&gt;. I still like to cover it, but this should be kept in mind.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;sup&lt;/span&gt;,
sub {
	&lt;span&gt;line-height&lt;/span&gt;: &lt;span&gt;0&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Superscript and subscript annoyingly meddle with line heights, which I dislike. This overrides that behaviour.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;:disabled&lt;/span&gt; {
	&lt;span&gt;opacity&lt;/span&gt;: &lt;span&gt;0.8&lt;/span&gt;;
	&lt;span&gt;cursor&lt;/span&gt;: not-allowed;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Firefox is the only major browser that doesn’t reduce the opacity of disabled elements, so I reduce it for parity. I also apply a &lt;code&gt;not-allowed&lt;/code&gt; cursor for some further clarity. Care must be taken here, as this could cause text to have insufficient contrast.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;:focus-visible&lt;/span&gt; {
	&lt;span&gt;outline-offset&lt;/span&gt;: &lt;span&gt;0.2rem&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Focus outlines are good, but when they’re too close, they’re often difficult to see. A slight offset helps address this.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</content>
        <author>
            <name>Declan Chidlow</name>
        </author>
        <media:content medium="image" url="https://vale.rocks/assets/og/post.webp"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://vale.rocks/posts/feed.xml</id>
            <title type="html">Vale.Rocks</title>
            <updated>2026-01-23T09:51:26Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19bea068ca1:402651:6fdf6456</id>
        <title type="html">Understanding the fundamentals of CSS Layout</title>
        <published>2026-01-23T08:44:16Z</published>
        <updated>2026-01-23T08:44:20Z</updated>
        <link href="https://polypane.app/blog/understanding-the-fundamentals-of-css-layout/" rel="alternate" type="text/html"/>
        <summary type="html">When developers say that CSS is hard, they're usually talking about CSS layout. What often gets omitted though is that developers are assumed to understand and…</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div id="article"&gt;&lt;p&gt;When developers say that CSS is hard, they're usually talking about CSS layout. What often gets omitted though is that developers are assumed to understand and effectively use CSS without being taught how it works in the first place.&lt;/p&gt;&lt;p&gt;I think this is because the syntax of CSS is simple, especially compared to JavaScript and even to HTML.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;select-thing-here&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;guess-a-property&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; give-it-a-value&lt;span&gt;;&lt;/span&gt;
  
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It leads to the impression that all you need to write CSS is to understand the syntax and have a reference for the properties and values.&lt;/p&gt;&lt;p&gt;That, of course, is not true.&lt;/p&gt;&lt;p&gt;Imagine if you were assumed to learn JS by just looking at its syntax. How would you ever learn what &lt;code&gt;this&lt;/code&gt; refers to, or how closures work, or how prototypal inheritance works? It would make no sense!&lt;/p&gt;&lt;p&gt;CSS is no less deserving of understanding its underlying concepts, so let's build that foundation.&lt;/p&gt;&lt;h2 id="prefer-video"&gt;Prefer video?&lt;/h2&gt;&lt;p&gt;This article is a written version of a talk by Kilian Valkhof, Polypane's creator. You can watch the video here:&lt;/p&gt;&lt;p&gt; &lt;iframe src="https://www.youtube.com/embed/Al4yGtRYyew"&gt;VIDEO&lt;/iframe&gt; &lt;/p&gt;&lt;p&gt;&lt;em&gt;Interested in having this talk at your conference or meetup? &lt;a href="https://kilianvalkhof.com/speaking/"&gt;Get in touch!&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;h2 id="galls-law"&gt;Gall's law&lt;/h2&gt;&lt;p&gt;When you look at CSS as a whole, it's a complex system that is hard to understand. But it's also a system that works.&lt;/p&gt;&lt;p&gt;We have a programming principle that covers that:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;All complex systems that work, evolved from simple systems that worked.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This is known as Gall's law, and it applies to CSS layout as well. At its core, CSS layout is built on a few fundamental concepts that work together to create the complex layouts we see on the web today.&lt;/p&gt;&lt;p&gt;CSS Layout started out with a single layout algorithm called &lt;strong&gt;normal flow&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Normal flow has a few basic rules, which all subsequent layout algorithms built upon. Indeed, that's the first lesson:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;CSS has &lt;em&gt;multiple&lt;/em&gt; layout algorithms.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Specific properties in CSS let you opt into different layout algorithms, each with their own rules and behaviors.&lt;/p&gt;&lt;p&gt;Once you understand the foundation of normal flow, you can start to understand how other layout algorithms like positioning, Flexbox, and Grid build upon it.&lt;/p&gt;&lt;p&gt;In other words, this article will help you go from this:&lt;/p&gt;&lt;img src="https://polypane.app/blogs/csslayout/cssbad.gif" alt=""&gt;&lt;p&gt;…to this:&lt;/p&gt;&lt;img src="https://polypane.app/blogs/csslayout/cssgood.gif" alt=""&gt;&lt;p&gt;Are you ready?&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2 id="everything-is-a-box"&gt;Everything is a box&lt;/h2&gt;&lt;p&gt;The most fundamental concept in CSS is that &lt;strong&gt;everything is a box&lt;/strong&gt;. Every element creates a rectangular box, and the x and y position of the box, along with it's width and height create the combined layout on the page.&lt;/p&gt;&lt;p&gt;CSS is:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;A bunch of boxes,&lt;/li&gt;&lt;li&gt;placed on the screen,&lt;/li&gt;&lt;li&gt;according to layout algorithms.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This might seem like an obvious statement, but it's important to understand: before screens became ubiquitous, the idea of a "live" layout algorithm wasn't really needed. Boxes didn't interact with each other. They were put in a place and then just ...stayed there.&lt;/p&gt;&lt;p&gt;Print pages are &lt;em&gt;laid out&lt;/em&gt; of course, and have been for centuries, but that didn't really happen in real time by a system of rules, an algorithm.&lt;/p&gt;&lt;p&gt;So, CSS is a bunch of boxes. The initial layout algorithm that places them on the screen is:&lt;/p&gt;&lt;h2 id="normal-flow"&gt;Normal flow&lt;/h2&gt;&lt;p&gt;Normal flow is the default layout algorithm in CSS. It's what you get when you don't explicitly opt into any other layout algorithm.&lt;/p&gt;&lt;p&gt;It comes from when the web was made for document sharing, and "normal flow" refers to the way text flows across a page: &lt;strong&gt;from left to right, from top to bottom&lt;/strong&gt; (for western languages).&lt;/p&gt;&lt;p&gt;From left to right is the &lt;em&gt;inline&lt;/em&gt; direction, from top to bottom is the &lt;em&gt;block&lt;/em&gt; direction. So text
characters will be inline, side by side, and wrap to the next line. Paragraphs and headings are block, and they stack vertically.&lt;/p&gt;&lt;p&gt;Browsers ship with a default stylesheet that dictates which elements are block and which are inline: &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; is block, but &lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt; is inline.&lt;/p&gt;&lt;p&gt;So that's why, if you write semantic HTML, the browser already makes your page look like a document.&lt;/p&gt;&lt;h3 id="non-western-languages"&gt;Non-western languages&lt;/h3&gt;&lt;p&gt;Not all languages are written left to right, top to bottom. Some languages are written right to left (like Arabic and Hebrew), and some are written top to bottom (like traditional Chinese and Japanese).&lt;/p&gt;&lt;p&gt;This is why CSS has a &lt;code&gt;writing-mode&lt;/code&gt; property that lets you change the inline and block directions to fit the language you're using.&lt;/p&gt;&lt;p&gt;Along with that writing mode, many of the explicit directional and sizing properties, like "left" or "height" now have logical equivalents. Instead of &lt;code&gt;left&lt;/code&gt;, which for western languages is the start of inline text, the property name is &lt;code&gt;inset-inline-start&lt;/code&gt;. Instead of &lt;code&gt;height&lt;/code&gt;, which is the size in the block direction, the property name is &lt;code&gt;block-size&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Writing CSS with these logical properties makes them more portable. For this article though, we'll stick to the physical properties like &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;width&lt;/code&gt; for simplicity.&lt;/p&gt;&lt;h3 id="formatting-context-and-anonymous-boxes"&gt;Formatting context and anonymous boxes&lt;/h3&gt;&lt;p&gt;Block and inline tell you how a given element lays out in comparison to the elements around it. This state of being block or inline is called the &lt;strong&gt;formatting context&lt;/strong&gt;, and each element has a formatting context.&lt;/p&gt;&lt;p&gt;But not everything is an element. Consider this HTML:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;This is &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;em&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;emphasized&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;em&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; is a block element and &lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt; is inline.&lt;/p&gt;&lt;p&gt;But what about the words "This is"? How does that work?&lt;/p&gt;&lt;p&gt;Earlier I wrote that in CSS, everything is a box, and that's also how normal flow solves this. The browser creates something called an &lt;strong&gt;anonymous box&lt;/strong&gt; around that text. In this case because &lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt; is an inline box, the anonymous box will also be inline: it will be an &lt;em&gt;anonymous inline box&lt;/em&gt;.&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&amp;lt;p&amp;gt;&lt;span&gt;This is &lt;/span&gt;&amp;lt;em&amp;gt;emphasized&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that, the browsers now has two inline boxes, and it knows what do to with that. It can lay them out next to each other.&lt;/p&gt;&lt;p&gt;If the code was this though:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  This is
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;a new paragraph&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; are block-level, so the anonymous box generated for "This is" will be an &lt;em&gt;anonymous &lt;strong&gt;block&lt;/strong&gt; box&lt;/em&gt;, and the browser can lay out the two boxes on top of each other:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&amp;lt;div&amp;gt;&lt;br&gt;&lt;p&gt;  This is&lt;/p&gt;&lt;p&gt;  &amp;lt;p&amp;gt;a new paragraph&amp;lt;/p&amp;gt;&lt;/p&gt;&amp;lt;/div&amp;gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="whitespace-and-anonymous-boxes"&gt;Whitespace and anonymous boxes&lt;/h3&gt;&lt;p&gt;This anonymous box generation is why some things you might expect to work don't. Take this example.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;half&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;First&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;half&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Second&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.half&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; inline-block&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;width&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 50%&lt;span&gt;;&lt;/span&gt;

    &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; hotpink&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.half + .half&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; dodgerblue&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Both divs are 50% wide, but they don't sit on the same line. Even though 50% + 50% is 100%.&lt;/p&gt;&lt;p&gt;This is caused by an anonymous inline box created by the newline, the whitespace, between the two divs. That inline box takes up space (the width of a single space character) to the right of the first div, and so the second div no longer fits on the same line:&lt;/p&gt;&lt;p&gt;It would be great if we can select that anonymous box and hide it but unfortunately that's not something we can do with CSS.&lt;/p&gt;&lt;p&gt;We can only target named boxes.&lt;/p&gt;&lt;h4 id="solving-the-whitespace-issue"&gt;Solving the whitespace issue&lt;/h4&gt;&lt;p&gt;We can solve this whitespace problem in several ways:&lt;/p&gt;&lt;h5 id="remove-the-whitespace"&gt;Remove the whitespace&lt;/h5&gt;&lt;p&gt;Remove the character space between the two &lt;code&gt;div&lt;/code&gt;s, and the anonymous box is never created (...until you run &lt;code&gt;prettier&lt;/code&gt; on your code).&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&amp;lt;div&amp;gt;&lt;br&gt;  &amp;lt;div class="half"&amp;gt;&lt;br&gt;  &amp;lt;/div&amp;gt;&amp;lt;div class="half"&amp;gt;&lt;br&gt;  &amp;lt;/div&amp;gt;&lt;br&gt;&amp;lt;/div&amp;gt;&lt;/pre&gt;&lt;/div&gt;&lt;h5 id="disable-wrapping"&gt;Disable wrapping&lt;/h5&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.container&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;white-space&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; nowrap&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When you disable wrapping, you force both &lt;code&gt;div&lt;/code&gt;s on the same line regardless of the width of the container. Notice that there is still that space between the two &lt;code&gt;div&lt;/code&gt;s, they don't sit flush against each other and that the second &lt;code&gt;div&lt;/code&gt; overflows the container.&lt;/p&gt;&lt;p&gt;Since the total width is still 50% + the width of a space character + 50%, the second &lt;code&gt;div&lt;/code&gt; overflows the container.&lt;/p&gt;&lt;h5 id="set-the-font-size-to-0"&gt;Set the font-size to 0&lt;/h5&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.container&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-size&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 0&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.container &amp;gt; div&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-size&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 16px&lt;span&gt;;&lt;/span&gt; 
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you set the font-size of the parent to 0, the anonymous box will still be created, but it won't have any width since the width of a space at font-size 0 is 0.&lt;/p&gt;&lt;p&gt;If your child elements have text in them, you will need to reset the font size in them or your text won't show up either.&lt;/p&gt;&lt;h5 id="white-space-collapse-discard-in-the-future"&gt;White-space-collapse: discard (in the future!)&lt;/h5&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.container&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;white-space-collapse&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; discard&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To show you how much CSS is still being worked on, white-space-collapse
is part of the newest CSS specification, text level 4.&lt;/p&gt;&lt;p&gt;The value we mention above, 'discard', isn't even supported in any browser as of January 2026. So this is a feature for the future and doesn't work yet.&lt;/p&gt;&lt;p&gt;Even normal flow, which has been around since &lt;em&gt;literally the beginning of CSS&lt;/em&gt;, is
still being worked on!&lt;/p&gt;&lt;h3 id="the-box-model"&gt;The box model&lt;/h3&gt;&lt;p&gt;We're not done talking about boxes interact by a long shot, but for the next few concepts we need to understand how boxes themselves are built up. For that we need to talk about the box model.&lt;/p&gt;&lt;p&gt;You have probably seen a version of this diagram before. A box in CSS actually
consists of a few distinct areas.&lt;/p&gt;&lt;p&gt;The center area is where you content is, the text or child elements. This content is pushed inwards by the padding. The edge of a box is called the border, and outside of the border is the margin which pushes a box away from other boxes.&lt;/p&gt;&lt;p&gt;For stupid browser reasons there are two models when it comes to determining the size of a box when you
give it a width: &lt;strong&gt;content-box&lt;/strong&gt; and &lt;strong&gt;border-box&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;One of those is the default in browsers, and the other one
makes sense.&lt;/p&gt;&lt;h4 id="content-box"&gt;Content-box&lt;/h4&gt;&lt;p&gt;The default is &lt;code&gt;content-box&lt;/code&gt;. In that model, &lt;code&gt;width&lt;/code&gt; is applied to the &lt;code&gt;content-box&lt;/code&gt;, and padding and borders are added to it for the final rendered width.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;Actual width = width + padding + border&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is confusing because when you set &lt;code&gt;width: 200px&lt;/code&gt;, the actual rendered width might be 250px after padding and borders.&lt;/p&gt;&lt;h4 id="border-box"&gt;Border-box&lt;/h4&gt;&lt;p&gt;To improve this situation, most modern CSS resets include the following CSS&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;*&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;box-sizing&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; border-box&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With &lt;code&gt;border-box&lt;/code&gt;, the width you set is applied to the &lt;code&gt;border-box&lt;/code&gt; of the element, so it &lt;strong&gt;includes&lt;/strong&gt; the border and padding layers. The space for the content is width minus padding minus border.&lt;/p&gt;&lt;p&gt;This means that when you see &lt;code&gt;width: 200px&lt;/code&gt; in your CSS, you know the element is actually 200px wide regardless of the padding and border. It's much easier to work with.&lt;/p&gt;&lt;p&gt;Try editing the values in the example below to see how &lt;code&gt;content-box&lt;/code&gt; and &lt;code&gt;border-box&lt;/code&gt; differ. Notice that both boxes have the same width value (200px), but the content-box is physically larger because padding and borders are added outside the content width.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;content-box&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Content Box&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;border-box&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;Border Box&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;div&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;width&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 200px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 20px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin-bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1rem&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.content-box&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;box-sizing&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; content-box&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;border&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 5px solid blue&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; lightblue&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.border-box&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;box-sizing&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; border-box&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;border&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 5px solid green&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; lightgreen&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;With this understanding of the box model, we can go back to talking about how boxes interact.&lt;/p&gt;&lt;h3 id="how-inline-boxes-work"&gt;How inline boxes work&lt;/h3&gt;&lt;p&gt;Everything in CSS is a box, but "box" isn't always what you expect.&lt;/p&gt;&lt;p&gt;A block box makes sense: the element is a rectangle, and the border sits on all edges.&lt;/p&gt;&lt;p&gt;But inline elements can wrap to the next line without becoming a block element, and so the box around an inline element is split up and looks, well, kind of broken:&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;The top and bottom borders are drawn for each line the element spans, while the inline borders and padding (left and right) are only drawn at the very start and very end.&lt;/p&gt;&lt;p&gt;That looks really broken, doesn't it? Browsers render an inline element as one, long, single line (it's inline after all), and then they slice that up into different line segments where it wraps. Thinking about it in that way makes it a bit easier to visualize what is happening and why it's happening. The box is sliced up into different parts.&lt;/p&gt;&lt;h4 id="decorations"&gt;Decorations&lt;/h4&gt;&lt;p&gt;If you have "decorations" (which is what borders and padding are called) on an inline element, you probably expect those to be applied to each line segment.&lt;/p&gt;&lt;p&gt;Luckily, we can tell browsers how to treat each broken-up line with &lt;code&gt;box-decoration-break&lt;/code&gt;. The default value is &lt;code&gt;slice&lt;/code&gt;, which is quite literally slicing up the box into different parts.&lt;/p&gt;&lt;p&gt;But you can also tell the browser to treat each line as its own box with &lt;code&gt;clone&lt;/code&gt;. Each line becomes its own box with borders and padding on all sides.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;span&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;box-decoration-break&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; clone&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;h4 id="placement-of-inline-elements"&gt;Placement of inline elements&lt;/h4&gt;&lt;p&gt;Some things like vertical margins, explicit widths and height don't apply to inline elements. This is because when an element is "in line", it's placement on the page is determined by the line it's in. So we know text is "in line", but what's a line?&lt;/p&gt;&lt;h3 id="lines-and-baselines"&gt;Lines and baselines&lt;/h3&gt;&lt;p&gt;Browsers started out as a way to lay out lines of text. The inline text inside a box is laid out in a number of &lt;em&gt;lines&lt;/em&gt;, and these lines are laid out based on the &lt;code&gt;line-height&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;p&gt;What are lines in browsers?&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Within each of these lines there is a &lt;strong&gt;baseline&lt;/strong&gt;. The baseline is determined by the font you use, and it's the bottom of your text characters (except for descenders, which go below the baseline).&lt;/p&gt;&lt;div&gt;&lt;p&gt;What are lines in browsers?&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;What you should understand here is that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Inline elements are laid out in lines&lt;/li&gt;&lt;li&gt;Each line has a baseline&lt;/li&gt;&lt;li&gt;The baseline is determined by the font&lt;/li&gt;&lt;li&gt;All inline elements align with the baseline &lt;em&gt;by default&lt;/em&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;IF you want your text to be in a different place (vertically) in the line, you can change the vertical alignment of an inline element with &lt;code&gt;vertical-align&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;span&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;vertical-align&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; baseline&lt;span&gt;;&lt;/span&gt; 
  &lt;span&gt;vertical-align&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; top&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;vertical-align&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; middle&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;vertical-align&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; bottom&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;vertical-align&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; text-top&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;vertical-align&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; text-bottom&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;top&lt;/code&gt; and &lt;code&gt;bottom&lt;/code&gt; values are determined by the line height you set on the element:&lt;/p&gt;&lt;p&gt;The &lt;code&gt;text-top&lt;/code&gt; and &lt;code&gt;text-bottom&lt;/code&gt; are determined by the font. Text-top will align the x-height with the top of ascenders (the parts of letters like b, d, f, h that extend above the x-height), and text-bottom will align the bottom of normal letters with the bottom of descenders.&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;text-top&lt;/span&gt;Baseline&lt;/p&gt;&lt;span&gt;text-bottom&lt;/span&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Lastly, &lt;code&gt;middle&lt;/code&gt; is weird.&lt;/p&gt;&lt;p&gt;It's calculated from a combination of the font metrics and the line-height, and that causes it to not always mean the exact middle of the line:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;middle = baseline + (x-height / 2)&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;In the example below, you can see that the word "middle" is actually closer to the bottom of the line than to the top.&lt;/p&gt;&lt;p&gt;I'd like to apologize for that on behalf of CSS.&lt;/p&gt;&lt;p&gt;For practical purposes, just know it exists but might not behave exactly as you expect.&lt;/p&gt;&lt;p&gt;From the explanation above, what you need to remember is that, by default, &lt;em&gt;all&lt;/em&gt; inline elements sit on the baseline.&lt;/p&gt;&lt;h4 id="inline-elements-sit-on-the-baseline"&gt;Inline elements sit on the baseline&lt;/h4&gt;&lt;p&gt;With that knowledge, does the following situation already make sense?&lt;/p&gt;&lt;p&gt;If you now find this obvious, then good job! The inline image is sitting on the baseline, and that's why there is 'unexpected' whitespace below it.&lt;/p&gt;&lt;p&gt;In the past, you might have fixed that by setting &lt;code&gt;display: block&lt;/code&gt; on the image, but this has other layout effects too:&lt;/p&gt;&lt;p&gt;The whitespace is gone, but the image now also no longer sits inline with any text, nor does it follow the text align.&lt;/p&gt;&lt;p&gt;Another way you could have fixed this is by setting &lt;code&gt;line-height: 0&lt;/code&gt; on the parent:&lt;/p&gt;&lt;p&gt;If the line-height is 0, then the space below the baseline is also 0, so the whitespace is gone. If you have other text in the parent, that text will also be affected and could even start overlapping.&lt;/p&gt;&lt;h5 id="fixing-images"&gt;'Fixing' images&lt;/h5&gt;&lt;p&gt;The easiest 'fix' in this case it to simply &lt;em&gt;move the image off the baseline&lt;/em&gt;, for example by aligning it to the top or bottom of the line instead:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;img&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;vertical-align&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; top&lt;span&gt;;&lt;/span&gt; 
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The bottom of the image no longer sits on the baseline, so there is no longer whitespace reserved below it!&lt;/p&gt;&lt;p&gt;Now that we have discussed inline behaviour and understand how layout happens &lt;em&gt;inside&lt;/em&gt; boxes when they're in normal flow, lets move outside the box and see how the space between boxes works.&lt;/p&gt;&lt;h3 id="margin-behavior"&gt;Margin behavior&lt;/h3&gt;&lt;p&gt;Margins are outside of an element and you can use them to push boxes away from each other.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;For block elements, this happens in all directions. For inline elements, as we previously mentioned, it's the line they sit on that determines their placement. Adding margin to inline elemens will only apply in the inline direction (left and right).&lt;/p&gt;&lt;h4 id="margin-collapse"&gt;Margin collapse&lt;/h4&gt;&lt;p&gt;When there are two block elements with margins stacked vertically and there are no other box parts (like border or padding) between them, the &lt;strong&gt;larger margin of the two is picked&lt;/strong&gt; and they 'collapse' into each other.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.hotpink&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1rem&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.dodgerblue&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 2rem&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We've made the margins semi-transparent here so you can see how they actually overlap, or 'collapse', rather than add up.&lt;/p&gt;&lt;p&gt;This happens when any two elements in the same &lt;em&gt;formatting context&lt;/em&gt; (remember that one?) have margins that interact.&lt;/p&gt;&lt;h4 id="margin-collapse-between-parents-and-children"&gt;Margin collapse between parents and children&lt;/h4&gt;&lt;p&gt;Elements and their children also share a formatting context, and that is why you also get margin collapse between parents and children.&lt;/p&gt;&lt;p&gt;You might add a margin to the first element inside a parent with the idea of pushing that element down from the top of the parent element, but due to margin collapse that margin is applied &lt;em&gt;outside of the parent&lt;/em&gt;, rather than pushing down the first element.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.parent&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1rem&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You'll frequently want to prevent this, and while there is no explicit &lt;code&gt;margin-collapse: none&lt;/code&gt; property, You can instead prevent margin collapse or create a new formatting context.&lt;/p&gt;&lt;p&gt;Firstly, adding a padding or a border, no matter how small, to the parent element will prevent margin collapse since the margins are no longer adjacent.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.parent&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1rem&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1px&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.child&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 3rem&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And while this works, you can see that 1px of padding is affecting the layout. This might matter, or it might not.&lt;/p&gt;&lt;p&gt;another way is to use &lt;code&gt;overflow: auto&lt;/code&gt; (or &lt;code&gt;hidden&lt;/code&gt;) on the parent element. This creates a new formatting context &lt;em&gt;inside&lt;/em&gt; the element, and now the element and the child element are in different formatting contexts, which prevents margin collapse.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.parent&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;overflow&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.child&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 3rem&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Overflow auto will prevent other elements from being shown outside of the parent element, and when you have restrictions on the width or height of the element, it will show a scrollbar when the content overflows.&lt;/p&gt;&lt;p&gt;The explicit option is to define the parent as a new formatting context, which you can do with &lt;code&gt;display: flow-root&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.parent&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; flow-root&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.child&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 3rem&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;flow-root&lt;/code&gt; is a bit of a magical property that creates a new block formatting context without affecting other layout aspects of the element. It's a great way to prevent margin collapse without side effects.&lt;/p&gt;&lt;h4 id="negative-margins"&gt;Negative margins&lt;/h4&gt;&lt;p&gt;Margins can also be negative. When you use negative margins you're not adding space between element, but you're changing the position of the element itself in a way that no longer interacts with the elements around it.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.top&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1rem&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;.overlap&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; -2rem&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The way they get calculated is by taking the margin value, which can also be collapsed, see how the two margins overlap, then subtracting the negative value and starting the new element there.&lt;/p&gt;&lt;p&gt;Negative margins exist, but you're better off ignoring them and using positioning instead for overlapping layouts. (More on positioning later.)&lt;/p&gt;&lt;h4 id="centering-block-elements-with-margin-auto"&gt;Centering block elements with margin: auto&lt;/h4&gt;&lt;p&gt;You can use margins to center elements horizontally by setting the left and right margins to &lt;code&gt;auto&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.block&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;width&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 200px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin-left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin-right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This element gets centered because the layout for horizontal margins uses a &lt;strong&gt;constraint-based layout&lt;/strong&gt; when either width or margin is set to auto.&lt;/p&gt;&lt;p&gt;A constraint-based layout means that there is a formula that gets solved to determine the layout. The rule is:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;total width = margin-left + border-left + padding-left + width + padding-right + border-right + margin-right&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Because borders and paddings are fixed values we can leave them out and instead simplify it to this:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;available space to fill out = margin-left + width + margin-right&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Let's see what happens when we plug in different values for the width and margins into this formula.&lt;/p&gt;&lt;h5 id="case-1-everything-is-set-to-auto"&gt;Case 1: everything is set to auto&lt;/h5&gt;&lt;p&gt;When all three values are set to &lt;code&gt;auto&lt;/code&gt;, the width takes up all available space, and both margins are set to 0.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.block&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin-right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;width&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is because the "preferred width" of a block element is 100%. So the equation becomes:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;total width = 0 + 100% + 0&lt;/p&gt;&lt;/blockquote&gt;&lt;h5 id="case-2-the-width-is-auto"&gt;Case 2: the width is auto&lt;/h5&gt;&lt;p&gt;If you have values for both margins but aren't defining with, then the width takes up all remaining space between the two margins.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.block&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;margin-left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 2rem&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin-right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 2rem&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;width&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is because the preferred width is still 100%, and the constraint then makes this formula:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;total width = 2rem + (100% - 4rem) + 2rem&lt;/p&gt;&lt;/blockquote&gt;&lt;h5 id="case-3-the-width-is-fixed"&gt;Case 3: the width is fixed&lt;/h5&gt;&lt;p&gt;When the width is fixed and both margins are set to &lt;code&gt;auto&lt;/code&gt;, the remaining space is divided equally between the two margins.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.block&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;width&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 50%&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin-left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin-right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is because the equation becomes:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;total width = 25% + 50% + 25%&lt;/p&gt;&lt;/blockquote&gt;&lt;h5 id="case-4-width-and-one-of-the-margins-are-fixed"&gt;Case 4: width and one of the margins are fixed&lt;/h5&gt;&lt;p&gt;When the width is fixed and one of the margins is also set to a fixed value, the other margin is set to the remaining space.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.block&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;width&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 200px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin-right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 50px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin-left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; auto&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The equation becomes:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;total width = (100% - 250px) + 200px + 50px&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The browser calculates the remaining space as &lt;code&gt;100% - 250px&lt;/code&gt; and adds it to the left margin, and so even though block elements normally start at the left edge of their container, this one is pushed to the right even though it doesn't have a specific value for &lt;code&gt;margin-left&lt;/code&gt;.&lt;/p&gt;&lt;h3 id="inline-block-the-hybrid"&gt;Inline-block: The hybrid&lt;/h3&gt;&lt;p&gt;There's one last trick normal flow has: &lt;code&gt;display: inline-block&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Given all the time we spent figuring out the differences between inline and block elements and how their specific behavior differs, what even is inline-block?&lt;/p&gt;&lt;p&gt;Inline-block tells the browser that an element behaves as &lt;strong&gt;inline for the formatting context around it&lt;/strong&gt;, but is &amp;amp;&lt;strong&gt;rendered as a block box&lt;/strong&gt;, creating its own formatting context.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.tag&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; inline-block&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 5px 10px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 5px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; dodgerblue&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;width&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 250px&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;text text text&lt;/p&gt;&lt;p&gt; We have a bunch of text that wraps across lines.&lt;/p&gt;&lt;p&gt;More text text text&lt;/p&gt;&lt;/div&gt;&lt;p&gt;You can see that the element sits inline with the rest of the text and you can even see that the text is placed on the same &lt;em&gt;baseline&lt;/em&gt;, but the element itself behaves as a block element, since it has a width, padding on all sides and the content inside of it wraps to multiple &lt;em&gt;lines&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;The CSS specification calls this behavior "shrink-to-fit". Before Flexbox, this was a great way to render things like a list of tags or navigation items:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.nav-item&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; inline-block&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 5px 10px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 5px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; dodgerblue&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="display-values-outer-and-inner"&gt;Display values: outer and inner&lt;/h4&gt;&lt;p&gt;The &lt;code&gt;display&lt;/code&gt; property can have two parts: an outer and an inner value. &lt;code&gt;inline-block&lt;/code&gt; as a value is from before this was introduced, but in "modern" CSS it's equivalent to:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; inline flow-root&lt;span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The outer value is how the element behaves in relation to other elements, so inline, and the inner value is how the element behaves in relation to its own content. We want that to have a block formatting context. You don't set that with "block", but with &lt;code&gt;flow-root&lt;/code&gt;. Think of it as "a new 'normal flow' starting point".&lt;/p&gt;&lt;p&gt;When you set a single item, that item is either for the inner or outer value depending on the value: some are inherently outer values, and some are inherently inner values. For example &lt;code&gt;display: grid&lt;/code&gt; creates a grid for the content, and the outer value is inferred to be &lt;code&gt;block&lt;/code&gt;, making it the same as &lt;code&gt;display: block grid&lt;/code&gt;. &lt;code&gt;display: inline&lt;/code&gt; on the other hand is inherently an outer value, so the inner value is inferred to be &lt;code&gt;flow&lt;/code&gt;, making it the same as &lt;code&gt;display: inline flow&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;code&gt;flow-root&lt;/code&gt; and &lt;code&gt;flow&lt;/code&gt; are the two new keywords here and they are &lt;em&gt;roughly&lt;/em&gt; equivalent to &lt;code&gt;block&lt;/code&gt; and &lt;code&gt;inline&lt;/code&gt;, but only for the inner value.&lt;/p&gt;&lt;h3 id="thats-normal-flow"&gt;That's Normal Flow&lt;/h3&gt;&lt;p&gt;CSS's simplest layout algorithm! Would you have expected that there was so much behind something that you can essentially describe as "text goes from left to right, top to bottom"?&lt;/p&gt;&lt;p&gt;The good news is that all other layout algorithms build upon these concepts. In fact, unless you explicitly take an element "out of flow", elements are still considered "in flow" regardless of what other layout algorithm you use, and the normal flow algorithm is used in addition to it.&lt;/p&gt;&lt;h4 id="key-takeaways-for-normal-flow"&gt;Key takeaways for normal flow&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;Block elements stack vertically (↓)&lt;/li&gt;&lt;li&gt;Inline elements flow horizontally (→)&lt;/li&gt;&lt;li&gt;Everything is a box&lt;/li&gt;&lt;li&gt;Anonymous boxes get created for text outside of elements&lt;/li&gt;&lt;li&gt;Margins collapse vertically&lt;/li&gt;&lt;li&gt;All inline elements sit on the baseline&lt;/li&gt;&lt;/ul&gt;&lt;h2 id="other-layout-algorithms"&gt;Other layout algorithms&lt;/h2&gt;&lt;p&gt;To get any other layout algorithm, you need to opt into them. There are three main layout algorithms in CSS:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Positioning&lt;/strong&gt; (relative, absolute, fixed, sticky)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Flexbox&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Grid&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There's a few more, like table layout, floats and multi-column, but the first two are hacks that we haven't needed in well over a decade, and multi-column is too niche for this already long article (and indeed, might soon be replaced by the masonry-style grid layout algorithm).&lt;/p&gt;&lt;h2 id="a-small-detour-sizes-in-percentages"&gt;A small detour: sizes in percentages&lt;/h2&gt;&lt;p&gt;Before we dive into the other layout algorithms, we need to talk about percentages. When you use a percentage value for a size or a margin, padding etc, what is that percentage based on?&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;div&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;width&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 50%&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;height&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 25%&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;margin&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 10% 5%&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 5% 2.5%&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Well, for &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; (and their min and max and logical equivalents), percentages are based on the size of the &lt;strong&gt;containing block&lt;/strong&gt;. So 50% width is 50 percent of the width of the containing block, and 25% height is 25% of the height of the containing block.&lt;/p&gt;&lt;h3 id="hold-up-containing-block"&gt;Hold up, containing block?&lt;/h3&gt;&lt;p&gt;So we have offset parents, stacking contexts and now also "containing block".&lt;/p&gt;&lt;p&gt;The containing block is the box that contains the element. For most elements, this is the &lt;strong&gt;content-box&lt;/strong&gt; (remember the box model) of the first parent element that is &lt;code&gt;display: block&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;For &lt;code&gt;position: absolute&lt;/code&gt; elements, it's the &lt;strong&gt;padding-box&lt;/strong&gt; of the closest stacking context, and for &lt;code&gt;position: fixed&lt;/code&gt; elements, it's the viewport.&lt;/p&gt;&lt;p&gt;Those last two are exceptions but in practical use those exceptions actually result in elements sizing in ways you would expect.&lt;/p&gt;&lt;h3 id="percentages-for-margin-and-padding"&gt;Percentages for margin and padding&lt;/h3&gt;&lt;p&gt;For margins and paddings, percentages are always based on the &lt;strong&gt;width&lt;/strong&gt; of the containing block, even for &lt;code&gt;margin-top&lt;/code&gt; and &lt;code&gt;padding-bottom&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;That's a bit confusing, but it's because of how the rest of CSS works. Most elements have a vertical size that changes based on their content: the more lines of text you add to a paragraph, the taller it gets.&lt;/p&gt;&lt;p&gt;If you were able to add a padding-top based on a percentage of the height, then that would increase the absolute height of the element, which in turn would increase the padding-top's absolute value, which would then update the height of the element and so on: it would create an infinite loop.&lt;/p&gt;&lt;p&gt;Instead, having percentages for vertical margins and paddings based on the &lt;code&gt;width&lt;/code&gt; of the containing block gives you a predictable value that doesn't change when it causes the height of an element to increase.&lt;/p&gt;&lt;h2 id="positioning-schemes"&gt;Positioning schemes&lt;/h2&gt;&lt;p&gt;With positioning, we can tell the browser how to place an element either in relation to its calculated flow position, or explicitly take it out of normal flow. It's also where the &lt;strong&gt;third dimension&lt;/strong&gt; is introduced with &lt;code&gt;z-index&lt;/code&gt;. That concept doesn't exist in normal flow, and adding z-index to an element without a position does nothing.&lt;/p&gt;&lt;p&gt;Elements by default have &lt;code&gt;position: static&lt;/code&gt;, which means no explicit positioning. To activate the positioning layout algorithm, pick a different value:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;static&lt;/code&gt; (the default)&lt;/li&gt;&lt;li&gt;&lt;code&gt;relative&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;absolute&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;fixed&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;sticky&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Each of these are their own little layout algorithm, so let's go through them one by one.&lt;/p&gt;&lt;h3 id="position-relative"&gt;Position: relative&lt;/h3&gt;&lt;p&gt;&lt;code&gt;relative&lt;/code&gt; is the basic value that lets you opt in to positioning layout. A relatively positioned element has a box placed in the same spot as normal flow, but you can move the element away from that box using &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;left&lt;/code&gt;, &lt;code&gt;bottom&lt;/code&gt;, and &lt;code&gt;right&lt;/code&gt;, which are called &lt;strong&gt;inset properties&lt;/strong&gt;. There's also a &lt;code&gt;inset&lt;/code&gt; shorthand property that lets you set all four inset properties at once.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.box&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; relative&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 40px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 40px&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;text text text&lt;/p&gt;&lt;p&gt;text text text&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Notice that the original box is the one that takes up space and pushes the text away, but the element itself is elsewhere. The original box is used for the rest of the layout.&lt;/p&gt;&lt;p&gt;This pushing away of the element from it's original box is something you'll rarely want, or need. What you mainly use &lt;code&gt;position: relative&lt;/code&gt; for is to &lt;strong&gt;make an element an offset parent&lt;/strong&gt; for absolutely positioned children. Before we get into what an offset parent is, let's explain &lt;code&gt;position: absolute&lt;/code&gt;.&lt;/p&gt;&lt;h3 id="position-absolute"&gt;Position: absolute&lt;/h3&gt;&lt;p&gt;Where elements with &lt;code&gt;position: relative&lt;/code&gt; are still in normal flow, &lt;code&gt;position: absolute&lt;/code&gt; elements are not. This means they do not interact with other elements: they don't take up space and don't push other elements out of the way.&lt;/p&gt;&lt;p&gt;Inset properties don't move them away from their initial box like &lt;code&gt;relative&lt;/code&gt;, they instead position the element in relation to the offset parent.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.box&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; absolute&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 40px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 40px&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;text text text&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;text text text&lt;/p&gt;&lt;/div&gt;&lt;h4 id="the-offset-parent"&gt;The offset parent&lt;/h4&gt;&lt;p&gt;The offset parent is the first element in the list of ancestors that is itself also positioned (any position value other than &lt;code&gt;static&lt;/code&gt;). By default, the &lt;code&gt;body&lt;/code&gt; is the only offset parent. So &lt;code&gt;position: absolute; top: 0;&lt;/code&gt; will place an element at the top of the page.&lt;/p&gt;&lt;p&gt;If you add positioning to any parent element, it becomes an offset parent, and now &lt;code&gt;top: 0&lt;/code&gt; no longer means the top of the body element, but of that parent element.&lt;/p&gt;&lt;p&gt;Any element that is positioned will become an offset parent, so you can have many offset parents inside of other offset parents. You're always positioning against the nearest offset parent up the DOM tree.&lt;/p&gt;&lt;h5 id="finding-the-offset-parent"&gt;Finding the offset parent&lt;/h5&gt;&lt;p&gt;Layout issues where your absolutely positioned element isn't where you expect it are almost always because what you think the offset parent is is different from what the browser thinks.&lt;/p&gt;&lt;p&gt;To find the offset parent in Chrome, Safari, Firefox etc&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Find the element in the element inspector&lt;/li&gt;&lt;li&gt;Switch to the console panel&lt;/li&gt;&lt;li&gt;Type &lt;code&gt;$0.offsetParent&lt;/code&gt; and hit enter&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In &lt;a href="https://polypane.app/"&gt;Polypane&lt;/a&gt;, you can see the offset parent directly in the element inspector's debug panel.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Find the element in the &lt;a href="https://polypane.app/docs/elements-panel/"&gt;element inspector&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Switch to the debug tab&lt;/li&gt;&lt;li&gt;See Offset parent under "contexts"&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Learn more about that in our dedicated article on &lt;a href="https://polypane.app/blog/offset-parent-and-stacking-context-positioning-elements-in-all-three-dimensions/#offset-parent"&gt;Offset parent and stacking context&lt;/a&gt; (more on that second one later).&lt;/p&gt;&lt;h3 id="position-fixed"&gt;Position: fixed&lt;/h3&gt;&lt;p&gt;&lt;code&gt;position: fixed&lt;/code&gt; is a lot like &lt;code&gt;absolute&lt;/code&gt;. Elements are taken out of the normal flow, but in this scheme, the offsetParent is always the viewport, and inset properties are used to position from the edge of it.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.header&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; fixed&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 0&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 0&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 0&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That means that while absolutely positioned elements scroll along with the rest of the page, &lt;code&gt;fixed&lt;/code&gt; elements will always stay in view.&lt;/p&gt;&lt;h3 id="position-sticky"&gt;Position: sticky&lt;/h3&gt;&lt;p&gt;The last scheme is &lt;code&gt;position: sticky&lt;/code&gt;. It was added to CSS much later than the other ones, and it's for elements that you like to keep in view for some part of the time, but still want to scroll along with their offset parent.&lt;/p&gt;&lt;p&gt;With &lt;code&gt;position:sticky&lt;/code&gt;, the element will be placed the same way as &lt;code&gt;position: relative&lt;/code&gt;, but the top/left/bottom/right values don't move the element. Instead they determine where an element gets stuck relative to the scrolling context, which is usually the viewport.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.sticky-header&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; sticky&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 50%&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;Scroll down to see me stick&lt;/p&gt;&lt;/div&gt;&lt;p&gt;As an element scrolls it will behave like a relatively positioned element, until it hits that inset value where it gets stuck.&lt;/p&gt;&lt;p&gt;Then it will stay stuck, until the offset parent's bottom catches up with it, at which point the element will get unstuck and continue scrolling with it's offset parent.&lt;/p&gt;&lt;p&gt;A tricky thing here is that it will only get stuck if the browser determines it &lt;em&gt;can&lt;/em&gt; get stuck. To learn more about debugging sticky positioning, check out our article: &lt;a href="https://polypane.app/blog/getting-stuck-all-the-ways-position-sticky-can-fail/#main"&gt;Getting stuck: all the ways position sticky can fail&lt;/a&gt;&lt;/p&gt;&lt;h2 id="overview-of-positioning-schemes"&gt;Overview of positioning schemes&lt;/h2&gt;&lt;p&gt;So to compare the different positioning schemes, here's a quick overview:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Position&lt;/th&gt;&lt;th&gt;In Flow?&lt;/th&gt;&lt;th&gt;Offset Parent&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;static&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Not applicable&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;relative&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Nearest positioned&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;absolute&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Nearest positioned&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;fixed&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Viewport&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;sticky&lt;/td&gt;&lt;td&gt;Yes*&lt;/td&gt;&lt;td&gt;Scrollport&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;* until it gets stuck, then it becomes like fixed.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;h2 id="the-stacking-context"&gt;The stacking context&lt;/h2&gt;&lt;p&gt;Remember how I said that with positioning, we introduce the third dimension? Well we haven't talked about that yet.&lt;/p&gt;&lt;p&gt;While the offset parent tells you how an element is positioned in the x and y directions, the &lt;strong&gt;stacking context&lt;/strong&gt; tells you how it's positioned in the third dimension, the z-axis. Or in other words, how close it is to the "front of the screen". And like offset parents, you have stacking contexts inside stacking contexts.&lt;/p&gt;&lt;p&gt;The default stacking context is not the &lt;code&gt;body&lt;/code&gt; element, but the &lt;code&gt;html&lt;/code&gt; element. So all elements are stacked in relation to the &lt;code&gt;html&lt;/code&gt; element by default.&lt;/p&gt;&lt;h3 id="default-stacking-order"&gt;Default stacking order&lt;/h3&gt;&lt;p&gt;Elements by default have a stacking order that's the same as source order and&lt;code&gt;z-index&lt;/code&gt; lets you reorder them.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;div::before&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; hotpink&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;z-index&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;div::after&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; rebeccapurple&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;z-index&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; -1&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
&lt;span&gt;div::before&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;z-index&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; -1&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;div::after&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;z-index&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;h3 id="reordering-happens-inside-the-stacking-context"&gt;Reordering happens inside the stacking context&lt;/h3&gt;&lt;p&gt;In the example below, the purple element has a &lt;code&gt;z-index&lt;/code&gt; of 99, which is (much) higher than the blue element's &lt;code&gt;z-index&lt;/code&gt; of just 2. So you'd expect the purple element to be in front of the blue one. However, because the purple element is inside of the pink element with a &lt;code&gt;z-index&lt;/code&gt; of 1, it's stuck inside the stacking context of the pink element.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.first&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; hotpink&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;z-index&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;.inner&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; rebeccapurple&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;z-index&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 99&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;.second&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;background&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; dodgerblue&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;z-index&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 2&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;So instead of 99 being more than 2, you should actually think of it as (1,99) which &lt;em&gt;is&lt;/em&gt; less than 2. No matter how high your z-index is, it will never win from elements in a stacking context with a higher z-index than the stacking context you're currently in.&lt;/p&gt;&lt;h3 id="creating-a-stacking-context"&gt;Creating a stacking context&lt;/h3&gt;&lt;p&gt;There's just a &lt;em&gt;few&lt;/em&gt; CSS properties that can create a stacking context:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;position&lt;/code&gt; of &lt;code&gt;fixed&lt;/code&gt; or &lt;code&gt;sticky&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;z-index&lt;/code&gt; other than &lt;code&gt;auto&lt;/code&gt; (with a &lt;code&gt;position&lt;/code&gt; other than &lt;code&gt;static&lt;/code&gt; or when in a flex or grid layout)&lt;/li&gt;&lt;li&gt;&lt;code&gt;opacity&lt;/code&gt; less than 1&lt;/li&gt;&lt;li&gt;&lt;code&gt;mix-blend-mode&lt;/code&gt; other than normal&lt;/li&gt;&lt;li&gt;&lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;backdrop-filter&lt;/code&gt;, &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;perspective&lt;/code&gt;, &lt;code&gt;clip-path&lt;/code&gt;, &lt;code&gt;mask&lt;/code&gt;, &lt;code&gt;mask-image&lt;/code&gt; or &lt;code&gt;mask-box-image&lt;/code&gt; other than none&lt;/li&gt;&lt;li&gt;&lt;code&gt;isolation&lt;/code&gt; set to isolate&lt;/li&gt;&lt;li&gt;&lt;code&gt;container-type&lt;/code&gt; set to size or inline-size (so any container element)&lt;/li&gt;&lt;li&gt;&lt;code&gt;will-change&lt;/code&gt; property set to &lt;code&gt;mix-blend-mode&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;perspective&lt;/code&gt;, &lt;code&gt;clip-path&lt;/code&gt;, &lt;code&gt;mask&lt;/code&gt;, &lt;code&gt;mask-image&lt;/code&gt; or &lt;code&gt;mask-box-image&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;contain&lt;/code&gt; set to &lt;code&gt;layout&lt;/code&gt;, &lt;code&gt;paint&lt;/code&gt;, &lt;code&gt;strict&lt;/code&gt; or &lt;code&gt;content&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;… This makes &lt;code&gt;offsetParent&lt;/code&gt; sound easy right? It's quite a lot if you list them all out. Most of these properties aren't used very often though.&lt;/p&gt;&lt;h4 id="the-most-common-ones-are"&gt;The most common ones are:&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;position&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;z-index&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;opacity&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;transform&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h4 id="explicit-stacking-context"&gt;Explicit stacking context&lt;/h4&gt;&lt;p&gt;Out of the entire list, &lt;code&gt;isolation: isolate&lt;/code&gt; is the most explicit way of indicating in your CSS that you're creating a new stacking context:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.container&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;isolation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; isolate&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="a-practical-stacking-context-example"&gt;A practical stacking context example&lt;/h3&gt;&lt;p&gt;Here's a practical example, a simple block quote design with a decorative quote mark. After positioning the quote mark correctly in the X and Y axis we can now see that the quote mark is in front of the text. Because the text itself isn't an element (it's an anonymous box), we can't bring it forward using &lt;code&gt;z-index&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;blockquote&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; relative&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;blockquote span&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; absolute&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 0&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 0&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;blockquote&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span title="“"&gt;&amp;amp;ldquo;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  Lorem ipsum dolor sit...
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;blockquote&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;blockquote&gt;&lt;span&gt;“&lt;/span&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempora tenetur molestias consequatur, quidem nam ipsum minima aliquid recusandae delectus enim.&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Instead, we need to bring the quote backwards with &lt;code&gt;z-index: -1&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;blockquote span&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; absolute&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 0&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 0&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;z-index&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; -1&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;blockquote&gt;&lt;span&gt;“&lt;/span&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempora tenetur molestias consequatur, quidem nam ipsum minima aliquid recusandae delectus enim.&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;When you do that however, the quote mark disappears behind the background of the blockquote. That's because the current stacking context is still the HTML element. So we need to make the parent element the stacking context.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;blockquote&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;isolation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; isolate&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;blockquote&gt;&lt;span&gt;“&lt;/span&gt;Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempora tenetur molestias consequatur, quidem nam ipsum minima aliquid recusandae delectus enim.&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The blockquote is now a new stacking context, and the quote mark can no longer go behind the background. Instead, it sits at the beginning of the stacking context inside the blockquote: behind the text but in front of the background.&lt;/p&gt;&lt;h3 id="finding-the-stacking-context"&gt;Finding the stacking context&lt;/h3&gt;&lt;p&gt;Finding the stacking context of an element is not as easy as finding the offset parent. Unlike &lt;code&gt;element.offsetParent&lt;/code&gt;, there is no &lt;code&gt;element.stackingContext&lt;/code&gt; property.&lt;/p&gt;&lt;p&gt;That's why we built a debug panel into Polypane that not only shows you the offset parent, but also the stacking context of the selected element, along with whether the element itself creates a stacking context. It's very neat, and you can learn more about it in our article on &lt;a href="https://polypane.app/blog/offset-parent-and-stacking-context-positioning-elements-in-all-three-dimensions/#stacking-context"&gt;Offset parent and stacking context&lt;/a&gt;.&lt;/p&gt;&lt;h4 id="the-polypane-debug-panel"&gt;The Polypane debug panel&lt;/h4&gt;&lt;p&gt;The top section in the debug panel shows you CSS attributes that commonly cause issues, like &lt;code&gt;display&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt; and &lt;code&gt;z-index&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Below that, it shows the different contexts that the element is in: the offset parent, the containing block and the stacking context.&lt;/p&gt;&lt;p&gt;Lastly, it shows whether the element itself is an offset parent, containing block or stacking context.&lt;/p&gt;&lt;img src="https://polypane.app/static/debugpanel-f7262de83ecb203b837d47f43224d7a3.png" alt="The debug tab in the Elements panel"&gt;&lt;p&gt;These three sections make it very easy to figure out what's going on with the layout of an element, and really speed up your CSS layout debugging.&lt;/p&gt;&lt;h2 id="grid-and-flexbox"&gt;Grid and Flexbox&lt;/h2&gt;&lt;p&gt;That leaves Grid and Flexbox. I'm not going to spend any time on the syntax or specific features of either layout algorithm, instead focusing on how they interact with the previous layout algorithms and concepts like offset parent and stacking context.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;For Flexbox, check out &lt;a href="https://every-layout.dev/"&gt;Every Layout&lt;/a&gt; by &lt;a href="https://heydonworks.com/"&gt;Heydon Pickering&lt;/a&gt; and &lt;a href="https://piccalil.li/"&gt;Andy Bell&lt;/a&gt; or &lt;a href="https://www.flexboxsimplified.com"&gt;Flexbox Simplified&lt;/a&gt; by &lt;a href="https://www.kevinpowell.co/"&gt;Kevin Powell&lt;/a&gt;. For Grid, check out &lt;a href="https://gridbyexample.com/"&gt;Grid by Example&lt;/a&gt; by &lt;a href="https://rachelandrew.co.uk/"&gt;Rachel Andrew&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Both Flex and Grid are significantly different from the layout algorithms that came before. That's because they were developed not as a way to lay out web documents, but as a way to lay out &lt;strong&gt;web apps&lt;/strong&gt;.&lt;/p&gt;&lt;h3 id="the-key-differences"&gt;The key differences&lt;/h3&gt;&lt;p&gt;When you look at Flex and Grid, they each approach layout from a different direction.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;h4 id="flexbox-is-bottom-up"&gt;&lt;strong&gt;Flexbox&lt;/strong&gt; is "bottom up"&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;Get a bunch of elements&lt;/li&gt;&lt;li&gt;Order them in a row or column&lt;/li&gt;&lt;li&gt;Distribute them according to rules you provide&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div&gt;&lt;h4 id="grid-is-top-down"&gt;&lt;strong&gt;Grid&lt;/strong&gt; is "top down"&lt;/h4&gt;&lt;ol&gt;&lt;li&gt;Create a bunch of areas&lt;/li&gt;&lt;li&gt;Determine their sizes and positions of those areas according to rules you provide&lt;/li&gt;&lt;li&gt;Place elements inside those areas&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;So Flexbox starts with the elements you have and distributed them, while Grid starts with defining the available spaces and then adding the elementes to them.&lt;/p&gt;&lt;p&gt;Additionally, Flex is fundamentally one-dimensional (though it can wrap to a next line) while Grid is two-dimensional.&lt;/p&gt;&lt;h3 id="flex-and-grid-versus-normal-flow"&gt;Flex and Grid versus normal flow&lt;/h3&gt;&lt;p&gt;Both are replacements of sorts for normal flow. They reason about the distribution of multiple elements in a specific way, rather than the positioning of a single element in relation to other elements.&lt;/p&gt;&lt;p&gt;A very big difference between Flex and Grid on one hand and previous positioning on the other hand is that Flex and Grid no longer depend on font properties. The layouts you make and their alignment are based on the generated boxes, not on the font properties of the elements.&lt;/p&gt;&lt;p&gt;This makes a lot of things much simpler.&lt;/p&gt;&lt;p&gt;Both also try to make sure that regardless of the elements inside of them, the layout will be adhered to. In Flexbox this means that if an element has a fixed &lt;code&gt;width&lt;/code&gt;, but that width doesn't fit in the space the Flex container has available for it, the elements size will be overwritten to make it fit the Flex layout. In Grid, an elements width and height are constrained by the grid area they're placed in.&lt;/p&gt;&lt;h3 id="flex-and-grid-combined-with-positioning"&gt;Flex and Grid combined with positioning&lt;/h3&gt;&lt;p&gt;Both &lt;code&gt;flex&lt;/code&gt; and &lt;code&gt;grid&lt;/code&gt; are &lt;code&gt;display&lt;/code&gt; properties, separate from the &lt;code&gt;position&lt;/code&gt; property. This means that the way to determine offset parents and stacking contexts doesn't change.&lt;/p&gt;&lt;p&gt;You can take an element out of a flex or grid layout with &lt;code&gt;position: absolute&lt;/code&gt; or &lt;code&gt;position: fixed&lt;/code&gt;. Likewise, each element inside a flex or grid container, including the container itself, can be made an offset parent with &lt;code&gt;position: relative&lt;/code&gt; or a stacking context with &lt;code&gt;isolation: isolate&lt;/code&gt;.&lt;/p&gt;&lt;h3 id="what-not-being-dependent-on-font-properties-means"&gt;What not being dependent on font properties means&lt;/h3&gt;&lt;p&gt;Unline &lt;code&gt;vertical-align: middle&lt;/code&gt; which uses the line-height and font properties (as mentioned above) to determine the middle of a &lt;em&gt;line&lt;/em&gt;, both flex and grid align based on the boxes they create.&lt;/p&gt;&lt;p&gt;&lt;code&gt;align-items: center&lt;/code&gt; in both layout algorithms will align the boxes of the elements in the middle of the flex or grid boxes, regardless of the font properties or line height of the elements.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;.grid,
.flex {
 align-items: center;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This new type of alignment logic is the same in Flex and grid and it worked so well that it was actually added to &lt;code&gt;display: block&lt;/code&gt; as well.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;align-content&lt;/code&gt; property lets you vertically center content in block elements, as long as they have a determined height. It was added to CSS Box Alignment level 3 and works across browsers.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.block&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;align-content&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; center&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;block&lt;span&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;1&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;2&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;3&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h4 id="-items-and--content"&gt;*-items and *-content&lt;/h4&gt;&lt;p&gt;Flex and grid both have &lt;code&gt;align-items&lt;/code&gt;, &lt;code&gt;align-content&lt;/code&gt; and &lt;code&gt;justify-content&lt;/code&gt; properties. Grid also has &lt;code&gt;justify-items&lt;/code&gt;. So what is &lt;code&gt;items&lt;/code&gt; and what is &lt;code&gt;content&lt;/code&gt;?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;*-items&lt;/code&gt; properties align the individual items, for example inside a grid area.&lt;/li&gt;&lt;li&gt;&lt;code&gt;*-content&lt;/code&gt; properties align the items as a group (usually as a row or column).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In the example below, we have a simple grid that has repeating rows of 50px high, and three 1fr columns. In the first/left example, we have &lt;code&gt;align-items: end&lt;/code&gt; which aligns the items to the &lt;strong&gt;end of their grid area&lt;/strong&gt;. In the second example, we set &lt;code&gt;align-content: end&lt;/code&gt;. This keeps the items the full size of their grid area, but aligns the entire group of items to the end of the container&lt;/p&gt;&lt;h3 id="grid-and-position-sticky"&gt;Grid and position: sticky&lt;/h3&gt;&lt;p&gt;Though the way to determine offset parents and stacking contexts doesn't change with Flex and Grid, when you combine Grid and position: sticky, that won't work as you might expect.&lt;/p&gt;&lt;p&gt;The reason for that is the default value that grid has for &lt;code&gt;align-items&lt;/code&gt;, which is &lt;code&gt;stretch&lt;/code&gt;. That means that the element with position: sticky already is as tall as the grid area and that means that when it reaches that inset value to get stuck, the bottom of the element is already at the bottom of the grid area and so it never gets stuck.&lt;/p&gt;&lt;p&gt;It's easy to miss that, because elements in a grid area without a background or border look like they are not filling the entire area.&lt;/p&gt;&lt;p&gt;An element doesn't &lt;em&gt;have&lt;/em&gt; to fill the entire grid area though. You can set &lt;code&gt;align-items: start&lt;/code&gt; on the grid container to have all items align to the start of their grid area instead of stretching to fill it, or &lt;code&gt;align-self: start&lt;/code&gt; on the element itself&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.grid-container&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  
  &lt;span&gt;align-items&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; stretch&lt;span&gt;;&lt;/span&gt;

  &lt;span&gt;div&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; sticky&lt;span&gt;;&lt;/span&gt;
    &lt;span&gt;top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 50%&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;

  &lt;span&gt;.start&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;align-self&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; start&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Check out our article: &lt;a href="https://polypane.app/blog/getting-stuck-all-the-ways-position-sticky-can-fail/#main"&gt;Getting stuck: all the ways position sticky can fail&lt;/a&gt; for more on this.&lt;/p&gt;&lt;h3 id="the-final-boss-centering-a-div"&gt;The final boss: centering a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;An age old joke about CSS is that it's impossible to center a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; vertically.&lt;/p&gt;&lt;p&gt;Now that you know the fundamentals, you know that's not true.&lt;/p&gt;&lt;p&gt;The reason vertical centering is 'hard' is because CSS is built on normal flow, where the height of elements is determined by their content.&lt;/p&gt;&lt;p&gt;If the height is determined by the content, and the content is a single line of text, then the height of the element is the height of that line of text. It's already vertically centered, it's just not the height you want.&lt;/p&gt;&lt;p&gt;With that knowledge, there are actually dozens of ways to vertically center a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. But modern CSS gives us one way that's definitely the simplest: &lt;code&gt;place-content&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;code&gt;place-content&lt;/code&gt; is a shorthand for the combination of &lt;code&gt;align-content&lt;/code&gt; and &lt;code&gt;justify-content&lt;/code&gt; and as we mentioned earlier in the article, that works in Flex, Grid &lt;strong&gt;and Block elements&lt;/strong&gt;. For it to work, your element does have to have a defined height (because if not, the contents define the height).&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.container&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;height&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 400px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;place-content&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; center&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;That's all it takes. Vertically centering is trivial.&lt;/p&gt;&lt;h2 id="things-we-left-out"&gt;Things we left out&lt;/h2&gt;&lt;p&gt;The purpose of this article is to explain the fundamentals of CSS layout, but there are more concepts that can help you understand your layouts that we didn't cover:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Content sizing&lt;/strong&gt;: how &lt;code&gt;min-content&lt;/code&gt;, &lt;code&gt;max-content&lt;/code&gt; and &lt;code&gt;fit-content&lt;/code&gt; work, and how intrinsic sizing works.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Container queries&lt;/strong&gt;: how they create new layout contexts.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Aspect-ratio&lt;/strong&gt;: how it affects sizing of elements.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Subgrid&lt;/strong&gt;: how it works and how it relates to grid.&lt;/li&gt;&lt;li&gt;Honestly, a deeper dive into both &lt;strong&gt;Flexbox&lt;/strong&gt; and &lt;strong&gt;Grid&lt;/strong&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Each of these topics deserve their own article and we might cover them in the future.&lt;/p&gt;&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;&lt;p&gt;When you get started with CSS layout, it can feel overwhelming. There are a lot of concepts to learn, beyond the mere syntax and available properties. If you don't take the time to understand those concepts, you'll end up always fighting or guessing your way around designing pages.&lt;/p&gt;&lt;p&gt;CSS is built on a set of small, logical concepts that each are easy to understand, from "text goes left to right, top to bottom" to "everything is a box" to "elements can be taken outside of flow". Each layout algorithm builds upon these concepts, adding new ways to position and align elements to handle more and more complex situations. But the building blocks remain the same.&lt;/p&gt;&lt;p&gt;Understanding these building blocks of layout makes it easier to reason about CSS, to visualize how the browser is going to lay out your page and how to fix layout issues when they arise.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://polypane.app/og-images/understanding-the-fundamentals-of-css-layout.png"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://polypane.app/rss.xml</id>
            <title type="html">polypane.app</title>
            <link href="https://polypane.app" rel="alternate" type="text/html"/>
            <updated>2026-01-23T08:44:20Z</updated>
        </source>
    </entry>
</feed>