<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Starred Articles</title>
    <description>Starred Articles</description>
    <atom:link href="https://feedbin.com/starred/630a51376f6236f1db65212704980f4f.xml" rel="self" type="application/rss+xml"/>
    <link>https://feedbin.com/</link>
    <item>
      <title><![CDATA[Webspace Invaders]]></title>
      <description><![CDATA[
                    
																																	<p>A couple of weeks back, I’m sitting at my desk when a direct message from my frontend friend <a href="https://www.kevinpowell.co/">Kevin Powell</a> pops up. Kevin’s a genuinely kind guy. <a href="https://www.youtube.com/@KevinPowell">He makes CSS videos on YouTube</a> and he’s got this way of explaining things that never makes you feel stupid for not knowing them already. He’s one of those folks who still has faith in the Web.</p>
<p>“<em>Hey, hope you’re doing well!</em> </p>
<p><em>I was going to link to something of yours for an article I’m writing for <a href="https://piccalil.li/">Piccalilli</a>, but I keep getting a “This site can’t be reached” error in Chrome. I tried in Firefox, and it’s saying it can’t establish a secure connection... <a href="https://bell.bz/">Andy</a> just left a comment saying it’s working for him, so it might be a regional thing? I have </em>no<em> idea.</em>”</p>
<p>Wait. My site is down? </p>
<p>I switch into the browser and load the site. It’s fine. I hit refresh. Still fine. I pick up my phone and load the site there. Also fine. I feel a little lift of relief, followed immediately by confusion: If my site is down for Kevin but not for me, something’s definitely not right. Maybe it really <em>is</em> a regional thing?</p>
<p>So, I do what any legitimate web person does when confronted with a problem: I throw technology at it. VPN time! I connect through Canada, because that’s where Kevin is based, and sure enough – my website doesn’t load. And I get the same results checking availability through <a href="https://check-host.net">check-host.net</a> from a bunch of other locations: Brazil, Czechia, Hong Kong, India, Japan, and the Netherlands all fail to load the site, for instance.</p>
<p>After a few emails back and forth with my shared hosting provider <a href="https://all-inkl.com/">all-inkl.com</a> the issue is identified: they’ve indeed installed a filter that blocks entire countries from accessing my site. Without telling me. They just decided on their own that the best solution to whatever was happening was geographic surgery. “Your website is being accessed permanently and en masse. This is presumably a DDoS attack,” they wrote. “We have installed a filter that only allows access from certain countries so that we can protect the server from overload.”</p>
<h2>An Alien Invasion</h2>
<p>A DDoS attack? A botnet trying to hammer my server into submission? Really? I look at the access logs. No, this is something different. This is methodical. Millions (!) of requests over a couple of days from things with User-Agent strings reading GPTBot, OAI-Searchbot, Claude-SearchBot, or Meta-ExternalAgent, plus a whole bunch of IP addresses from Singapore, Shenzhen, and other parts of Asia scanning my articles and notes sections. That’s not a botnet DDoS attack. That’s an LLM bot “attack” by OpenAI, Meta, Anthropic, and many others.</p>
<p>In their hunger for data to train their large language models, companies from all over the world are systematically harvesting every word I’ve ever published, feeding it into their language models to keep them fresh – and the side effect, the collateral damage, is that Kevin in Montreal now can’t read my articles because my hosting provider decided the solution was to block Canada and half the rest of the world.</p>
<p>I sat there staring at those logs for a while. The irony wasn’t lost on me. This is my little corner of the web. My writing. With my weird little style mixer up there in the top right. And now it is simultaneously being strip-mined by AI companies and effectively made inaccessible to actual humans around the world who might want to read it.</p>
<p>This is where we are in 2026. There’s something happening on the Web at the moment that almost feels like watching that old arcade game <a href="https://en.wikipedia.org/wiki/Space_Invaders">Space Invaders</a> play out across our servers. Bots and scrapers marching in formation, attacking our servers wave after wave, systematically requesting page after page, relentlessly filling their data stores while we watch our access logs fill up.</p>
<p>The webspace invaders have arrived.</p>
<p>And the numbers are staggering. About 50 percent of web traffic is now coming from non-humans. Bots, crawlers, and agents are constantly moving across our servers in search for new pieces of content to absorb and while <a href="https://radar.cloudflare.com/year-in-review/2025#per-bot-traffic">search engine crawlers are still the highest traffic bot category</a>, AI/LLM bot traffic is constantly surging, with <a href="https://radar.cloudflare.com/year-in-review/2025#ai-crawler-traffic-by-purpose">AI training being the main crawl purpose</a>. According to Cloudflare’s 2025 Year in Review report, <a href="https://radar.cloudflare.com/year-in-review/2025#ai-traffic-share">AI bots originated 4.2 % of HTML requests</a>. But this number <em>excludes</em> Googlebot, which crawls for both search indexing and AI training, and averaged at 4.5 % of requests. Yes, according to Cloudflare, Google’s crawl volume is still bigger than all other AI crawlers combined. And in some months of 2025, the numbers were even more extreme. Take April 26, 2025, for example: on that day, human traffic only accounted for 34.5 % of HTTP requests worldwide. AI bots came in at 4.1 %. A number that is already high on its own but is being dwarfed by Googlebot with a whopping 11 % of HTTP requests. I repeat: 11 % of HTTP requests worldwide coming from Googlebot alone. The rest of “Non-AI” bots averaged at 50.1 % of requests that day. And this number might still include a fair amount of LLM training bots that hide behind fake User-Agent strings.</p>
<h2>The Open Web Subsidizing Big AI?</h2>
<p>Here’s the thing about running a personal site on modest hosting, the kind of hosting most of us are on: you notice when things change. The big platforms, they can absorb all this additional traffic without blinking. They have CDNs and server farms and maybe even entire teams of people thinking about handling bot traffic.</p>
<p>But me? I’m running a simple setup. A Craft CMS site on shared hosting, some static files here and there, nothing fancy. Just like the majority of people with a personal site or a blog. And suddenly, I’m getting hammered with enough traffic that my hosting provider is implementing emergency measures. And I’m not the only one. Just recently, <a href="https://remysharp.com/">Remy</a> did not only notice unusual spikes of traffic on his blog, but his project <a href="https://jsbin.com/">JS Bin</a> was <a href="https://front-end.social/@rem/115980811360095804">hit so hard by massive spikes in network inbound traffic</a> that <a href="https://github.com/jsbin/jsbin/issues/3583">he was struggling to keep the site online</a>. <a href="https://adactio.com/journal/21831">As Jeremy mentioned</a>, The Wikimedia Foundation <a href="https://diff.wikimedia.org/2025/04/01/how-crawlers-impact-the-operations-of-the-wikimedia-projects/">is also seeing unprecedented amounts of traffic generated by scraper bots</a>. While humans tend to browse with intention, those AI crawlers “binge read” indiscriminately and visit also the less popular pages. 65 % of this resource-consuming traffic they get for Wikipedia is now coming from bots. </p>
<p>And when I asked <a href="https://mastodon.social/@matthiasott/115994736479388596">on Mastodon</a> and <a href="https://bsky.app/profile/matthiasott.com/post/3mds2dm7cdc27">Bluesky</a>, plenty of people chimed in. Turns out I’m <em>really</em> not alone. People are seeing the same pattern on their own sites – often serious enough that they’ve had to take action. There’s even a classic tell at the moment: waves of “users” visiting from Singapore with suspiciously old Chrome User-Agent strings.</p>
<p>There’s a power imbalance at work here that’s hard to ignore. Large “AI” companies, the ones with <a href="https://hbr.org/2025/11/how-generative-ai-is-reshaping-venture-capital">billions in venture capital</a>, send their bots to harvest free content. Not only from big publishers or Wikipedia, but from small, independent websites, too. But we, the people running these sites – often as passion projects, as ways to freely share what we’ve learned, as digital gardens we tend in our spare time – we’re the ones paying for the bandwidth and server resources to handle all those additional requests while those companies profit from the training data they extract. It’s an asymmetric battle: small systems absorbing the demands generated at an entirely different, industrial scale.</p>
<p>These companies might not be plotting evil, but they’re <a href="https://drewdevault.com/2025/03/17/2025-03-17-Stop-externalizing-your-costs-on-me.html">externalizing their costs</a> and aren’t particularly concerned about the collateral damage. Sure, some still provide robots.txt compliance. They’ve <a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler">published documentation about their crawlers</a>, albeit often incomplete. But even compliance and documentation won’t change the fundamental math: when you have dozens – no hundreds, soon thousands of companies (just look at <a href="https://huggingface.co/models">the download counts on Hugging Face</a> for any popular model), each running their own scrapers, each one binge-reading your site not once every two weeks like earlier versions of Googlebot used to do, but <em>several times a day</em>, the aggregate effect can quickly overwhelm a small site. And as long as the training data keeps flowing and the models keep improving, why would they change anything?</p>
<h2>Good Bot, Bad Bot</h2>
<p>Alright, it’s bad, obviously. But things get even messier. In Space Invaders, <a href="https://www.youtube.com/watch?v=MU4psw3ccUI">the aliens followed predictable patterns</a>. You could learn their behavior, anticipate their movements, and develop strategies to play the game longer and score higher. The game we’re playing on the Web now? It’s unpredictable.</p>
<p>Because on the Web, we’re now playing against adaptable opponents. Scrapers from Singapore that hide behind spoofed User-Agent strings, in an attempt to appear as though they are a real browser, instead of identifying themselves honestly. Scrapers that change their IP addresses regularly. Scrapers that obviously <em>don’t</em> give a penny about your cute robots.txt. Scrapers that obviously don’t care about the gentleman’s agreement that the web was built upon.</p>
<p>And we’re just entering the next phase. Agentic AI. Systems that can autonomously explore websites, adapt their scraping strategies based on what defenses they encounter, rotate IP addresses, work around rate limiting. The bots are getting smarter by the minute. More persistent. More capable of treating every attempt to protect our sites as a new puzzle to solve.</p>
<p>This isn’t Space Invaders anymore. Those aliens invading our webspaces are locking on and firing relentlessly, constantly adapting, learning, and getting stronger. And here I am – one human with a keyboard – trying to keep my site accessible to other real humans while it is being harvested and under fire from all directions.</p>
<p>And to cap it all, all of this is happening against a backdrop of genuine geopolitical competition where training data for large language models is being seen as strategic infrastructure. The countries and companies that control the best AI models are said to have real future advantages – economically, militarily, politically. This transforms web scraping from an annoyance into something that’s part of a much larger power struggle. Chinese AI companies might not be that concerned about conventions around robots.txt or polite crawling. But honestly, American AI companies are under pressure to maintain their lead, and when prioritizing growth over everyone else in your path is the business model, they’re perfectly willing to cut corners, too. Everyone’s racing, and the open web looks like free resources up for grabs. The result is that my personal blog – your personal blog – becomes collateral damage in this competition. The scrapers don’t care that you’re not Facebook or the New York Times. Your content is data. Data is valuable. And in this race, the externality of overwhelming small site owners is just the cost of doing business.</p>
<h2>“Just Use Cloudflare”</h2>
<p>So naturally, there’s a corporate solution being offered. Cloudflare, with its massive network infrastructure, <a href="https://blog.cloudflare.com/declaring-your-aindependence-block-ai-bots-scrapers-and-crawlers-with-a-single-click/">provides (free) AI bot protection now</a>. You add some DNS entries, flip a switch in the Cloudflare dashboard, and let their systems handle the AI crawler traffic for you.</p>
<p>And from what I hear, it works fairly well. Cloudflare can identify and block malicious scrapers at a scale that individual site owners simply cannot. They can distinguish between real browsers and bots masquerading as browsers. They have the data, the machine learning models, the infrastructure, and the threat intelligence.</p>
<p>But I keep coming back to this question: should we all need to route our traffic through Cloudflare? Is that really the sustainable solution to the webspace invaders problem? Is that really the open, independent web we want?</p>
<p>Because here’s the thing: Cloudflare already sees an enormous percentage of web traffic. They’re a neutral actor now. But power structures change. Companies change. Countries and their governments change. Obviously. Every time we centralize infrastructure in response to a threat, we make the web a little less distributed, a little less resilient, and a little less ours. And we all saw last year what happens when half of the web relies on just one single point of failure. Within just one month, Cloudflare experienced two major outages: a global outage on <a href="https://blog.cloudflare.com/18-november-2025-outage/">November 18, 2025</a> took down roughly one in five webpages globally at the height of the incident, and one-third of the world’s 10,000 most popular websites, apps, and services, according to some estimates. Another significant incident occured on <a href="https://blog.cloudflare.com/5-december-2025-outage/">December 5</a> while Cloudflare attempted to detect and mitigate <a href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components">an industry-wide vulnerability in React Server Components</a>.</p>
<p>I sat with all this for a while, trying to figure out how I felt about it and what to do with it. On one hand, I don’t have the time or expertise to run sophisticated bot detection on my own. I have a day job. I have a life and a family. The idea of spending weekends maintaining IP blocklists and analyzing traffic patterns is exhausting just to think about.</p>
<p>On the other hand, every small site owner routing through Cloudflare means more consolidation, more centralization, more of the web’s infrastructure dependent on a single company’s good intentions and continued solvency.</p>
<p>I don’t have a good answer here. I really don’t. It’s one of those problems where all the solutions feel inadequate, somehow. </p>
<p>Still, I decided to give it one last try. And so, I spent the last few weeks moving my site from my shared hosting plan over to a virtual private server (VPS) where I have more control over the protection layers of my site and where nobody can add a country filter without my knowledge. I’ll write more about other aspects of the setup in future posts.</p>
<h2>Activating Your Site Shields</h2>
<p>But for now, let’s look at a few practical ways to fight back against those webspace invaders. Because although the bots and agents are getting smarter by the minute, there is still a lot we can do to better protect our sites and hosting bills.</p>
<h3>You, Robot?</h3>
<p>The first step (still) is to <strong>start with <a href="https://en.wikipedia.org/wiki/Robots.txt">robots.txt</a></strong>, even though this won’t stop them all. Create a robots.txt file in your web root if you don’t have one yet, and then explicitly disallow known AI scrapers of your choice:</p>

																												
																																						
																																	<p>If you want to go further, you can block wide whole range of known LLM bots with a list like the one at <a href="https://github.com/ai-robots-txt/ai.robots.txt">ai.robots.txt</a> or even serve a robots.txt that continuously updates via <a href="https://knownagents.com/docs/robots-txt">Known Agents</a> (formerly known as Dark Visitors – interesting name change, by the way). It’s not a complete solution, but it’s a starting point that takes a few minutes to set up.</p>
<p>However, using robots.txt will only work for the actors that actually honour this convention. If a crawler decides to ignore your robots.txt, nothing will stop it. That’s why a lot of people, <a href="https://ethanmarcotte.com/wrote/blockin-bots/">just like Ethan</a>, have also started blocking bots via <code>.htaccess</code>.</p>
<p>For my new VPS setup, I now use <a href="https://nginx.org/">nginx</a> to serve my site. And so, instead of using <code>.htaccess</code>, which does only exist on Apache, I added a user agent check to the <a href="https://nginx.org/en/docs/beginners_guide.html#conf_structure">nginx config</a> instead, similar to <a href="https://github.com/ai-robots-txt/ai.robots.txt/blob/main/nginx-block-ai-bots.conf">the one you can find in the ai.robots.txt repo</a>:</p>

																												
																																						
																																	<p>If the User-Agent string matches that of one of the bots on the list, the server now returns a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403">403 Forbidden</a> status code and blocks the request. In case you're running an Eleventy blog on nginx, <a href="https://rknight.me/blog/blocking-bots-with-nginx/">Robb has written about a similar approach</a> he is using to block bots on his site.</p>
<p>One important note, though: if you want to still allow the Internet Archive to archive your site, which you should, make sure to not block their <code>archive-org_bot</code> – like I did at first. ;)</p>
<h3>Rate Limiting</h3>
<p>If you control your server configuration, rate limiting also becomes essential. Real humans very rarely request fifty pages per second. But aggressive scrapers definitely do. Therefore, putting a cap on how often someone can repeat an action within a certain timeframe can help stop malicious bot activity and reduce strain on your server. If you’re on nginx, look into <code>limit_req_zone</code> and <code>limit_req</code>, as explained in <a href="https://blog.nginx.org/blog/rate-limiting-nginx">this post on the nginx blog</a>. Apache has <a href="https://httpd.apache.org/docs/current/mod/mod_ratelimit.html">mod_ratelimit</a>.</p>
<p>The tricky part is setting the right thresholds. Too strict and you risk blocking legitimate users or search engines. Too loose and the scrapers slip through easily. I’ve been experimenting with this, and honestly, it’s been a lot of trial and error and I’m still not 100 % sure if my settings are ideal. The general advice is to conservative – maybe 5 to 10 requests per second per IP – and adjust based on what you see in your logs.</p>
<p>My own setup, which complements the nginx config setup above, now distinguishes between three tiers of visitors based on User-Agent strings: the most aggressive bots and crawlers are blocked immediately, less aggressive crawlers – those that respect robots.txt – and user-initiated AI searches are moderately throttled, while everyone else (presumably humans, I hope, I’m still very hopeful, I guess) falls into the least restrictive tier. Using IP addresses would strengthen this approach even further, since, as we know, malicious actors will spoof their User-Agent strings. But for now, this additional rate limiting layer should help catch another significant number of aggressive invaders and cut off the peaks.</p>
<h3>Monitoring Your Access Logs</h3>
<p>I know, it sounds tedious. And it is. But you need to know what’s actually hitting your site. And means looking into the access logs of your server or webspace, not just your analytics dashboard. Because tools like Fathom or Plausible will only tell you half of the story. They apply their own filters to keep your analytics clean and bot-free, which is great for understanding human visitors. But that also means that there is no way around looking at your raw logs. </p>
<p>I recently spent hours going through mine, identifying the IPs with the most traffic and blocking them manually as a first, probably a bit unsustainable counter-measure. But it helped me to understand the problem better – and also to check whether my actions actually had an effect.</p>
<p>Tools like <a href="https://goaccess.io/">GoAccess</a> can be super helpful for this, because they give you real-time insights and even let you set up alerts for unusual traffic spikes. Because ideally, you want to know when something weird is happening, not discover it three days later when your hosting provider has already implemented their own “solution.” This goes double for client sites.</p>
<h3>Using IP Blocklists</h3>
<p>IP blocklists are part of any solid firewall setup, but you need to be thoughtful about it. There are well‑established resources that publish lists of IP addresses of known scraping networks, exploiters, spam sources, and other malicious actors – data centers that legitimate traffic would never come from. Useful resources include <a href="https://www.abuseipdb.com/">AbuseIPDB</a>, <a href="https://www.spamhaus.org/blocklists/do-not-route-or-peer/">Spamhaus’s Don’t Route Or Peer Lists (DROP)</a>, <a href="https://www.blocklist.de/en/index.html">blocklist.de</a>, or <a href="https://iplists.firehol.org/">All Cybercrime IP Feeds by FireHOL</a>. </p>
<p>You can then block those IPs with a firewall like Linux’s <a href="https://wiki.archlinux.org/title/Iptables">iptables</a> via <a href="https://wiki.archlinux.org/title/Ipset">ipset</a>. This, however, requires ongoing maintenance because those lists need updating. There are ways to configure your server so that the lists get updated automatically. In my case, I am now using <a href="https://hestiacp.com">Hestia Control Panel</a> on my server, which makes configuring iptables a bit more comfortable when blocking individual IPs or whole blacklists. But you should still be very careful to not block legitimate traffic or even yourself. It is all about finding the most trustworthy lists and not being overly aggressive. And again, watching your logs.</p>
<h3>Poisoning Well</h3>
<p>For the really aggressive invaders, especially if you’re concerned about LLM scrapers harvesting your content without consent, effectively violating your copyright, there’s another approach: <a href="https://heydonworks.com/article/poisoning-well/">serve different content to suspected bots</a>, or as Heydon put it, poison the well, actually. Feed them garbage data. It won’t stop the bandwidth drain, but it does something about the copyright violation.</p>
<h3>WAFs: Blocking the Baddest Baddies</h3>
<p>The final layer of protection would be to install a web application firewall (WAF) that includes bot protection. Open-source tools like <a href="https://www.bunkerweb.io">BunkerWeb</a>, <a href="https://safepoint.cloud/landing/safeline">SafeLine</a>, or the really interesting <a href="https://anubis.techaro.lol">Anubis</a> provide multi-layered defense against bot attacks through CAPTCHA verification or other challenges, dynamic protection, and anti-replay protection. They are basically an additional layer (a reverse-proxy) between your app or website and the internet that only lets the good traffic through.</p>
<p>But the bigger you make the concrete walls around your digital garden, the more likely it might also have negative consequences. You might block legitimate crawlers. You might hurt your accessibility and block real humans. And you might spend hours installing and maintaining systems that break in weird ways. To me personally, tools that put a challenge in front of real humans still feel a bit like admitting defeat.</p>
<p>So I haven’t yet tried Anubis, for example – although it might well be the best way to really get rid of the crawlers. But I’ll definitely keep exploring this space and try out this and other tools, now that I have my own virtual private personal website server.</p>
<h2>The Web We Wanted</h2>
<p>We built the web on optimistic assumptions. We assumed good faith. We assumed people would respect robots.txt because we all understood we were building something great together. We created these protocols and conventions because we believed in mutual respect and shared purpose. The Web was supposed to be for everyone.</p>
<p>But when training data becomes worth billions of dollars, when AI capabilities determine who wins and loses in global economic competition, when scraping is a strategic commercial activity, then those assumptions break down.</p>
<p>Part of me wants to believe that we can still maintain an open web without surrendering to corporate infrastructure or turning every website into a defensive fortress shooting webspace invaders. Part of me wants to believe that international cooperation could establish better norms over time and that the companies building these AI systems will take responsibility for the externalities they’re creating. (Haha.)</p>
<p>But I’m now also old enough to be an optimistic realist. I’m watching my access logs, I’m watching the arms race escalate, and I’m wondering how long places like mine can hold out. How long before every personal website needs enterprise-level protection just to function? How long before the cost of publishing your thoughts online becomes prohibitive for regular people? For some, it might be already. </p>
<p>It feels like we might soon be forced to choose between accessibility and survival. Between openness and sustainability. Between our ideals about what the web should be and the practical reality of keeping our sites online.</p>
<h2>An Infinite Game</h2>
<p>Stories like mine are now probably happening to more small site owners everywhere. Many of them might not even know why their hosting bills are climbing or why their sites are suddenly down in certain countries. And once they find out, many of them might not be able or willing to set up a VPS and spend hours fiddling with server logs, firewalls, blocklists, and nginx config files.</p>
<p>Yes, the AI companies need to do better. They actually should throttle their scraping to reasonable levels. They actually should respect the limited resources of small sites. They actually should develop industry standards that don’t externalize costs onto individuals who are just trying to share their work. This might even be in their best interest, because ultimately, they also risk destroying the very ecosystem they depend on – after all, what will their LLMs learn from when the independent web has been scraped into extinction? But I’m also not holding my breath for voluntary restraint when there are billions of dollars at stake. In the end, it is also on us to adapt to this new reality – clear-eyed and pragmatic.</p>
<p>So what do we do?  We document what’s happening. We block scrapers. We move to better, more capable servers. We share our approaches and learn from each other. We push for better standards and regulations where we can. We make noise about the problem instead of suffering silently. Because this is the Open Web and and the Web was designed so that we <em>can</em> still do all that. That’s the magic of it.</p>
<p>I for one am not giving up my little corner on the Web, of course. I'll continue to document and write about that journey that is my personal website. Over the coming weeks, I will share my experiences with my new VPS setup – how I configured the server, how I set up my continuous deployment pipeline for Craft CMS (yes, I have that now, Manuel! 😉), how I configured caching and other performance improvements, and I’ll also finally write more about the redesign and CSS of my site. Because that’s what our little corners on the Web are all about. That’s what <a href="https://matthiasott.com/articles/into-the-personal-website-verse">the personal website verse</a> is all about. And that’s what we are trying to protect here.</p>
<p>So, if you’re running a website, check your logs. Set up some basic protections. Share what you find. Be part of this conversation. <a href="https://daverupert.com/2026/02/futurescapes/">Write about the future you want</a>. Because the alternative is watching the independent web getting scraped into oblivion while we all wonder what happened to the Web we loved.</p>
<p>Here’s the thing about Space Invaders, the game: it didn’t end when the aliens reached the bottom. It just started over, harder and faster. Maybe that’s where we are now. It’s not game over. Not yet. Just the next level. And we need to get better at playing it.</p>
<p>👾👾👾</p>
<p><em>What has your experience been with AI scrapers? Have you found approaches that work? Have you “given up” or moved to Cloudflare? I’m genuinely curious what’s happening in your corner of the Web, because I suspect we’re all dealing with variations of the same problem. So let me know via Webmention, <a href="https://mastodon.social/@matthiasott">Mastodon</a>, <a href="https://bsky.app/profile/matthiasott.com">Bluesky</a>, <a href="mailto:mail@matthiasott.com">email</a>, or maybe even in a response blog post.</em></p>

																												
																												<style>.newsletter-teaser {
display: flex; align-items: center; box-shadow: 0 3px 10px rgba(17,53,80,0.18); border-radius: 8px;margin-top: 2rem; padding: 1em 1em 0;
}</style>
<div class="newsletter-teaser">
<div><img src="http://matthiasott.com/assets/pictures/_standardImage768/OwnYourWeb-Visual.png" alt="" loading="lazy" style="display: block;
width: 25vw;
max-width: 192px; height: auto; margin: 0 1.25em 1em 0;
transform: rotate(-6deg);"></div>
<div>
																						
																																	<p><strong>Own Your Web</strong> – my newsletter about building personal websites, publishing independently, and reclaiming your corner of the Web.</p>
<p><strong>Sign up here:</strong>  <br>
👉 <a href="https://matthiasott.com/newsletter">https://matthiasott.com/newsletter</a></p>

																												
																												</div>
</div>
																						
																																						            	]]></description>
      <pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://matthiasott.com/articles/webspace-invaders</link>
      <dc:creator>Matthias Ott</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5111795579</guid>
    </item>
    <item>
      <title><![CDATA[Fresh Hot CSS: Trig Functions]]></title>
      <description><![CDATA[<p>Today’s topic is Trig Functions. I feel like Trig Functions don’t get a lot of love because nobody seems to know what
you would actually use them for. I don’t get to go over it in the talk very much. Let’s actually show a
demo of where you might want to use something like this.</p>

<h3>Trig Functions</h3>
<p>The Trig functions that are now available are as follows:</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/sin"><code>sin()</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/cos"><code>cos()</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/tan"><code>tan()</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/asin"><code>asin()</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/acos"><code>acos()</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/atan"><code>atan()</code></a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/atan2"><code>atan2()</code></a></li>
</ul>
<p>If you have ever studied Trigonometry, then these do exactly what you think they do. If you didn’t, this is all
extremely helpful for calculating angles and lengths of sides of triangles. Math stuff.</p>
<p>Let’s talk about where you’d use them.</p>
<h3>Placing things in a circle</h3>
<p>This is the classic example that shows off being able to calculate the placement of multiple items based on their angle relative to each other.</p>

      See the Pen &lt;a href="https://codepen.io/bramus/pen/YzBEXJy"&gt;
  CSS Wrapped 2023: Trig Functions&lt;/a&gt; by Bramus (&lt;a href="https://codepen.io/bramus"&gt;@bramus&lt;/a&gt;)
  on &lt;a href="https://codepen.io"&gt;CodePen&lt;/a&gt;.
      
<h3>CLOCKS!!</h3>
<p>You can now easily make a clock that will have hands that are correct</p>

      See the Pen &lt;a href="https://codepen.io/stoumann/pen/wvxOQKo"&gt;
  CSS sin() and cos() Demo 4&lt;/a&gt; by Mads Stoumann (&lt;a href="https://codepen.io/stoumann"&gt;@stoumann&lt;/a&gt;)
  on &lt;a href="https://codepen.io"&gt;CodePen&lt;/a&gt;.
      
<h3>Browser Support</h3>
<p>These functions are well supported, and can absolutely be used, today!</p>
<p></p>
<h3>More Resources</h3>
<ul>
<li><a href="https://web.dev/articles/css-trig-functions">Web.dev</a></li>
</ul>]]></description>
      <pubDate>Thu, 05 Feb 2026 05:30:25 +0000</pubDate>
      <link>https://alex.party/posts/2026-02-05-fresh-hot-css-trig-functions/</link>
      <dc:creator>Alex.Party</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5113111810</guid>
    </item>
    <item>
      <title><![CDATA[Good Tidings!]]></title>
      <description><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1J1kjGVkpvx-ADVo4ug07g.png"></figure><p>I heard from quite a few people who were excited about the Web Component Engineering course but missed the Thanksgiving sale window. So, as we wrap up the year and head into a new one, I’m running <strong>one more sale</strong> as a bit of year-end / new-year “good tidings.”</p><p>From now through the first week of the new year, you can get <strong>25% off</strong> <a href="https://bluespire.com/course/web-component-engineering">my Web Component Engineering course</a> when you use the discount code <strong>GOODTIDINGS25</strong> at checkout.</p><h3>About the&nbsp;Course</h3><p><a href="https://bluespire.com/course/web-component-engineering/">Web Component Engineering</a> is a self-paced, in-depth course on modern UI engineering through the lens of Web Components and core Web Standards. It’s designed to help you move beyond framework-only knowledge and really understand the Web Platform you’re building&nbsp;on.</p><p>You’ll get a deep dive into topics&nbsp;like:</p><ul><li>Using and authoring Web Components in real applications</li><li>Working directly with DOM APIs instead of only through frameworks and abstractions</li><li>Modern CSS for robust, scalable UI systems (including Shadow DOM styling, container queries, and&nbsp;more)</li><li>Accessibility as a first-class concern in component design</li><li>Form-associated custom elements and integrating components with real&nbsp;forms</li><li>Design systems, tokens, and component libraries built on Web Components</li><li>Application architecture, routing, and app shells with Web Components</li><li>Tools and libraries like Storybook, Playwright, Lit, FAST, and others you’ll use in production</li></ul><p>The course is trusted by engineers at top companies and is designed to be the “missing manual” for serious UI engineers who want to master the modern Web Platform.</p><h3>What You&nbsp;Get</h3><p>When you enroll, you get access&nbsp;to:</p><ul><li>13 modules with over 170 lessons, fully self-paced</li><li>A custom interactive learning app that lets you follow along, run demos, and experiment in the browser with no&nbsp;setup</li><li>Runnable examples, downloadable notes, and reference materials</li><li>A Certificate of Completion you can share with your team or&nbsp;manager</li></ul><p>Whether you’re building a design system, maintaining a complex front-end, or just want to understand what’s really possible with modern Web Standards, this is designed to meet you where you are and push you to the next&nbsp;level.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4iAAqIV2CXYGu6gpWP0TxA.png"><figcaption>The Interactive Learning Experience</figcaption></figure><h3>Sale Details</h3><ul><li><strong>Discount:</strong> 25%&nbsp;off</li><li><strong>Code:</strong> <strong>GOODTIDINGS25</strong></li><li><strong>Valid through:</strong> The end of the first week of the new&nbsp;year</li><li><strong>Course:</strong> <a href="https://bluespire.com/course/web-component-engineering/?utm_source=chatgpt.com">https://bluespire.com/course/web-component-engineering/</a></li></ul><p>If you missed the Thanksgiving window, or you’ve been on the fence, this is another chance to invest in your skills and deepen your understanding of the Web Platform.</p><p>Thanks for being part of the Web community, and here’s to a great year of building better UIs with Web Standards.</p><img src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=6f435c8162f3" width="1" height="1" alt="">]]></description>
      <pubDate>Mon, 08 Dec 2025 14:16:30 +0000</pubDate>
      <link>https://eisenbergeffect.medium.com/good-tidings-6f435c8162f3?source=rss-257e6cfa66b3------2</link>
      <dc:creator>Stories by EisenbergEffect on Medium</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5041388967</guid>
    </item>
    <item>
      <title><![CDATA[The Year Of The Linux Desktop (for fitness games)]]></title>
      <description><![CDATA[<h2>A tale told in memes</h2>
<p>I'm <a href="https://xeiaso.net/notes/2026/year-linux-desktop/">joining the bandwagon</a> and declaring that 2026 will be the Year of the Linux Desktop. Specifically for the subset of fitness gaming apps, it's no longer necessary (or preferred) to be tethered to Windows.</p>
<p>Two long-term trends have led to this moment.</p>
<p>Windows has been on a "slowly, then all at once" descent in quality that <a href="https://www.windowscentral.com/microsoft/windows-11/2025-has-been-an-awful-year-for-windows-11-with-infuriating-bugs-and-constant-unwanted-features">really started ramping up last year</a>.</p>
<p><a href="https://surfin.dog/@finn/115102306400374113"><img src="https://www.steele.blue/img/UG9RBhfwWz-600.png" alt="Mastodon post from user finn: &quot;very funny that linux didn't have to get better to gain desktop market share, the alternatives just had to get worse&quot;" decoding="async" loading="lazy" width="940" height="309" srcset="https://www.steele.blue/img/UG9RBhfwWz-600.png 600w, https://www.steele.blue/img/UG9RBhfwWz-940.png 940w" sizes="100vw"></a></p>
<p>Working with our client sysadmins on a daily basis, I get to see first-hand the OOB patches and hotfixes we've been scrambling to apply (including this weekend when Patch Tuesday <a href="https://www.thurrott.com/windows/331777/emergency-windows-11-updates-are-out-to-address-shutdown-and-remote-connection-issues">broke core RDP functionality</a>) and pray don't make the experience worse. Combined with a "Continuous Innovation" and "Controlled Feature Rollout" strategy that launches features when you least expect it, and it's made everything feel ephemeral and ready to break at any moment. When was the last time you got excited for an update, rather than dread it?</p>
<p>I don't think it's any surprise this corresponds with Microsoft's headfirst embrace of generative AI. Both internally (Satya claims 30% of Microsoft's code is AI-written) and externally (the "everything is Copilot" meme, the rebranding of Edge as an agentic browser), it's inescapable.</p>
<p><img src="https://www.steele.blue/img/ld0-K6xbrO-600.jpeg" alt="&quot;My body is a machine&quot; meme making fun of Microsoft" decoding="async" loading="lazy" width="828" height="899" srcset="https://www.steele.blue/img/ld0-K6xbrO-600.jpeg 600w, https://www.steele.blue/img/ld0-K6xbrO-828.jpeg 828w" sizes="100vw"></p>
<p>At the same time, desktop Linux has been steadily improving and growing more reliable. Some of this has to be spillover from investment in downstream OSes like ChromeOS, Android, and Steam OS, all based to some degree or other on Linux. Core peripherals like Bluetooth work across all my devices I've tried it on, on machines both <a href="https://frame.work/linux">designed to run Linux</a> and on PCs with random hardware I didn't even check compatibility with before installing.</p>
<h2>My Distro</h2>
<p>One meme I've struggled with is the tendancy for Linux desktops to start off strong, but degrade in reliability over time.</p>
<p>Historically this feels like a problem I'm also culpable for. A common experience is for userland changes applied to make a Linux desktop functional would inevitably break some unrelated part of the system. Install a new Python 3 runtime for a project, and somehow you've broken your virtual desktop setup, because it relied on 2.7.</p>
<p><a href="https://mastodon.social/@jk/113877803289325585"><img src="https://www.steele.blue/img/TPOsydsXH_-600.png" alt="Social media post from Mastodon user jk: &quot;a big reason i use linux is because it gives me Control over the computer. no windows dark pattern spying shit. i can set up everything to look and function exactly how i want. but every couple months there's a package update that overrides or changes something so it no longer works how i configured it. they just reached in and broke something i worked hard on. and i have no choice in this, because you need to eventually update packages to install new ones. i don't really feel in Control at all&quot;" decoding="async" loading="lazy" width="629" height="281" srcset="https://www.steele.blue/img/TPOsydsXH_-600.png 600w, https://www.steele.blue/img/TPOsydsXH_-629.png 629w" sizes="100vw"></a></p>
<p>The critical drive lately has been the rise of atomic desktops. These distros are "immutable", in that core filesystems are read-only and cannot be modified, even as root. The OS (and supporting packages) are updated all-at-once, and can be rolled back to a previous release if anything goes wrong.</p>
<p>Almost all userland work is accomplished with high amounts of isolation. GUI apps are Flatpak, and CLIs use Homebrew, so they don't interfere with system packages. Want to setup a development environment? On Bluefin, you're pushed to use devcontainers, so everything happens inside an OCI (here Podman) container.</p>
<p>It's pretty incredible how far you can take this approach. Bazzite (a gaming-focused distro) works so well that it's very common to as a drop-in replacement on Steam Deck (replacing an Arch-based OS). I'm running it on my gaming desktop and spent more time fighting Windows UEFI bugs than I did getting a fully-working setup.</p>
<p>Bazzite ships with Steam and its Proton compatibility layer, but my games need to run outside Steam. How does that behave? Turns out, great!</p>
<h2>Zwift</h2>
<p>My favorite <a href="https://www.steele.blue/zwift-greenscreen/">cycling-themed MMORPG</a> doesn't have a native Linux port and probably never will. It also has quite a few Windows system dependencies (its launcher relies on an Edge-based WebView2), and of course it needs access to a GPU and peripherals like Bluetooth/ANT+. I've been trying to get Zwift running for years with tools like Wine/Lutris, but every attempt at getting it running failed, and I'd inevitably corrupt my environment installing incompatible, conflicting versions of Visual C++ Redistributables.</p>
<p>This has been almost completely fixed with Kim Eik's <a href="https://github.com/netbrain/zwift">netbrain/zwift repo</a> - an all-in-one Docker container that setups an isolated Wine environment, installs Zwift, and applies a known set of working patches. The result is a setup that's easier to run than Windows.</p>
<p><img src="https://www.steele.blue/img/dKbdrGnXIY-600.png" alt="Screenshot of Zwift running on a Bazzite Linux desktop" decoding="async" loading="lazy" width="1920" height="1080" srcset="https://www.steele.blue/img/dKbdrGnXIY-600.png 600w, https://www.steele.blue/img/dKbdrGnXIY-1000.png 1000w, https://www.steele.blue/img/dKbdrGnXIY-1920.png 1920w" sizes="100vw"></p>
<p>Not everything works yet. In particular, Bluetooth (needed to interact with the trainer, heart rate monitor, etc) isn't available, though <a href="https://github.com/netbrain/zwift/issues/188">there's ongoing development</a> that I'm hopeful for.</p>
<p>In the meantime, I'm setting things up using alternate means. My trainer <a href="https://support.wahoofitness.com/hc/en-us/articles/9211851310738-Using-Wi-Fi-with-a-KICKR-trainer-BIKE-or-RUN">supports Wi-Fi connections</a>, so no Bluetooth is needed for power, cadence, and resistance. I even set up the trainer on an isolated VLAN (as it's essentially an expensive IoT device I don't have full control over), and Podman was able to discover it after setting <code>networking=host</code>.</p>
<p>Other peripherals (controllers, HR monitors) still require Bluetooth, so I've just got an old Android device running <a href="https://www.zwift.com/companion">Zwift Companion</a> sitting on my trainer desk. Honestly this is the most "non-native" part of my entire setup, but I'm still using supported tools (and was having to use the Companion even on Windows after an update broke BLE connections).</p>
<p>Zwift runs at a clean 60fps, and I'm able to <a href="https://stream.steele.blue/">stream to Owncast</a> via OBS just like before.</p>
<h2>Beat Saber</h2>
<p>This one I thought would be more challenging. As a VR rhythm game, I'd expect significant hurdles to a clean, performant experience.</p>
<p>My setup has always been a little unorthodox: on Windows I've been running a modded Beat Saber, installed using <a href="https://www.bsmanager.io/">BSManager</a> to setup extra songs/mods, and control which version I run. The game then streams to a Quest 2 headset, and I pray the latency gods are favorable. I spent the better part of last year getting this setup working well on Windows, and settled on tweaking Virtual Desktop to stream the game, limiting the resolution and frame rate along the way. Other tools like Oculus Link and Steam's own wireless streaming just weren't cutting it.</p>
<p><lite-youtube videoid="l6kEJ94mC3I"></lite-youtube></p>
<p>Turns out, Linux VR gaming is pretty solid these days. The <a href="https://lvra.gitlab.io/">LVRA community</a> has been compiling tools and best practices for years, and many things are turnkey now. Need to stream to a Quest headset? Pick between <a href="https://lvra.gitlab.io/docs/steamvr/alvr/">ALVR</a> and <a href="https://lvra.gitlab.io/docs/fossvr/wivrn/">WiVRn</a>. And again, both are installed as Flatpaks, so if you get it wrong, it's super easy to change course!</p>
<p>I went with WiVRn, as it bundles its own, Steam-independent OpenXR runtime, which has been supported by Beat Saber for years. And with a few small changes to the <a href="https://github.com/WiVRn/WiVRn/blob/master/docs/steamvr.md">command-line arguments in Steam</a>, I had vanilla Beat Saber running on my headset almost immediately.
Interestingly, performance seemed to be even better on Linux than Windows; I was streaming at 120fps (on Windows I limited to 72) and my audio latency was down by nearly a third (~60ms compared to 90 on Windows).
This isn't uncommon, quite a few benchmarks a game running on Linux can <a href="https://arstechnica.com/gaming/2025/06/games-run-faster-on-steamos-than-windows-11-ars-testing-finds/">outperform its Windows counterpart</a>.</p>
<p>Finally, I wanted to get modded Beat Saber running. Luckily BSManager provides a Linux flatpak that installed without issue, and I could download and mod versions just like on Windows.
Getting it running on the headset proved to be a much bigger challenge. Out of the box, the WiVRn app list on the device wasn't detecting the modded Beat Saber, so I couldn't start it.</p>
<p>With some help from the BSManager Discord, I found the set of environment variables that needed set (within BSManager) to get it to launch. But the workflow was pretty horrendous: start WiVRn on the PC, then launch the client on the headset. Then take the headset <em>off</em> and launch Beat Saber on the PC. Then, put the headset back on and start playing.</p>
<p><img src="https://www.steele.blue/img/zUKLvPJTjP-600.png" alt="Screenshot of Beat Saber running on a Bazzite Linux desktop" decoding="async" loading="lazy" width="1920" height="1080" srcset="https://www.steele.blue/img/zUKLvPJTjP-600.png 600w, https://www.steele.blue/img/zUKLvPJTjP-1000.png 1000w, https://www.steele.blue/img/zUKLvPJTjP-1920.png 1920w" sizes="100vw"></p>
<p>It turns out there were a few bugs in both BSManager and WiVRn that prevented the app from being discovered, and launching properly. With some trial and error (and reading a bit more C++ than I was ready for), I was able to get it running. The details are in <a href="https://github.com/Zagrios/bs-manager/issues/968#issuecomment-3766039142">this GitHub Issue</a> - and fixes have already landed in the WiVRn codebase.</p>
<p>This feels like a telling anecdote where the Linux gaming ecosystem is at: the fundamental components to play even complicated games are solid, and have been steadily improving. And when things don't work, there's a community to help address it. Hopefully more of this knowledge will get pulled out of private Discords and into public knowledge bases (or simply fixed at the root).</p>
<h2>I can tinker with Linux when I need to cool down</h2>
<p>I feel like I've just scratched the surface of what a Linux gaming PC can do. My library is pretty small, but I expect to grow it, especially with the <a href="https://store.steampowered.com/sale/steamframe">Steam Frame</a> inbound, offering another VR headset running Linux directly.</p>
<p>Of course there are other neat features you can try with a Linux machine with a decent GPU. Bluefin's AI strategy is one of local operation and control. So if you want to experiment with LLMs, you can <a href="https://docs.projectbluefin.io/ai/#ramalama">install Ramalama</a>, pull down a model from Huggingface you're comfortable with, and use your own GPU and resources. It feels like a breath of fresh air to have control of what's running on your machine, compared to the Windows experience of "what fresh hell with this restart bring today".</p>
]]></description>
      <pubDate>Sun, 25 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://www.steele.blue/linux-desktop-zwift-beat-saber/</link>
      <dc:creator>Matt Steele</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5095033904</guid>
    </item>
    <item>
      <title><![CDATA[Almost Plain Text, Nicely Done]]></title>
      <description><![CDATA[
<p class="wp-block-paragraph">Just got two emails that I thought were quite nice. It reminds me of an opinion I’ve really held strong for quite a while. I even <a href="https://frontendmasters.com/blog/simple-typographic-email-template/">made a template for it one time</a>, and I wrote:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">if we go for HTML email, we can apply&nbsp;<em>just enough</em>&nbsp;layout and styles to make it almost like a plain text email, only with a bit more class.</p>



<ul class="wp-block-list">
<li>A pleasant readable typeface</li>



<li>Reasonable line length</li>



<li>Breathable line height</li>



<li>Anchor links on words</li>



<li>Dark/light mode</li>
</ul>
</blockquote>



<p class="wp-block-paragraph">Here’s one from Cursor:</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="996" data-attachment-id="9846" data-permalink="https://email-is-good.com/2026/01/15/almost-plain-text-nicely-done/screenshot-2026-01-15-at-6-34-13-am/" data-orig-file="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?fit=1552%2C1510&amp;ssl=1" data-orig-size="1552,1510" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Screenshot 2026-01-15 at 6.34.13 AM" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?fit=300%2C292&amp;ssl=1" data-large-file="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?fit=1024%2C996&amp;ssl=1" src="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?resize=1024%2C996&amp;ssl=1" alt="Email notification about the release of GPT-5.2 Codex in Cursor, addressed to Chris, highlighting its capabilities for long-running tasks." class="wp-image-9846" srcset="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?resize=1024%2C996&amp;ssl=1 1024w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?resize=300%2C292&amp;ssl=1 300w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?resize=768%2C747&amp;ssl=1 768w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?resize=1536%2C1494&amp;ssl=1 1536w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?resize=1200%2C1168&amp;ssl=1 1200w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?resize=1320%2C1284&amp;ssl=1 1320w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.34.13-AM.png?w=1552&amp;ssl=1 1552w" sizes="auto, (max-width: 1000px) 100vw, 1000px"></figure>



<p class="wp-block-paragraph">I find a “button” style could be added to that short list of OK things in an otherwise chill HTML email. But I <em>really</em> like how short and sweet the email is. Begs to be actually read.</p>



<p class="wp-block-paragraph">And one from Mandy Brown:</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="787" data-attachment-id="9848" data-permalink="https://email-is-good.com/2026/01/15/almost-plain-text-nicely-done/screenshot-2026-01-15-at-6-29-20-am/" data-orig-file="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?fit=1518%2C1166&amp;ssl=1" data-orig-size="1518,1166" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Screenshot 2026-01-15 at 6.29.20 AM" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?fit=300%2C230&amp;ssl=1" data-large-file="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?fit=1024%2C787&amp;ssl=1" src="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?resize=1024%2C787&amp;ssl=1" alt="Email screenshot discussing the concept of 'burnout' and its evolving meanings, authored by Mandy Brown." class="wp-image-9848" srcset="https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?resize=1024%2C787&amp;ssl=1 1024w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?resize=300%2C230&amp;ssl=1 300w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?resize=768%2C590&amp;ssl=1 768w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?resize=1200%2C922&amp;ssl=1 1200w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?resize=1320%2C1014&amp;ssl=1 1320w, https://i0.wp.com/email-is-good.com/wp-content/uploads/2026/01/Screenshot-2026-01-15-at-6.29.20-AM.png?w=1518&amp;ssl=1 1518w" sizes="auto, (max-width: 1000px) 100vw, 1000px"></figure>



<p class="wp-block-paragraph">Probably the nicest typography of any newsletter ever. But really isn’t doing <em>that</em> much to pull it off, just a nice typeface with smart setting.</p>
]]></description>
      <pubDate>Fri, 16 Jan 2026 01:44:07 +0000</pubDate>
      <link>https://email-is-good.com/2026/01/15/almost-plain-text-nicely-done/</link>
      <dc:creator>Email is good.</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5084501228</guid>
    </item>
    <item>
      <title><![CDATA[Progressive Enhancement and Web Components]]></title>
      <description><![CDATA[Now that web components have gained more traction over the last few years (used by companies like Salesforce, Github, and Adobe), we should explore how web components can offer progressive enhancement to applications without the bloat of JavaScript frameworks.]]></description>
      <pubDate>Wed, 16 Jul 2025 10:00:00 +0000</pubDate>
      <link>https://sparkbox.com/foundry/progressive_enhancement_and_web_components</link>
      <dc:creator>The Foundry from Sparkbox</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4883179220</guid>
    </item>
    <item>
      <title><![CDATA[Testing HTML Light DOM Web Components: Easier Than Expected!]]></title>
      <description><![CDATA[
<figure class="wp-block-image size-large"><img width="1024" height="576" src="https://cloudfour.com/wp-content/uploads/2025/11/light-dom-wc-test-feature-r1-1024x576.jpg" alt="An HTML custom element, `<light-dom-component>` wrapping three topics designed with green checkmarks to illustrate passing tests: render to DOM, accessibility, events" class="wp-image-8432" srcset="https://cloudfour.com/wp-content/uploads/2025/11/light-dom-wc-test-feature-r1-1024x576.jpg 1024w, https://cloudfour.com/wp-content/uploads/2025/11/light-dom-wc-test-feature-r1-300x169.jpg 300w, https://cloudfour.com/wp-content/uploads/2025/11/light-dom-wc-test-feature-r1-768x432.jpg 768w, https://cloudfour.com/wp-content/uploads/2025/11/light-dom-wc-test-feature-r1-1536x864.jpg 1536w, https://cloudfour.com/wp-content/uploads/2025/11/light-dom-wc-test-feature-r1.avif 1600w" sizes="(max-width: 1024px) 100vw, 1024px"></figure>



<p>A recent project of ours involved <a href="https://cloudfour.com/topics/app-modernization/">modernizing</a> a large, decades-old legacy web application. Our fantastic design team redesigned the interfaces and created <a href="https://cloudfour.com/thinks/responsive-design-process-that-works/">in-browser prototypes</a> that we referenced throughout development as we built HTML/CSS patterns and <a href="https://cloudfour.com/topics/web-components/">HTML web components</a>. For the web components, we used the <a href="https://meyerweb.com/eric/thoughts/2023/11/01/blinded-by-the-light-dom/">Light DOM</a> and <a href="https://cloudfour.com/thinks/web-components-as-progressive-enhancement/">progressive enhancement</a> where possible, keeping accessibility front and center.</p>



<p>Going in, we weren’t sure how challenging it’d be to write tests for HTML web components. <strong>Spoiler alert:</strong> It wasn’t too different than testing framework-specific components (e.g., Vue or React) and in some cases, even easier! </p>



<p>For a project of this scope, a strong test suite was crucial, as it enabled us to focus on new features instead of constantly fixing regression bugs and repeating <em>manual</em> browser testing again and again. These are the patterns that worked well for us.</p>



<h2 class="wp-block-heading">The web component testing stack we used</h2>



<p>Our testing stack consisted of:</p>



<ul class="wp-block-list">
<li><a href="https://vitest.dev/">Vitest</a> – Fast, ESM-native test runner with Vite integration using a <a href="https://vitest.dev/config/environment.html#environment"><code>jsdom</code> environment</a></li>



<li>Lit’s <a href="https://lit.dev/docs/components/rendering/"><code>render()</code> function</a> and <a href="https://lit.dev/docs/templates/overview/"><code>html</code> templates</a> to render to the DOM</li>



<li>The <a href="https://testing-library.com/docs/dom-testing-library/intro/">DOM Testing Library</a> (no framework wrapper), along with:
<ul class="wp-block-list">
<li><a href="https://testing-library.com/docs/user-event/intro/"><code>@testing-library/user-event</code></a> – User interaction simulation</li>



<li><a href="https://testing-library.com/docs/ecosystem-jest-dom"><code>@testing-library/jest-dom</code></a> – DOM-specific matchers, for example <code>toBeVisible()</code>, <code>toBeChecked()</code>, and <code>toHaveFocus()</code></li>
</ul>
</li>



<li><a href="https://github.com/chaance/vitest-axe"><code>vitest-axe</code></a> – Automated accessibility rule checking (to catch low-hanging fruit)</li>



<li><a href="https://testing-library.com/docs/ecosystem-eslint-plugin-testing-library"><code>eslint-plugin-testing-library</code></a> and <a href="https://testing-library.com/docs/ecosystem-eslint-plugin-jest-dom"><code>eslint-plugin-jest-dom</code></a> ESLint plugins to help enforce testing best practices</li>
</ul>



<p>Most of these tools are standard. The interesting choice here is using Lit’s <code>html</code> and <code>render()</code> in the tests. We built our first (and one of the most complex) web components with Lit, then switched to vanilla web components for the rest. Since <code>lit</code> was already a dependency, we continued to use its <a href="https://lit.dev/docs/templates/overview/">templating features</a>, <a href="https://lit.dev/docs/templates/expressions/">expressions</a>, <a href="https://lit.dev/docs/templates/conditionals/">conditionals</a>, and <a href="https://lit.dev/docs/templates/directives/">directives</a> in our tests. A few of the benefits:</p>



<ul class="wp-block-list">
<li>Setting up new tests included no manual DOM manipulation</li>



<li>Provided a declarative HTML-like syntax with editor syntax highlighting</li>



<li>We were able to create shared parameterizable example/demo Lit templates (shared by tests <em>and</em> Storybook stories), which significantly reduced the boilerplate</li>



<li>Helped standardize how each test (and Storybook story) should be set up for easier maintenance</li>



<li>The rendered HTML is immediately in the DOM, which means Testing Library queries and standard DOM APIs work without special setup</li>



<li>Better TypeScript support within our tests</li>
</ul>



<p>Overall, a better, more efficient developer experience.</p>



<p class="has-gray-lighter-background-color has-background">Worth noting, there is also a <a href="https://lit.dev/docs/libraries/standalone-templates/">standalone <code>lit-html</code> library</a> that we could have used instead of the full <code>lit</code> package. <code>lit-html</code> provides <code>html</code> and <code>render</code>, as well as the directive modules. Today I learned. 🙂</p>



<h2 class="wp-block-heading">Light DOM web components simplified testing</h2>



<p>One of the most impactful early architectural decisions we made was to build all web components using <a href="https://meyerweb.com/eric/thoughts/2023/11/01/blinded-by-the-light-dom/">the Light DOM</a> instead of Shadow DOM. While we didn’t realize it at the time, it dramatically simplified testing and component composition.</p>



<p>From a testing perspective, it meant we could query anything, anywhere, anytime:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript">render(
  html`<span class="xml">
    <span class="hljs-tag">&lt;<span class="hljs-name">my-amazing-component</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">my-tree-component</span> 
        <span class="hljs-attr">data</span>=</span></span><span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(jsonData())}</span><span class="xml"><span class="hljs-tag">
      &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">my-tree-component</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">dialog</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">dialog</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">my-amazing-component</span>&gt;</span>
  `</span>,
  <span class="hljs-built_in">document</span>.body,
);</code></span></pre>


<p>With <strong>Shadow DOM</strong>, we’d need something like:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript"><span class="hljs-comment">// ❌ What you'd have to do with Shadow DOM</span>
<span class="hljs-keyword">const</span> component = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'my-amazing-component'</span>);
<span class="hljs-keyword">const</span> shadowRoot = component.shadowRoot; <span class="hljs-comment">// May be null!</span>
<span class="hljs-keyword">const</span> button = shadowRoot?.querySelector(<span class="hljs-string">'button'</span>); <span class="hljs-comment">// Doesn't cross boundaries</span>
<span class="hljs-comment">// Or use special testing utilities that pierce shadow boundaries</span>
<span class="hljs-comment">// And if there are nested shadow boundaries, *eek!* 😬</span></code></span></pre>


<p>With <strong>Light DOM</strong>, it’s much simpler:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript"><span class="hljs-comment">// ✅ What we actually do</span>
<span class="hljs-keyword">const</span> component = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'my-amazing-component'</span>);
<span class="hljs-keyword">const</span> button = component.querySelector(<span class="hljs-string">'button'</span>); <span class="hljs-comment">// Just works</span></code></span></pre>


<p>And <strong>Testing Library queries</strong> also <em>just work</em>:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript">render(validationExample(), <span class="hljs-built_in">document</span>.body);

<span class="hljs-comment">// screen.getByRole() finds elements INSIDE your components</span>
<span class="hljs-keyword">const</span> emailInput = screen.getByRole(<span class="hljs-string">'textbox'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/email/i</span> });
<span class="hljs-keyword">const</span> submitBtn = screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-string">'Submit'</span> });

<span class="hljs-comment">// These work because there's no Shadow DOM boundary blocking queries</span>
<span class="hljs-keyword">await</span> user.click(submitBtn);
expect(emailInput).toHaveAttribute(<span class="hljs-string">'aria-invalid'</span>, <span class="hljs-string">'true'</span>);
expect(emailInput).toHaveFocus();</code></span></pre>


<p>Testing Library’s philosophy is “query like a user would.” With Light DOM web components, the mental model matches perfectly.</p>



<h2 class="wp-block-heading">Testing web component events</h2>



<p>Most, if not all, web components we built dispatched <a href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent">custom events with <code>detail</code> data</a>. We used the following Vitest features to confirm the expected event data:</p>



<ul class="wp-block-list">
<li><a href="https://vitest.dev/api/vi.html#vi-fn"><code>vi.fn()</code> function spy</a></li>



<li><a href="https://vitest.dev/api/expect.html#tohavebeencalledwith"><code>toHaveBeenCalledWith()</code> assertion</a></li>



<li><a href="https://vitest.dev/api/expect.html#expect-objectcontaining"><code>objectContaining()</code> assertion</a></li>
</ul>


<pre class="wp-block-code"><span><code class="hljs language-typescript shcb-code-table"><span class="shcb-loc"><span>it(<span class="hljs-string">'Emits change event when "Confirm" is clicked'</span>, <span class="hljs-keyword">async</span> () =&gt; {
</span></span><span class="shcb-loc"><span>  <span class="hljs-keyword">const</span> user = userEvent.setup();
</span></span><span class="shcb-loc"><span>  render(
</span></span><span class="shcb-loc"><span>    html`<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">my-tree-component</span> </span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag">      <span class="hljs-attr">data</span>=</span></span><span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(jsonData())}</span><span class="xml"><span class="hljs-tag"></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">    &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">my-tree-component</span>&gt;</span>`</span>,</span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">    <span class="hljs-built_in">document</span>.body,</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  );</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-comment">// Set up the handler function spy for the 'change' listener</span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-comment">// Attached to `document` to confirm event bubbles</span></span></span></span></span></span>
</span></span><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-keyword">const</span> changeHandler = vi.fn();</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'my-tree-component-change'</span>, changeHandler);</span></span></span></span></span>
</span></mark><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-comment">// Click confirm button</span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-keyword">const</span> confirmButton = screen.getByRole(<span class="hljs-string">'button'</span>, { name: <span class="hljs-regexp">/^confirm$/i</span> });</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-keyword">await</span> user.click(confirmButton);</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-comment">// Event should be emitted with correct details</span></span></span></span></span></span>
</span></span><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  expect(changeHandler).toHaveBeenCalledWith(</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">    expect.objectContaining({</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">      <span class="hljs-keyword">type</span>: <span class="hljs-string">'my-tree-component-change'</span>,</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">      detail: {</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">        action: <span class="hljs-string">'change'</span>,</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">        selectedEntityIds: [<span class="hljs-string">'test1'</span>],</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">      },</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">      bubbles: <span class="hljs-literal">true</span>,</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">    }),</span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  );</span></span></span></span></span>
</span></mark><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">	</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-comment">// Clean up the event listener</span></span></span></span></span></span>
</span></span><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-built_in">document</span>.removeEventListener(<span class="hljs-string">'my-tree-component-change'</span>, changeHandler);</span></span></span></span></span>
</span></mark><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">});</span></span></span></span></span>
</span></span></code></span></pre>




<h2 class="wp-block-heading">Testing hidden inputs generated by web components</h2>



<p>One of our goals was to minimize the need for legacy backend code refactors. The legacy application relied on good ol’ traditional form submission architecture. Some of the legacy UI relied on JavaScript-generated hidden inputs that satisfied the backend code. Our new web components needed to match this behavior.</p>



<p>When testing the hidden inputs feature of a web component, Light DOM web components made it much simpler because any hidden inputs created by the web component are included in the form submission automatically:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript shcb-code-table"><span class="shcb-loc"><span>render(
</span></span><span class="shcb-loc"><span>  html`<span class="xml"></span>
</span></span><span class="shcb-loc"><span><span class="xml">    <span class="hljs-tag">&lt;<span class="hljs-name">form</span>&gt;</span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag">      <span class="hljs-tag">&lt;<span class="hljs-name">my-tree-component</span> </span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag">        <span class="hljs-attr">data</span>=</span></span><span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(jsonData())}</span><span class="xml"><span class="hljs-tag"></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">      &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">my-tree-component</span>&gt;</span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Submit the form<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag">    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag">  `</span>,</span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag">  <span class="hljs-built_in">document</span>.body,</span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag">);</span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-comment">// Get the form</span></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-keyword">const</span> form = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'form'</span>) <span class="hljs-keyword">as</span> HTMLFormElement;</span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-comment">// Hidden inputs are in Light DOM, so form submission includes them automatically</span></span></span></span></span></span></span></span></span>
</span></span><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-keyword">const</span> hiddenInputs = form.querySelectorAll(<span class="hljs-string">'input[type="hidden"][name="entity"]'</span>);</span></span></span></span></span></span></span></span>
</span></mark><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag">expect(hiddenInputs).toHaveLength(<span class="hljs-number">5</span>);</span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-comment">// They participate in form submission naturally</span></span></span></span></span></span></span></span></span>
</span></span><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(form);</span></span></span></span></span></span></span></span>
</span></mark><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-keyword">const</span> entities = formData.getAll(<span class="hljs-string">'entities'</span>);</span></span></span></span></span></span></span></span>
</span></mark><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-tag"><span class="hljs-tag">expect(entities).toHaveLength(<span class="hljs-number">5</span>);</span></span></span></span></span></span></span></span>
</span></span></code></span></pre>


<p>In some cases, we needed to confirm the hidden inputs rendered in a specific order with specific prefixed values. To test this, <a href="https://vitest.dev/api/expect.html#tomatch">Vitest’s <code>toMatch()</code> assertion with regular expressions</a> came in handy:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript">expect(entities[<span class="hljs-number">0</span>]).toMatch(<span class="hljs-regexp">/^OP(AND|OR|NOR|NAND)$/</span>);
expect(entities[<span class="hljs-number">1</span>]).toMatch(<span class="hljs-regexp">/^EL/</span>);
expect(entities[<span class="hljs-number">2</span>]).toMatch(<span class="hljs-regexp">/^OP(AND|OR|NOR|NAND)$/</span>);
expect(entities[<span class="hljs-number">3</span>]).toMatch(<span class="hljs-regexp">/^EL/</span>);
expect(entities[<span class="hljs-number">4</span>]).toMatch(<span class="hljs-regexp">/^EL/</span>);</code></span></pre>


<h2 class="wp-block-heading">Testing both attribute and property APIs</h2>



<p>Most of the web components supported both declarative (HTML attributes) and imperative (JavaScript properties) APIs. We added basic “render” tests for each use case:</p>


<pre class="wp-block-code"><span><code class="hljs language-typescript shcb-code-table"><span class="shcb-loc"><span>it(<span class="hljs-string">'Renders via `content` attribute'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
</span></span><span class="shcb-loc"><span>  render(
</span></span><span class="shcb-loc"><span>    html`<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">my-data-table</span></span></span>
</span></span><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag">      <span class="hljs-attr">data</span>=</span></span><span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(jsonData())}</span><span class="xml"><span class="hljs-tag"></span></span>
</span></mark><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">    &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">my-data-table</span>&gt;</span>`</span>,</span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">    <span class="hljs-built_in">document</span>.body,</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  );</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  </span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-keyword">const</span> tableEl = screen.getByRole(<span class="hljs-string">'table'</span>);</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  expect(tableEl).toBeVisible();</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">});</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">it(<span class="hljs-string">'Renders via `content` property'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-function">  render(html`<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">my-data-table</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">my-data-table</span>&gt;</span>`</span>, <span class="hljs-built_in">document</span>.body);</span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-function"><span class="xml"><span class="hljs-tag"><span class="hljs-tag">		</span></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-function"><span class="xml"><span class="hljs-tag"><span class="hljs-tag">  <span class="hljs-comment">// Set the JSON data via the property</span></span></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-function"><span class="xml"><span class="hljs-tag"><span class="hljs-tag">  <span class="hljs-keyword">const</span> component = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'my-data-table'</span>) <span class="hljs-keyword">as</span> MyDataTable;</span></span></span></span></span></span></span></span></span>
</span></span><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-function"><span class="xml"><span class="hljs-tag"><span class="hljs-tag">  component.data = jsonData();</span></span></span></span></span></span></span></span></span>
</span></mark><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-function"><span class="xml"><span class="hljs-tag"><span class="hljs-tag">  </span></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-function"><span class="xml"><span class="hljs-tag"><span class="hljs-tag">  <span class="hljs-keyword">const</span> tableEl = screen.getByRole(<span class="hljs-string">'table'</span>);</span></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-function"><span class="xml"><span class="hljs-tag"><span class="hljs-tag">  expect(tableEl).toBeVisible();</span></span></span></span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-function"><span class="xml"><span class="hljs-tag"><span class="hljs-tag">});</span></span></span></span></span></span></span></span></span>
</span></span></code></span></pre>


<h2 class="wp-block-heading">Typing web component references</h2>



<p>We wrote all web components and tests in TypeScript. This helped catch API changes anytime we refactored or fixed bugs. In tests where we wanted to access component properties or methods, we needed to add a <a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions">type assertion</a> since <code>querySelector()</code> can possibly return <code>null</code>:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript shcb-code-table"><span class="shcb-loc"><span>render(
</span></span><span class="shcb-loc"><span>  html`<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">my-tree-component</span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag">    <span class="hljs-attr">data</span>=</span></span><span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(jsonData())}</span><span class="xml"><span class="hljs-tag"></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">my-tree-component</span>&gt;</span>`</span>,</span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-built_in">document</span>.body,</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">);</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-comment">// Use a type assertion since we know the element is in the DOM</span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-keyword">const</span> myTreeComponent = <span class="hljs-built_in">document</span>.querySelector(</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">  <span class="hljs-string">'my-tree-component'</span>,</span></span></span></span></span>
</span></span><mark class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">) <span class="hljs-keyword">as</span> MyTreeComponent; </span></span></span></span></span>
</span></mark><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag"><span class="hljs-comment">// Now you have full TypeScript support</span></span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">expect(myTreeComponent.treeNodes).toHaveLength(<span class="hljs-number">21</span>);</span></span></span></span></span>
</span></span><span class="shcb-loc"><span><span class="xml"><span class="hljs-tag"><span class="hljs-subst"><span class="xml"><span class="hljs-tag">myTreeComponent.data = newData; <span class="hljs-comment">// Type-safe property access</span></span></span></span></span></span>
</span></span></code></span></pre>


<h2 class="wp-block-heading">Tests and Storybook stories shared Lit <code>html</code> templates</h2>



<p>As mentioned earlier, sharing Lit <code>html</code> templates helped reduce boilerplate and standardized how we set up all tests and Storybook stories. Below is an example Lit <code>html</code> template for an input validator component:</p>


<pre class="wp-block-code"><span><code class="hljs language-typescript"><span class="hljs-comment">// input-validator-example.ts</span>

<span class="hljs-keyword">import</span> { html } <span class="hljs-keyword">from</span> <span class="hljs-string">'lit'</span>;
<span class="hljs-keyword">import</span> { ifDefined } <span class="hljs-keyword">from</span> <span class="hljs-string">'lit/directives/if-defined.js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> InputValidatorExampleArgs {
	<span class="hljs-keyword">type</span>: HTMLInputElement[<span class="hljs-string">'type'</span>];
	validationError?: <span class="hljs-built_in">string</span>;
	required?: <span class="hljs-built_in">boolean</span>;
	pattern?: <span class="hljs-built_in">string</span>;
	ariaDescribedby?: <span class="hljs-built_in">string</span>;
}

<span class="hljs-comment">/**
 * Used by tests and Storybook stories.
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">inputValidatorExample</span>(<span class="hljs-params">{
	<span class="hljs-keyword">type</span>,
	validationError,
	required = <span class="hljs-literal">true</span>,
	pattern,
	ariaDescribedby,
}: InputValidatorExampleArgs</span>) </span>{
	<span class="hljs-keyword">const</span> inputId = <span class="hljs-string">`input-<span class="hljs-subst">${<span class="hljs-keyword">type</span>}</span>`</span>;
	<span class="hljs-keyword">let</span> label = <span class="hljs-keyword">type</span>.charAt(<span class="hljs-number">0</span>).toUpperCase() + <span class="hljs-keyword">type</span>.slice(<span class="hljs-number">1</span>);

	<span class="hljs-keyword">if</span> (<span class="hljs-keyword">type</span> === <span class="hljs-string">'select'</span>) {
		label = <span class="hljs-string">`<span class="hljs-subst">${label}</span> an option`</span>;
	}

	<span class="hljs-keyword">if</span> (<span class="hljs-keyword">type</span> === <span class="hljs-string">'text'</span> &amp;&amp; pattern) {
		label = <span class="hljs-string">`<span class="hljs-subst">${label}</span> with regex pattern`</span>;
	}

	<span class="hljs-keyword">let</span> field;
	<span class="hljs-keyword">if</span> (<span class="hljs-keyword">type</span> === <span class="hljs-string">'select'</span>) {
		field = html`<span class="xml">
			<span class="hljs-tag">&lt;<span class="hljs-name">select</span>
				<span class="hljs-attr">id</span>=</span></span><span class="hljs-subst">${inputId}</span><span class="xml"><span class="hljs-tag">
				?<span class="hljs-attr">required</span>=</span></span><span class="hljs-subst">${required}</span><span class="xml"><span class="hljs-tag">
				?<span class="hljs-attr">pattern</span>=</span></span><span class="hljs-subst">${pattern}</span><span class="xml"><span class="hljs-tag">
				<span class="hljs-attr">aria-describedby</span>=</span></span><span class="hljs-subst">${ifDefined(ariaDescribedby)}</span><span class="xml"><span class="hljs-tag">
			&gt;</span>
				<span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">""</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
				<span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"1"</span>&gt;</span>Option 1<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
				<span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"2"</span>&gt;</span>Option 2<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
				<span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"3"</span>&gt;</span>Option 3<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
			<span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
		`</span>;
	} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-keyword">type</span> === <span class="hljs-string">'textarea'</span>) {
		field = html`<span class="xml">
			<span class="hljs-tag">&lt;<span class="hljs-name">textarea</span>
				<span class="hljs-attr">id</span>=</span></span><span class="hljs-subst">${inputId}</span><span class="xml"><span class="hljs-tag">
				<span class="hljs-attr">minlength</span>=<span class="hljs-string">"10"</span>
				?<span class="hljs-attr">required</span>=</span></span><span class="hljs-subst">${required}</span><span class="xml"><span class="hljs-tag">
				?<span class="hljs-attr">pattern</span>=</span></span><span class="hljs-subst">${pattern}</span><span class="xml"><span class="hljs-tag">
				<span class="hljs-attr">aria-describedby</span>=</span></span><span class="hljs-subst">${ifDefined(ariaDescribedby)}</span><span class="xml"><span class="hljs-tag">
			&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">textarea</span>&gt;</span>
		`</span>;
	} <span class="hljs-keyword">else</span> {
		field = html`<span class="xml"> <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
			<span class="hljs-attr">id</span>=</span></span><span class="hljs-subst">${inputId}</span><span class="xml"><span class="hljs-tag">
			<span class="hljs-attr">.type</span>=</span></span><span class="hljs-subst">${<span class="hljs-keyword">type</span>}</span><span class="xml"><span class="hljs-tag">
			<span class="hljs-attr">minlength</span>=</span></span><span class="hljs-subst">${ifDefined(<span class="hljs-keyword">type</span> === <span class="hljs-string">'password'</span> ? <span class="hljs-string">'5'</span> : <span class="hljs-literal">undefined</span>)}</span><span class="xml"><span class="hljs-tag">
			?<span class="hljs-attr">required</span>=</span></span><span class="hljs-subst">${required}</span><span class="xml"><span class="hljs-tag">
			<span class="hljs-attr">.pattern</span>=</span></span><span class="hljs-subst">${ifDefined(pattern) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>}</span><span class="xml"><span class="hljs-tag">
			<span class="hljs-attr">aria-describedby</span>=</span></span><span class="hljs-subst">${ifDefined(ariaDescribedby)}</span><span class="xml"><span class="hljs-tag">
		/&gt;</span>`</span>;
	}

	<span class="hljs-keyword">return</span> html`<span class="xml">
		<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-group"</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=</span></span><span class="hljs-subst">${inputId}</span><span class="xml"><span class="hljs-tag">&gt;</span></span><span class="hljs-subst">${label}</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
			<span class="hljs-tag">&lt;<span class="hljs-name">input-validator</span> <span class="hljs-attr">validation-error</span>=</span></span><span class="hljs-subst">${ifDefined(validationError)}</span><span class="xml"><span class="hljs-tag">&gt;</span>
				</span><span class="hljs-subst">${field}</span><span class="xml">
			<span class="hljs-tag">&lt;/<span class="hljs-name">input-validator</span>&gt;</span>
		<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
	`</span>;
}</code></span></pre>


<p>This allowed us to use the same HTML for both tests and Storybook stories:</p>


<pre class="wp-block-code"><span><code class="hljs language-typescript"><span class="hljs-comment">// InputValidator.test.ts</span>

render(inputValidatorExample({ <span class="hljs-keyword">type</span>: <span class="hljs-string">'tel'</span> }), <span class="hljs-built_in">document</span>.body);</code></span></pre>

<pre class="wp-block-code"><span><code class="hljs language-typescript"><span class="hljs-comment">// InputValidator.stories.ts</span>

<span class="hljs-comment">/**
 * The component supports various `&lt;input&gt;` `type` values. Below are examples
 * of different input types that can be used with the component.
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> VariousInputTypesSupported: Story = {
  render: <span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span>
    html`<span class="hljs-subst">${[
      <span class="hljs-string">'email'</span>,
      <span class="hljs-string">'url'</span>,
      <span class="hljs-string">'password'</span>,
      <span class="hljs-string">'tel'</span>,
      <span class="hljs-string">'number'</span>,
      <span class="hljs-string">'date'</span>,
      <span class="hljs-string">'time'</span>,
      <span class="hljs-string">'datetime-local'</span>,
      <span class="hljs-string">'month'</span>,
      <span class="hljs-string">'week'</span>,
      <span class="hljs-string">'search'</span>,
      <span class="hljs-string">'text'</span>,
      <span class="hljs-string">'checkbox'</span>,
    ].map((<span class="hljs-keyword">type</span>) =&gt; inputValidatorExample({ <span class="hljs-keyword">type</span> }</span><span class="xml">))}`</span>,
};</code></span></pre>


<h2 class="wp-block-heading">Testing for accessibility</h2>



<p><a href="https://cloudfour.com/topics/accessibility/">Building accessible user interfaces</a> is something we believe in and strive for as a team. Our web component tests helped reinforce this <a href="https://cloudfour.com/thinks/cloud-fours-core-values/#we-%e2%9d%a4%ef%b8%8f-the-web">core value</a>.</p>



<h3 class="wp-block-heading">Every web component had an accessibility violation test assertion</h3>



<p>As a baseline standard practice, we always included a <code>vitest-axe</code> <code>toHaveNoViolations()</code> test assertion (including checking multiple UI states as needed):</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript shcb-code-table"><span class="shcb-loc"><span>it(<span class="hljs-string">'Has no accessibility violations'</span>, <span class="hljs-keyword">async</span> () =&gt; {
</span></span><span class="shcb-loc"><span>  render(formValidatorExample(), <span class="hljs-built_in">document</span>.body);
</span></span><span class="shcb-loc"><span>  
</span></span><span class="shcb-loc"><span>  <span class="hljs-keyword">const</span> component = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'form-validator'</span>) <span class="hljs-keyword">as</span> HTMLElement;  
</span></span><span class="shcb-loc"><span>  <span class="hljs-keyword">const</span> submitBtn = screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-string">'Submit'</span> });
</span></span><span class="shcb-loc"><span>
</span></span><span class="shcb-loc"><span>  <span class="hljs-comment">// Initial form state  </span>
</span></span><mark class="shcb-loc"><span>  expect(<span class="hljs-keyword">await</span> axe(component)).toHaveNoViolations();  
</span></mark><span class="shcb-loc"><span>
</span></span><span class="shcb-loc"><span>  <span class="hljs-comment">// Invalid form state</span>
</span></span><span class="shcb-loc"><span>  <span class="hljs-keyword">await</span> user.click(submitBtn);
</span></span><mark class="shcb-loc"><span>  expect(<span class="hljs-keyword">await</span> axe(component)).toHaveNoViolations();
</span></mark><span class="shcb-loc"><span>});
</span></span></code></span></pre>


<h3 class="wp-block-heading">Testing Library <code>ByRole</code> queries as the default query</h3>



<p>With all our tests, we’d default to querying the DOM using <a href="https://testing-library.com/docs/queries/byrole/">Testing Library’s <code>ByRole</code> queries</a> with accessible names. If a query fails, the control is not accessible (either an incorrect role or an incorrect/missing <a href="https://developer.mozilla.org/en-US/docs/Glossary/Accessible_name">accessible name</a>):</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript"><span class="hljs-keyword">const</span> submitBtn = screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-string">'Submit'</span> });</code></span></pre>


<p class="has-gray-lighter-background-color has-background"><strong>Remember:</strong> The “<a href="https://www.w3.org/TR/using-aria/#rule1">first rule of ARIA</a>” is to prefer native, semantic HTML elements and only introduce <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles"><em>ARIA</em> roles</a> as needed. In both cases, <code>ByRole</code> queries help confirm the proper role.</p>



<h3 class="wp-block-heading">Assertions for ARIA attributes where applicable</h3>



<p>In some cases, we’d assert certain ARIA attribute values where it made sense, for example:</p>


<pre class="wp-block-code"><span><code class="hljs language-bash">expect(input).toHaveAttribute(<span class="hljs-string">'aria-invalid'</span>, <span class="hljs-string">'false'</span>);</code></span></pre>


<h3 class="wp-block-heading">Testing focus management</h3>



<p>Keyboard users rely on proper focus management. For example, forms should move focus to the first invalid field on validation:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript shcb-code-table"><span class="shcb-loc"><span>it(<span class="hljs-string">'Validates an empty form on submit'</span>, <span class="hljs-keyword">async</span> () =&gt; {
</span></span><span class="shcb-loc"><span>  <span class="hljs-comment">// … setup</span>
</span></span><span class="shcb-loc"><span>    
</span></span><span class="shcb-loc"><span>  <span class="hljs-comment">// Submit empty form</span>
</span></span><span class="shcb-loc"><span>  <span class="hljs-keyword">await</span> user.click(submitBtn);
</span></span><span class="shcb-loc"><span>    
</span></span><span class="shcb-loc"><span>  <span class="hljs-comment">// Focus moves to first invalid field</span>
</span></span><mark class="shcb-loc"><span>  expect(emailInput).toHaveFocus();
</span></mark><span class="shcb-loc"><span>});
</span></span></code></span></pre>


<p>Other use cases where focus management assertions are important include testing that the focus returns to the appropriate elements after dialogs close or actions complete.</p>



<p class="has-gray-lighter-background-color has-background"><strong>Tip:</strong> As part of our development process, we use our keyboards to navigate through the UI. Did the <kbd>Tab</kbd> jump to the control we expected? Did we expect the <kbd>Escape</kbd> key to close a dialog? After submitting an invalid form, is the focus on the first invalid input? Not using a mouse and manually testing these scenarios with a keyboard helps guide the assertions we include in the tests.</p>



<h2 class="wp-block-heading">Test file organization</h2>



<p>As our test suite grew, we started organizing test files by <strong>feature</strong> or <strong>concern</strong>. This helped avoid monolithic <code>Component.test.ts</code> files with <em>hundreds</em> of tests. Here are the common test file categories we saw occur organically:</p>



<h3 class="wp-block-heading">Interaction tests: <code>ComponentName.interactions.test.ts</code></h3>



<p>Tests included assertions for user interactions, click handlers, keyboard navigation, and UI state changes in response to user actions.</p>



<h3 class="wp-block-heading">Event tests: <code>ComponentName.events.test.ts</code></h3>



<p>Tests included assertions for custom event emissions, event bubbling, and event payloads.</p>



<h3 class="wp-block-heading">Rendering tests: <code>ComponentName.rendering.test.ts</code></h3>



<p>Tests included assertions for initial render, conditional rendering, and DOM structure. </p>



<h3 class="wp-block-heading">Feature-specific tests: <code>ComponentName.feature.test.ts</code></h3>



<p>For specific features, we named test files after the feature:</p>



<ul class="wp-block-list">
<li><code>ComponentName.hidden-inputs.test.ts</code></li>



<li><code>ComponentName.sorting.test.ts</code></li>



<li><code>ComponentName.validation.test.ts</code></li>
</ul>



<h2 class="wp-block-heading">Directory structure patterns</h2>



<p>We preferred to collocate the test files next to the component or feature. This kept the test suite maintainable, discoverable, and faster to run. We found it easier to find and run tests for specific features.</p>



<h3 class="wp-block-heading">Pattern 1: Tests alongside component</h3>



<p>For simpler components:</p>


<pre class="wp-block-code"><span><code class="hljs shcb-code-table"><span class="shcb-loc"><span>ComponentName/
</span></span><span class="shcb-loc"><span>├── _component-name.css
</span></span><span class="shcb-loc"><span>├── ComponentName.ts
</span></span><span class="shcb-loc"><span>├── ComponentName.stories.ts
</span></span><mark class="shcb-loc"><span>├── ComponentName.interactions.test.ts
</span></mark><mark class="shcb-loc"><span>├── ComponentName.rendering.test.ts
</span></mark><mark class="shcb-loc"><span>└── ComponentName.events.test.ts
</span></mark><span class="shcb-loc"><span>
</span></span></code></span></pre>


<h3 class="wp-block-heading">Pattern 2: Feature sub-directories with tests</h3>



<p>For complex features that warrant their own folder:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript shcb-code-table"><span class="shcb-loc"><span>ComponentName/
</span></span><span class="shcb-loc"><span>├── _component-name.css
</span></span><span class="shcb-loc"><span>├── ComponentName.ts
</span></span><span class="shcb-loc"><span>├── ComponentName.stories.ts
</span></span><span class="shcb-loc"><span>├── validation/
</span></span><span class="shcb-loc"><span>│   ├── use-validation.ts
</span></span><mark class="shcb-loc"><span>│   ├── ComponentName.validation.test.ts
</span></mark><mark class="shcb-loc"><span>│   ├── ComponentName.validation.initial-render.test.ts
</span></mark><mark class="shcb-loc"><span>│   └── ComponentName.validation.attribute-changes.test.ts
</span></mark><span class="shcb-loc"><span>├── single-select/
</span></span><span class="shcb-loc"><span>│   ├── component-name-single-select-example.ts <span class="hljs-comment">// The `html` template for tests</span>
</span></span><mark class="shcb-loc"><span>│   └── ComponentName.single-select.test.ts
</span></mark><span class="shcb-loc"><span>├── multi-select/
</span></span><span class="shcb-loc"><span>│   ├── component-name-multi-select-example.ts <span class="hljs-comment">// The `html` template for tests</span>
</span></span><mark class="shcb-loc"><span>│   └── ComponentName.multi-select.test.ts
</span></mark><span class="shcb-loc"><span>└── pre-select/
</span></span><span class="shcb-loc"><span>    ├── use-pre-select.ts
</span></span><mark class="shcb-loc"><span>    ├── use-pre-select.test.ts
</span></mark><mark class="shcb-loc"><span>    └── use-pre-select.object-support.test.ts
</span></mark><span class="shcb-loc"><span>
</span></span></code></span></pre>


<h3 class="wp-block-heading">Pattern 3: Helper/utility tests</h3>



<p>Tests&nbsp;for&nbsp;pure&nbsp;functions&nbsp;and utilities:</p>


<pre class="wp-block-code"><span><code class="hljs language-accesslog shcb-code-table"><span class="shcb-loc"><span>MyTreeComponent/
</span></span><span class="shcb-loc"><span>├── _my-tree-component.css
</span></span><span class="shcb-loc"><span>├── MyTreeComponent.ts
</span></span><span class="shcb-loc"><span>├── MyTreeComponent.interactions.test.ts
</span></span><span class="shcb-loc"><span>├── MyTreeComponent.events.test.ts
</span></span><span class="shcb-loc"><span>└── helpers/
</span></span><span class="shcb-loc"><span>    ├── flatten-tree.ts
</span></span><mark class="shcb-loc"><span>    ├── flatten-tree.test.ts
</span></mark><span class="shcb-loc"><span>    ├── generate-node-id.ts
</span></span><mark class="shcb-loc"><span>    ├── generate-node-id.test.ts
</span></mark><span class="shcb-loc"><span>    ├── set-tree-ids.ts
</span></span><mark class="shcb-loc"><span>    └── set-tree-ids.test.ts
</span></mark><span class="shcb-loc"><span>
</span></span></code></span></pre>


<h2 class="wp-block-heading">Using a <code>for</code>/<code>of</code> loop to run repetitive test assertions</h2>



<p>This pattern isn’t groundbreaking, but there were times when we used an array of input names and a <code>for</code>/<code>of</code> loop to run the same assertions against multiple inputs.</p>



<p>For example, without a loop:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript"><span class="hljs-keyword">const</span> dataTable = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'data-table'</span>) <span class="hljs-keyword">as</span> DataTable;

expect(within(dataTable).getAllByRole(<span class="hljs-string">'checkbox'</span>)).toHaveLength(<span class="hljs-number">6</span>);

<span class="hljs-keyword">const</span> selectAllCheckbox = within(dataTable).getByRole(
	<span class="hljs-string">'checkbox'</span>, 
	{ <span class="hljs-attr">name</span>: <span class="hljs-string">'Select all'</span> }
);
expect(selectAllCheckbox).toBeVisible();
expect(selectAllCheckbox).toBeChecked();

<span class="hljs-keyword">const</span> entity01Checkbox = within(dataTable).getByRole(
	<span class="hljs-string">'checkbox'</span>, 
	{ <span class="hljs-attr">name</span>: <span class="hljs-string">'Select Entity_01'</span> }
);
expect(entity01Checkbox).toBeVisible();
expect(entity01Checkbox).toBeChecked();

<span class="hljs-keyword">const</span> entity02Checkbox = within(dataTable).getByRole(
	<span class="hljs-string">'checkbox'</span>, 
	{ <span class="hljs-attr">name</span>: <span class="hljs-string">'Select Entity_02'</span> }
);
expect(entity02Checkbox).toBeVisible();
expect(entity02Checkbox).toBeChecked();

<span class="hljs-keyword">const</span> entity03Checkbox = within(dataTable).getByRole(
	<span class="hljs-string">'checkbox'</span>, 
	{ <span class="hljs-attr">name</span>: <span class="hljs-string">'Select Entity_03'</span> }
);
expect(entity03Checkbox).toBeVisible();
expect(entity03Checkbox).toBeChecked();

<span class="hljs-keyword">const</span> entity04Checkbox = within(dataTable).getByRole(
	<span class="hljs-string">'checkbox'</span>, 
	{ <span class="hljs-attr">name</span>: <span class="hljs-string">'Select Entity_04'</span> }
);
expect(entity04Checkbox).toBeVisible();
expect(entity04Checkbox).toBeChecked();

<span class="hljs-keyword">const</span> entity05Checkbox = within(dataTable).getByRole(
	<span class="hljs-string">'checkbox'</span>, 
	{ <span class="hljs-attr">name</span>: <span class="hljs-string">'Select Entity_05'</span> }
);
expect(entity05Checkbox).toBeVisible();
expect(entity05Checkbox).toBeChecked();

</code></span></pre>


<p>Using a <code>for</code>/<code>of</code> loop:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript"><span class="hljs-keyword">const</span> checkboxNames = [
	<span class="hljs-string">'Select all'</span>,
	<span class="hljs-string">'Select Entity_01'</span>,
	<span class="hljs-string">'Select Entity_02'</span>,
	<span class="hljs-string">'Select Entity_03'</span>,
	<span class="hljs-string">'Select Entity_04'</span>,
	<span class="hljs-string">'Select Entity_05'</span>,
];
<span class="hljs-keyword">const</span> dataTable = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'data-table'</span>) <span class="hljs-keyword">as</span> DataTable;

expect(within(dataTable).getAllByRole(<span class="hljs-string">'checkbox'</span>)).toHaveLength(
	checkboxNames.length,
);
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> name <span class="hljs-keyword">of</span> checkboxNames) {
	<span class="hljs-keyword">const</span> checkbox = within(dataTable).getByRole(<span class="hljs-string">'checkbox'</span>, { name });
	expect(checkbox).toBeVisible();
	expect(checkbox).toBeChecked();
}
</code></span></pre>


<p>Using the <code>for</code>/<code>of</code> loop<em> felt</em> cleaner and was easier to maintain.</p>



<h2 class="wp-block-heading">Thinking critically about clicking various controls at once</h2>



<p>This is less a pattern and more a reminder for our future selves to not just accept all ESLint rules without critical thinking.</p>



<p>Initially, we did the following:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript"><span class="hljs-comment">// Expand each of the root nodes.</span>
<span class="hljs-keyword">const</span> tools = screen.getAllByRole(<span class="hljs-string">'group'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/^tool\\\\./i</span> });
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> tool <span class="hljs-keyword">of</span> tools) {
	<span class="hljs-keyword">await</span> user.click(tool);
}

<span class="hljs-comment">// Expand each of the second-level nodes.</span>
<span class="hljs-keyword">const</span> services = screen.getAllByRole(<span class="hljs-string">'group'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/^service\\\\./i</span> });
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> service <span class="hljs-keyword">of</span> services) {
	<span class="hljs-keyword">await</span> user.click(service);
}
</code></span></pre>


<p>However, <a href="https://eslint.org/docs/latest/rules/no-await-in-loop">the <code>no-await-in-loop</code> ESLint rule</a> flagged the <code>await</code> within the <code>for</code>/<code>of</code> loop. <em>Technically,</em> the rule is correct:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Performing an operation on each element of an iterable is a common task. However, performing an <code>await</code> as part of each operation may indicate that the program is not taking full advantage of the parallelization benefits of <code>async</code>/<code>await</code>.</p>



<p>Often, the code can be refactored to create all the promises at once, then get access to the results using <code>Promise.all()</code> (or one of the other <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#promise_concurrency">promise concurrency methods</a>). Otherwise, each successive operation will not start until the previous one has completed.</p>
</blockquote>



<p>The ESLint rule suggested the following:</p>


<pre class="wp-block-code"><span><code class="hljs language-typescript"><span class="hljs-comment">// Expand each of the root nodes.</span>
<span class="hljs-keyword">const</span> tools = screen.getAllByRole(<span class="hljs-string">'group'</span>, { name: <span class="hljs-regexp">/^tool\\\\./i</span> });
<span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(tools.map(<span class="hljs-function">(<span class="hljs-params"><span class="hljs-params">tool</span></span>) =&gt;</span> user.click(tool)));

<span class="hljs-comment">// Expand each of the second-level nodes.</span>
<span class="hljs-keyword">const</span> services = screen.getAllByRole(<span class="hljs-string">'group'</span>, { name: <span class="hljs-regexp">/^service\\\\./i</span> });
<span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(services.map(<span class="hljs-function">(<span class="hljs-params"><span class="hljs-params">service</span></span>) =&gt;</span> user.click(service)));
</code></span></pre>


<p>That <em>makes sense</em>. However, for our use case, we want each successive operation to wait until the previous one has completed. Imagine a user clicking various UI controls. The user will not be clicking <em>all of them at once</em>, it’d be impossible! Instead, the user would click the controls one by one. Our tests should match how a user will interact with our UI. Additionally, if the DOM updates after each click, a race condition may occur, potentially making the test flaky.</p>



<p>We ended up turning off the <code>no-await-in-loop</code> rule in each of those lines with a comment to help explain <em>why</em> we disabled the ESLint rule:</p>


<pre class="wp-block-code"><span><code class="hljs language-javascript"><span class="hljs-comment">// Expand each of the root nodes.</span>
<span class="hljs-keyword">const</span> tools = screen.getAllByRole(<span class="hljs-string">'group'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/^tool\\\\./i</span> });
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> tool <span class="hljs-keyword">of</span> tools) {
	<span class="hljs-comment">// We want to run the clicks sequentially to avoid UI race conditions.</span>
	<span class="hljs-comment">// Additionally, this closer aligns with how a real user would interact with the UI.</span>
	<span class="hljs-comment">// eslint-disable-next-line no-await-in-loop</span>
	<span class="hljs-keyword">await</span> user.click(tool);
}

<span class="hljs-comment">// Expand each of the second-level nodes.</span>
<span class="hljs-keyword">const</span> services = screen.getAllByRole(<span class="hljs-string">'group'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/^service\\\\./i</span> });
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> checkbox <span class="hljs-keyword">of</span> individualCheckboxes) {
	<span class="hljs-comment">// We want to run the clicks sequentially to avoid UI race conditions.</span>
	<span class="hljs-comment">// Additionally, this closer aligns with how a real user would interact with the UI.</span>
	<span class="hljs-comment">// eslint-disable-next-line no-await-in-loop</span>
	<span class="hljs-keyword">await</span> user.click(checkbox);
}
</code></span></pre>


<h2 class="wp-block-heading">Avoid leaking state between tests</h2>



<p>An important detail I want to highlight in particular: We need to reset the document body after each test run to avoid leaking state. In our case, we set this up globally in our <code>vitest-setup.ts</code> config file using the <a href="https://vitest.dev/api/#aftereach">Vitest <code>afterEach()</code> teardown function</a>. That way, we wouldn’t have to worry about manually adding this in each test or accidentally forgetting:</p>


<pre class="wp-block-code"><span><code class="hljs language-typescript"><span class="hljs-comment">// vitest-setup.ts</span>

<span class="hljs-comment">/**
 * We are using Lit's `render` and `html` functions to render in the tests.
 * We need to reset the document body after each test to avoid leaking state.
 */</span>
afterEach(<span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
  render(html`<span class="xml">`</span>, <span class="hljs-built_in">document</span>.body);
});</code></span></pre>


<h2 class="wp-block-heading">Adding basic dialog functionality to jsdom</h2>



<p>Our tests used <code>jsdom</code>. There is an <a href="https://github.com/jsdom/jsdom/issues/3294">open <code>jsdom</code> <code>HTMLDialogElement</code> issue</a> that remains unresolved. We ended up mocking the <code>HTMLDialogElement</code> <code>show()</code>, <code>showModal()</code>, and <code>close()</code> methods in the <code>vitest-setup.ts</code> file as follows:</p>


<pre class="wp-block-code"><span><code class="hljs language-typescript"><span class="hljs-comment">// vitest-setup.ts</span>

<span class="hljs-comment">// Add basic dialog functionality to jsdom</span>
<span class="hljs-comment">// @see https://github.com/jsdom/jsdom/issues/3294</span>
HTMLDialogElement.prototype.show = vi.fn(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mock</span>(<span class="hljs-params">
  <span class="hljs-keyword">this</span>: HTMLDialogElement,
</span>) </span>{
  <span class="hljs-keyword">this</span>.open = <span class="hljs-literal">true</span>;
});
HTMLDialogElement.prototype.showModal = vi.fn(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mock</span>(<span class="hljs-params">
  <span class="hljs-keyword">this</span>: HTMLDialogElement,
</span>) </span>{
  <span class="hljs-keyword">this</span>.open = <span class="hljs-literal">true</span>;
});
HTMLDialogElement.prototype.close = vi.fn(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mock</span>(<span class="hljs-params">
  <span class="hljs-keyword">this</span>: HTMLDialogElement,
</span>) </span>{
  <span class="hljs-keyword">this</span>.open = <span class="hljs-literal">false</span>;
});</code></span></pre>


<p>This <a href="https://github.com/jsdom/jsdom/issues/3294#issuecomment-1268330372">workaround</a> allowed us to keep moving forward with any tests that included HTML dialog element assertions.</p>



<h2 class="wp-block-heading">Wrapping up</h2>



<p>At the beginning of the project, I was feeling a bit nervous because I wasn’t sure how easy it would be to test HTML web components. Choosing to build Light DOM web components was <em>absolutely</em> the right choice, and once we got rolling, it made testing HTML web components no different from testing framework-specific components. I’m elated to have gone through this experience, and I must say, I love me some <a href="https://cloudfour.com/topics/web-components/">HTML web components</a>. ❤️</p>



<h2 class="wp-block-heading">More resources</h2>



<ul class="wp-block-list">
<li>Testing Library has <a href="https://testing-library.com/docs/dom-testing-library/api-accessibility">accessibility-focused helper functions</a> that can help debug tests</li>



<li>Testing Library’s <a href="https://testing-library.com/docs/queries/about#priority">queries order of priority list</a> is a handy reference for building more accessible user interfaces</li>



<li><a href="https://kentcdodds.com/blog/common-mistakes-with-react-testing-library">Common mistakes with React Testing Library</a> by Kent C. Dodds (Even though it mentions React, all of the guidelines still apply for non-React projects)</li>



<li><a href="https://frontendmasters.com/blog/light-dom-only/">Light-DOM-Only Web Components are Sweet</a> by Chris Coyier</li>



<li><a href="https://www.youtube.com/watch?v=uIuf5LlA6KQ">Come to the light side: HTML Web Components</a> by Chris Ferdinandi (11ty Conf 2024 video)</li>



<li><a href="https://adactio.com/journal/20618">HTML Web Components</a> by Jeremy Keith</li>
</ul>



<p></p>

<hr>
<h2>We’re Cloud Four</h2>
<p>We solve complex responsive web design and development challenges for ecommerce, healthcare, fashion, B2B, SaaS, and nonprofit organizations.</p>

<p><a href="https://cloudfour.com/made/"><b>See our work</b></a></p>]]></description>
      <pubDate>Tue, 18 Nov 2025 16:30:00 +0000</pubDate>
      <link>https://cloudfour.com/thinks/testing-html-light-dom-web-components-easier-than-expected/</link>
      <dc:creator>Sharing what we learn about the responsive web – Cloud Four</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5020031108</guid>
    </item>
    <item>
      <title><![CDATA[Talking CSS, Web Components, App Design and (gulp) AI on ShopTalk Show]]></title>
      <description><![CDATA[
<p>I had a blast chatting with Chris and Dave on <a href="https://shoptalkshow.com/689/">episode 689 of ShopTalk Show</a>, my personal favorite podcast about building websites:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>In this episode we sit down with Tyler Sticka to discuss upgrading his project, Colorpeek. We explore the practical applications of web components and CSS, and how they are shaping the future of web development. Tyler shares his experiences with prototyping and the challenges of maintaining simplicity in design.</p>
</blockquote>



<p>You can listen right now <a href="https://plnk.to/shoptalk?to=page">wherever you subscribe to podcasts</a>, or in video form <a href="https://www.youtube.com/watch?v=6DK2k9qMj8o">on YouTube</a>:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="Tyler Sticka on Colorpeek 2 and Awesome CSS (ep689)" width="500" height="281" src="https://www.youtube.com/embed/6DK2k9qMj8o?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
</div></figure>



<p></p>

<hr>
<h2>We’re Cloud Four</h2>
<p>We solve complex responsive web design and development challenges for ecommerce, healthcare, fashion, B2B, SaaS, and nonprofit organizations.</p>

<p><a href="https://cloudfour.com/made/"><b>See our work</b></a></p>]]></description>
      <pubDate>Mon, 03 Nov 2025 18:25:04 +0000</pubDate>
      <link>https://cloudfour.com/thinks/talking-css-web-components-app-design-and-gulp-ai-on-shoptalk-show/</link>
      <dc:creator>Sharing what we learn about the responsive web – Cloud Four</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5002919835</guid>
    </item>
    <item>
      <title><![CDATA[Invisible success]]></title>
      <description><![CDATA[  <p>Eric Bailey has some great thoughts about why it’s <a href="https://ericwbailey.website/published/invisible-success/">tough to show success on a design systems team</a>:</p>
<blockquote>
<p>In a business context, design system work means numbers go down. Less bug reports, faster design iteration, shorter development cycles, fewer visual inconsistencies, smaller staffing requirements that enable folks to work on more interesting challenges, etc. All good things.</p>
<p>Unfortunately, contemporary business practices only reward numbers going up. There isn’t much infrastructure in place to quantify the constant, silent, boring, predictable, round-the-clock passive successes of this aspect of design systems after the initial effort is complete.</p>
</blockquote>
<p>This is an interesting discussion because it’s hard to quantify what’s good work when it comes to design systems. For example, when I was on a team like that I would struggle to make the case for refactoring tons of janky and confusing <span class="small-caps">CSS</span>.</p>
<p>Ultimately I felt like I was good at the systems part—I could tell when there were too many similar components or when we should refactor something—but I was really, really bad at the storytelling part of design systems. And that kind of work is really more story than systems: explaining to folks up the chain why this seemingly insignificant series of tasks is an investment in the future. Or why this one tiny detail is worthy of our care today so we don’t have to worry about it later.</p>  ]]></description>
      <pubDate>Fri, 19 Apr 2024 18:13:15 +0000</pubDate>
      <link>https://www.csscade.com/invisible-success</link>
      <dc:creator>The Cascade</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4430363646</guid>
    </item>
    <item>
      <title><![CDATA[VS Code – highlight just the active indent guide]]></title>
      <description><![CDATA[Long term readers may wonder – VS Code, Ben? Really? Let’s not getting into that now, instead, just the issue before us. The problem before us is that we want to highlight just the active indent guide of the code in the editor. By default, VS Code highlights both the active indent level, and also, […]]]></description>
      <pubDate>Thu, 08 Jan 2026 22:39:57 +0000</pubDate>
      <link>https://benfrain.com/vs-code-highlight-just-the-active-indent-guide/</link>
      <dc:creator>Ben Frain</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5076633239</guid>
    </item>
    <item>
      <title><![CDATA[Review: MoErgo Go60, a split ergonomic and fully programmable keyboard]]></title>
      <description><![CDATA[I’m going to call this from the start. The Go60 is the best travel-friendly split keyboard, perhaps even the best all-around split keyboard, you can buy right now. I’m no MoErgo fanboy – in my review of their Glove80 I was pretty clear that there were areas I felt the Glove80 fell short. I feel […]]]></description>
      <pubDate>Sat, 06 Dec 2025 18:35:20 +0000</pubDate>
      <link>https://benfrain.com/review-moergo-go60-a-split-ergonomic-and-fully-programmable-keyboard/</link>
      <dc:creator>Ben Frain</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5039791549</guid>
    </item>
    <item>
      <title><![CDATA[How our dog increased my appreciation for accessibility]]></title>
      <description><![CDATA[
<figure class="wp-block-image size-large"><img width="1024" height="576" src="https://cloudfour.com/wp-content/uploads/2025/08/coco-feature-r2-1024x576.jpg" alt="An illustrated brown and white spotted dog with its tongue hanging out and a goofy look on its face wags its tail. A thought bubble says, &quot;a11y?&quot;" class="wp-image-8382" srcset="https://cloudfour.com/wp-content/uploads/2025/08/coco-feature-r2-1024x576.jpg 1024w, https://cloudfour.com/wp-content/uploads/2025/08/coco-feature-r2-300x169.jpg 300w, https://cloudfour.com/wp-content/uploads/2025/08/coco-feature-r2-768x432.jpg 768w, https://cloudfour.com/wp-content/uploads/2025/08/coco-feature-r2-1536x864.jpg 1536w, https://cloudfour.com/wp-content/uploads/2025/08/coco-feature-r2.jpg 1600w" sizes="(max-width: 1024px) 100vw, 1024px"></figure>



<p>“<strong>Ouch!</strong> WTF was that Coco?”</p>



<p>It was Easter morning. I was bent over wiping Sophie’s feet. Sophie is my mom’s one-year-old cocker spaniel. We were dog sitting, and Coco, our husky pitbull mutt, was thrilled. In her excitement, Coco whipped around and headbutted me. </p>



<p>I yelled loud enough that our oldest came running to see what happened. I told them I was fine. I’d have a lump on my head or a black eye, but I was okay. </p>



<p>Or I so thought until the world started spinning. I sat down on the couch in hopes things would stabilize. I tried to look at my phone to figure out if I could take Tylenol or Ibuprofen. The screen was blurry and made me feel sick.</p>



<p>At urgent care later, the doctor confirmed what we already suspected. Coco had given me a concussion.</p>



<h2 class="wp-block-heading">A dark room with no screens</h2>



<p>The doctor’s prescription was simple. Give your brain a rest from all stimulus for three to four days. Assuming your concussion isn’t dangerous—and mine wasn’t—that means stay in a dark room with no screens. </p>



<p>While I lay in bed with blackout curtains closed and a mask over my eyes, I learned to appreciate some features of our HomePod Mini that I had never used before:</p>



<ul class="wp-block-list">
<li>If I asked Siri to text someone, the HomePod would ping when they replied and I could ask to have the message read aloud.</li>



<li>Siri could call someone through the HomePod speaker.</li>
</ul>



<p>These aren’t earth-shattering features, but they helped me feel connected when I couldn’t use screens or ear buds.</p>



<h2 class="wp-block-heading">My new favorite accessibility features</h2>



<p>When I felt well enough to try working again, I found it necessary to make several changes to my computer.</p>



<h3 class="wp-block-heading">Reduce motion is your friend</h3>



<p>Animation and motion would cause pain in my temple. This persisted for weeks after the concussion. I once had to leave a local meetup early because the presenter used  animated gifs in their presentation, and it triggered concussion symptoms.</p>



<figure class="wp-block-image size-large"><img width="1024" height="798" src="https://cloudfour.com/wp-content/uploads/2025/08/reduce-motion-1024x798.png" alt="" class="wp-image-8380" srcset="https://cloudfour.com/wp-content/uploads/2025/08/reduce-motion-1024x798.png 1024w, https://cloudfour.com/wp-content/uploads/2025/08/reduce-motion-300x234.png 300w, https://cloudfour.com/wp-content/uploads/2025/08/reduce-motion-768x598.png 768w, https://cloudfour.com/wp-content/uploads/2025/08/reduce-motion.png 1430w" sizes="(max-width: 1024px) 100vw, 1024px"><figcaption class="wp-element-caption">The reduce motion option in the macOS accessibility settings. </figcaption></figure>



<p>I have since returned to normal motion on my iPhone, but I still have reduced motion set on my desktop machine. Too much movement on a larger screen can still be  overwhelming. I’ve also turned off auto-play of animated images wherever possible.</p>



<h3 class="wp-block-heading">Dark mode isn’t only a fad</h3>



<p>I’ll admit that I’ve been pretty dismissive of dark mode. I thought it was something developers came up with because they like working in the dark.</p>



<p>But even though dark mode isn’t included in accessibility settings, I now think of it as one. Bright lights were too much for me and dark mode helped. It has moved up my priority list for the next version of our own site.</p>



<p>As an aside, Gmail has a dark theme, but it doesn’t turn on automatically when someone has <code>prefers-color-scheme</code> set to <code>dark</code>. That seems silly to me. The hard part is developing a dark version of your app or site. Once you have one, don’t make people search around for it.</p>



<figure class="wp-block-image size-large"><img width="1024" height="518" src="https://cloudfour.com/wp-content/uploads/2025/08/gmail-dark-theme-1024x518.jpg" alt="" class="wp-image-8381" srcset="https://cloudfour.com/wp-content/uploads/2025/08/gmail-dark-theme-1024x518.jpg 1024w, https://cloudfour.com/wp-content/uploads/2025/08/gmail-dark-theme-300x152.jpg 300w, https://cloudfour.com/wp-content/uploads/2025/08/gmail-dark-theme-768x389.jpg 768w, https://cloudfour.com/wp-content/uploads/2025/08/gmail-dark-theme-1536x778.jpg 1536w, https://cloudfour.com/wp-content/uploads/2025/08/gmail-dark-theme.jpg 1600w" sizes="(max-width: 1024px) 100vw, 1024px"><figcaption class="wp-element-caption">Gmail’s dark theme can be found by clicking on the gear icon to open settings. Then select the Themes tab and the Set Theme button. You’ll need to scroll down to find the Dark theme. Hover (sigh) over the blocks to see the theme name.</figcaption></figure>



<h3 class="wp-block-heading">You can use Night Shift mode during the day</h3>



<p>Apple’s <a href="https://support.apple.com/en-us/102191">Night Shift mode</a> shifts the color of a display to be warmer in the evening because “studies have shown that exposure to bright blue light in the evening can affect your circadian rhythms and make it harder to fall asleep.”</p>



<p>The slightly warmer colors in Night Mode were easier for my brain to process after the concussion. I left it on all the time.</p>



<h2 class="wp-block-heading">Accessibility is for everyone</h2>



<p>My concussion gave me a greater appreciation for the accessibility features that operating systems and browsers have built. If you worked on those features, I want to thank you for everything you do. If you built your website to honor <code>prefers-reduced-motion</code> and dark mode, I want you to know that your work made a difference to me.</p>



<p>It’s a misnomer to think that accessibility is only for people with permanent conditions. I like the way Maria Town, president of the American Association of People with Disabilities, talked about this reality in an <a href="https://www.advocate.com/politics/2020/7/13/disabled-advocate-everyone-will-become-disabled-if-theyre-lucky">interview with Advocate magazine</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Everyone will become disabled if they’re lucky enough. Aging is a privilege. Far too few of us get the opportunity to live to be a ripe old age. And if you do get the opportunity, you will likely become disabled.</p>
</blockquote>



<p>This is our reality. Whether it is a temporary injury, a permanent condition, old age, or a literal boneheaded dog giving you a concussion, you will need accessibility features at some point in your life. </p>



<p>So let’s recommit to supporting accessibility in our own work not only to support those who need it now, but also because it is in our own self-interest. You never know when you may suddenly find yourself needing accessibility features.</p>



<p>P.S. Coco was fine. She has a hard head.</p>

<hr>
<h2>We’re Cloud Four</h2>
<p>We solve complex responsive web design and development challenges for ecommerce, healthcare, fashion, B2B, SaaS, and nonprofit organizations.</p>

<p><a href="https://cloudfour.com/made/"><b>See our work</b></a></p>]]></description>
      <pubDate>Mon, 11 Aug 2025 15:17:44 +0000</pubDate>
      <link>https://cloudfour.com/thinks/how-our-dog-increased-my-appreciation-for-accessibility/</link>
      <dc:creator>Sharing what we learn about the responsive web – Cloud Four</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4908835815</guid>
    </item>
    <item>
      <title><![CDATA[Common Threads]]></title>
      <description><![CDATA[Visualizing how musicals use motifs to tell stories.]]></description>
      <pubDate>Tue, 09 Dec 2025 08:00:00 +0000</pubDate>
      <link>https://pudding.cool/2025/12/motifs</link>
      <dc:creator>The Pudding</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5043054137</guid>
    </item>
    <item>
      <title><![CDATA[]]></title>
      <description><![CDATA[
      <p>Favourite tree doing favourite tree things.</p><figure><img src="https://paulrobertlloyd.com/media/2025/267/p1/1.jpg" alt="Luminous bright orange leaves on the branches of a maple tree." class="u-photo"></figure>
    ]]></description>
      <pubDate>Wed, 24 Sep 2025 19:37:23 +0000</pubDate>
      <link>https://paulrobertlloyd.com/2025/267/p1/</link>
      <dc:creator>Paul Robert Lloyd</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4956720841</guid>
    </item>
    <item>
      <title><![CDATA[Uncrate's 100-ish favorite things on Amazon]]></title>
      <description><![CDATA[<div style="float: left; margin: 0 15px 15px 0; padding: 0; border: 1px solid #000000;"><a href="https://uncrate.com/uncrates-100-ish-favorite-things-on-amazon/" rel="bookmark">





    
    



		<img src="https://uncrate.com/assets_c/2025/12/amazon-uncrate-100-thumb-960xauto-187187.jpg" class="t webfeedsFeaturedVisual" alt="Uncrate's 100-ish favorite things on Amazon" title="Uncrate's 100-ish favorite things on Amazon" width="960">
	</a></div>Our hundred (give or take) picks available on Amazon. <br><br>Visit <a href="https://uncrate.com/uncrates-100-ish-favorite-things-on-amazon/">Uncrate</a> for the full post.]]></description>
      <pubDate>Mon, 01 Dec 2025 17:31:15 +0000</pubDate>
      <link>https://uncrate.com/uncrates-100-ish-favorite-things-on-amazon/</link>
      <dc:creator>Uncrate</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5034071573</guid>
    </item>
    <item>
      <title><![CDATA[Against the protection of stocking frames.]]></title>
      <description><![CDATA[
      <p>I think it’s long past time I start discussing “artificial intelligence” (“AI”) as a failed technology. Specifically, that large language models (LLMs) have repeatedly and consistently failed to demonstrate value to anyone other than their investors and shareholders. The technology is a failure, and I’d like to invite you to join me in treating it as such.</p>
<p>I’m not the first one to land here, of course; the likes of <a href="https://karendhao.com/">Karen Hao</a>, <a href="https://thecon.ai/">Alex Hanna, Emily Bender</a>, and more have been on this beat longer than I have. And just to be clear, describing “AI” as a failure doesn’t mean it doesn’t have useful, individual applications; it’s possible you’re already thinking of some that matter to you. But I think it’s important to see those as exceptions to the technology’s overwhelming bias toward failure. In fact, I think describing the technology <em>as a thing that has failed</em> can be helpful in elevating what does actually work about it. Heck, maybe it’ll even help us <a href="https://www.anildash.com/2025/05/02/what-would-good-ai-look-like/">build a better alternative to it</a>.<sup class="footnote-ref"><a class="footnote" href="https://ethanmarcotte.com/wrote/against-stocking-frames/#fn-dream" id="fnref-fn-dream" aria-labelledby="title-footnotes">1</a></sup></p>
<p>In other words, approaching “AI” as failure opens up some really useful lines of thinking and criticism. I want to spend more time with them.</p>
<hr>
<p>Right, so: why do I think it’s a failure? Well, there are a few reasons.</p>
<p>The first is that as a product class, “AI” <em>is</em> a failed technology. I don’t think it’s controversial to suggest that LLMs haven’t measured up to any of the <a href="https://www.microsoft.com/en-us/microsoft-365/business-insights-ideas/resources/improving-productivity-with-ai-tools">lofty</a> <a href="https://openai.com/index/planning-for-agi-and-beyond/">promises</a> made by their vendors. But in more concrete terms, <a href="https://www.prnewswire.com/news-releases/two-thirds-of-shoppers-say-no-to-ai-shopping-assistants--trust-issues-could-slow-retails-ai-revolution-302385556.html">consumers dislike “AI” when it shows up in products</a>, and it makes them <a href="https://business.yougov.com/content/49622-artificial-intelligence-ai-generated-brand-advertising-how-do-consumers-around-the-world-feel-comfortable">actively mistrust the brands that employ it</a>. In other words, we’re some three years into the <a href="https://en.wikipedia.org/wiki/Gartner_hype_cycle">hype cycle</a>, and LLMs haven’t met any markers of success we’d apply to, well, literally any other technology.</p>
<p>This failure can’t be separated from the staggering social, cultural, and ecological costs associated with simply <em>using</em> these services: the <a href="https://www.bbc.com/news/articles/cy8gy7lv448o">environmental harms</a> baked into these platforms; <a href="https://variety.com/vip/content-owner-lawsuits-against-ai-companies-comprehensive-updated-index-1236101707/">the violent disregard for copyright</a> that brought them into being; the <a href="https://www.cbsnews.com/news/ai-chatbots-teens-suicide-parents-testify-congress/">real-world deaths they’ve potentially caused</a>; the <a href="https://uniglobalunion.org/news/ai-action-summit-africa-tech-workers/">workforce</a> of <a href="https://www.cmswire.com/digital-experience/he-helped-train-chatgpt-it-traumatized-him/">underpaid and traumatized</a> <a href="https://www.wired.com/story/hundreds-of-google-ai-workers-were-fired-amid-fight-over-working-conditions/">contractors</a> that are quite literally building these platforms; and many, many more. I mention these costs because this isn’t a case of a well-built technology failing to find its market. As a force for devastation and harm, “AI” is a wild success; but as a viable product it is, again, a failure.</p>
<p>And yet despite all of this, “AI” feels like it’s just, like, <em>everywhere.</em> Consumers may not like or even trust “AI” features, but that hasn’t stopped product companies from shipping them. Corporations are constantly launching new LLM initiatives, often <a href="https://newsroom.ibm.com/2025-05-06-ibm-study-ceos-double-down-on-ai-while-navigating-enterprise-hurdles">simply because of “the risk of falling behind”</a> their competitors. What’s more, according to a recent MIT report, <a href="https://fortune.com/2025/08/18/mit-report-95-percent-generative-ai-pilots-at-companies-failing-cfo/">very nearly all corporate “AI” pilots fail</a>.</p>
<p>I want to suggest that the ubiquity of LLMs is another sign of the technology’s failure. It is not succeeding on its own merits; rather, it’s being propped up by terrifying amounts of investment capital, not to mention a recent glut of <a href="https://www.gsa.gov/about-us/newsroom/news-releases/gsa-announces-new-partnership-with-openai-delivering-deep-discount-to-chatgpt-08062025">government</a> <a href="https://www.gov.uk/government/news/openai-to-expand-uk-office-and-work-with-government-departments-to-turbocharge-the-uks-ai-infrastructure-and-transform-public-services">contracts</a>.<sup class="footnote-ref"><a class="footnote" href="https://ethanmarcotte.com/wrote/against-stocking-frames/#fn-founders" id="fnref-fn-founders" aria-labelledby="title-footnotes">2</a></sup> Without that fiscal support, I very much doubt LLMs would even exist at the scale they currently do.</p>
<p>So. The technology doesn’t deliver consistent results, much less desirable ones; what’s more, it extracts terrible costs to <em>not</em> reliably produce anything of value. It is fundamentally a failure. And yet, private companies and public institutions alike keep adopting it. Why is that?</p>
<p>From where I sit, the most consistent application of LLMs at work has been through top-down corporate mandate: a company’s leadership will suggest, urge, or outright require employees to incorporate “AI” in their work. <a href="https://zapier.com/blog/zapier-ai-first-hiring-leaning/">Zapier’s post on its “AI-first” mandate</a> is one recent example. At some point, the company decided to mandate “AI” usage across their organization, joining such august brands as <a href="https://www.techmeme.com/250407/p26#a250407p26">Shopify</a>, <a href="https://www.theverge.com/news/657594/duolingo-ai-first-replace-contract-workers">Duolingo</a>, and <a href="https://bsky.app/profile/kyleconrad.bsky.social/post/3lxkg6tlekk2a">Taco Bell</a>. But in this post from the summer, Zapier’s global head of talent talks about how the company’s expanding the size and scope of that initial mandate. Here’s the intro:</p>
<blockquote>
<p>Recently, we shared our <a href="https://zapier.com/blog/how-zapier-rolled-out-ai/">AI adoption playbook</a>, which showed that 89% of the Zapier team is already using AI in their daily work. But to make AI transformation truly sustainable, we have to start at the beginning: how we hire and onboard people into Zapier to build this future with us.</p>
</blockquote>
<p>I’ve written before about <a href="https://ethanmarcotte.com/wrote/design-system-adoption/">the problems with “adoption” as a success metric</a>: that “usage of a thing” doesn’t communicate anything about the quality of that usage, or about the health of the system overall. But despite that, Zapier’s moved beyond mandated adoption, and has begun changing its hiring and onboarding practices<span class="w"> —</span> including how it evaluates employee performance. How does an “AI” mandate show up in a performance review? I’m so glad you asked:</p>
<blockquote>
<p>Starting immediately, all new Zapier hires are expected to meet a minimum standard for AI fluency. That doesn’t mean deep technical expertise in every case<span class="w"> —</span> but it does mean showing a mindset of curiosity toward AI, a demonstrated willingness to experiment with it, and an ability to think strategically about how AI can amplify their work.</p>
<p>[…]</p>
<p>We map skills across four levels, keeping in mind that AI skills vary and are heavily role-specific.</p>
<ul>
<li><strong>Unacceptable</strong>: Resistant to AI tools and skeptical of their value</li>
<li><strong>Capable</strong>: Using the most popular tools, with likely under three months of hands-on experience</li>
<li><strong>Adoptive</strong>: Embedding AI in personal workflows, tuning prompts, chaining models, and automating tasks to boost efficiency</li>
<li><strong>Transformative</strong>: Uses AI not just as a tool but to rethink strategy and deliver user-facing value that wasn’t possible a couple years ago</li>
</ul>
</blockquote>
<p>There’s an insidious thing nestled in here.</p>
<p><a href="https://piccalil.li/blog/are-peoples-bosses-really-making-them-use-ai/">Andy Bell</a> and <a href="https://www.bloodinthemachine.com/p/how-ai-is-killing-jobs-in-the-tech-f39">Brian Merchant</a> have both documented tech workers’ reactions to “AI” mandates: what it feels like to have parts of your job outsourced to automation, and how it changes what it feels like to show up for work. I’d recommend reading both posts in full; it’s possible you’ll see something of your own feelings mirrored in those testimonials. And those stories track with my own conversations with tech workers, who’ve shared how difficult it is to talk openly about their concerns at work. I’ve heard repeatedly about a kind of stifling social pressure: an implicit, unstated expectation that “AI” has to be seen as good and useful; pointing out limitations or raising questions feels difficult, if not dangerous.</p>
<p>But this Zapier post is the first example I’ve seen of a company making that implicit expectation into an explicit one. Here, the official policy is that <em>attitude toward a technology</em> should be used as a quantifiable measurement of how well a person aligns with the company’s goals: what the industry has historically (and euphemistically) referred to as <a href="https://www.bbc.com/worklife/article/20211015-what-does-being-a-cultural-fit-actually-mean">culture fit</a>. At this company, you could receive a negative performance review for being perceived as “resistant” or “skeptical” of LLMs. You’d be labeled as <code>unacceptable</code>.</p>
<p>I mean, look: on the face of it, that’s absurd. <em>That is absurd behavior.</em> Imagine screening prospective hires by asking their opinions about your company’s hosting provider, or evaluating employees for how they feel about Microsoft Teams. Just to be clear, I fully believe evaluations like these have happened in the industry<span class="w"> —</span> hiring and performance reviews are both riddled with bias, especially in tech. But this is the first time I’ve seen a company policy explicitly state that acceptance of “AI” is <em>a matter of cultural compliance</em>. That you’re either on board with “artificial intelligence,” or you’re not one of us.</p>
<p>This is where I think approaching “AI” as a failure becomes useful, even vital. It underscores that the technology’s real value isn’t improving productivity, or even in improving products. Rather, it’s a social mechanism employed to ensure compliance in the workplace, and to weaken worker power. Stories like the one at Zapier are becoming more common, where executive fiat is being used to force employees to use a technology that could <a href="https://ethanmarcotte.com/wrote/tooled/">deskill</a> them, and make them more replaceable. Arguably, this is the one use case where “artificial intelligence” has delivered some measure of consistent, provable results.</p>
<p>But here’s the thing: this is a success <em>only</em> if tech workers allow it to be. I’m convinced we can turn this into a failure, too. And we do that by getting organized.</p>
<p><span class="w"> —</span> okay, yes, I know. I am the person who thinks <a href="https://ethanmarcotte.com/books/you-deserve-a-tech-union/">you deserve a union</a>. But it’s not just me: from <a href="https://kotaku.com/diablo-4-blizzard-union-ai-layoffs-microsoft-2000621003">game studios</a> to <a href="https://newsguild.org/guild-members-are-winning-strong-protections-from-employer-pushed-ai/">newsrooms</a>, many workers are unionizing specifically because they want contractual protections from “artificial intelligence.” Heck, <a href="https://www.wbur.org/cognoscenti/2023/08/15/hollywood-writers-actors-strike-generative-artificial-intelligence-ethan-marcotte">the twin strikes in Hollywood</a> weren’t about banning “AI,” but giving workers control over how and when the technology was employed. I think at minimum, we deserve that level of control over our work.</p>
<p>With all that said, you don’t have to be unionized to start <em>organizing</em>: to have conversations with your coworkers, to share how you’re feeling about these changes at work, and start talking about what you’d like to do about those changes, together. It really is that simple.</p>
<p>That isn’t to say organizing is easy, mind. It involves having many, many conversations with your coworkers, and looking for shared concerns about issues in the workplace. And, look: I’m writing this post at a time where the labor market’s tight, when there’s so much pressure to not just adopt LLMs but to accept them unquestioningly. In that context, I realize that inviting coworkers to share some thoughts about automation can feel difficult, if not dangerous. But it’s only by organizing<span class="w"> —</span> by talking and listening to each other, and acting together in solidarity<span class="w"> —</span> do we have a chance at building a better, safer version of the tech industry.</p>
<p>“Artificial intelligence” is a failure. Let’s you and I make sure it stays that way.</p>
<hr>
    <div class="footnotes">
    <h2 class="subhed-section subhed-dashed" id="title-footnotes">Footnotes</h2>
    <ol><li id="fn-dream" class="footnote-item"><p>I’m profoundly skeptical this could ever happen under capitalism. But what the hell, I’m willing to hypothesize a bit. <a href="https://ethanmarcotte.com/wrote/against-stocking-frames/#fnref-fn-dream" class="reversefootnote">↩︎</a></p>
</li>
<li id="fn-founders" class="footnote-item"><p>To keep this too-long post a little less long, I’m not going to spend any time talking about the founders of these platforms, <a href="https://www.theverge.com/policy/772760/tech-ceos-ai-trump-white-house-dinner">their wholesale embrace</a> of the United States’ current far-right administration, and selling said administration tools to visit harm upon <a href="https://www.americanimmigrationcouncil.org/blog/ice-immigrationos-palantir-ai-track-immigrants/">marginalized communities</a> and <a href="https://www.motherjones.com/politics/2025/04/clearview-ai-immigration-ice-fbi-surveillance-facial-recognition-hoan-ton-that-hal-lambert-trump/">political opponents</a> alike. But please believe I <em>am</em> thinking about them. <a href="https://ethanmarcotte.com/wrote/against-stocking-frames/#fnref-fn-founders" class="reversefootnote">↩︎</a></p>
</li>
  </ol>
  </div>
      
      <hr>
      <p>This has been “<a href="https://ethanmarcotte.com/wrote/against-stocking-frames/">Against the protection of stocking frames.</a>” a post from <a href="https://ethanmarcotte.com/wrote/">Ethan’s journal.</a></p>
      <p><a href="mailto:listener+rss@ethanmarcotte.com?subject=Reply%20to:%20“Against the protection of stocking frames.”">Reply via email</a></p>
      
    ]]></description>
      <pubDate>Thu, 18 Sep 2025 04:00:00 +0000</pubDate>
      <link>https://ethanmarcotte.com/wrote/against-stocking-frames/</link>
      <dc:creator>My journal — Ethan Marcotte’s website</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4949870126</guid>
    </item>
    <item>
      <title><![CDATA[The line and the stream.]]></title>
      <description><![CDATA[
      <p>I come from one of the poorest corners of a small, rural state. When I go back to visit, I book a night or two in this big hotel in my neck of the woods. The trips are heartwarming, and this last one was no exception: I got a chance to share a couple meals with old friends and family, surrounded by the hills and forests I still think of as home. This year has felt long and dark, and their faces and voices brightened it.</p>
<p>This hotel, though. Let me tell you about it.</p>
<p>It’s new, first of all. Even if you’re driving through the area for the first time, you’ll be able to spot “new construction” as something that sticks out. What’s more, this huge hotel’s been dropped on top of a mountain that’s too tiny for it. This isn’t one of the hot tourist destinations, either; this mountain’s home to a humble little ski resort, one that’s always been overshadowed by more popular tourist destinations, all of them a short drive away. Now, I need to stress here that I’m no skier<span class="w"> —</span> I did a single season on my high school cross-country skiing team that we will <em>not</em> be discussing<span class="w"> —</span> but I do know this resort’s historically drawn most of its customers from nearby towns: they’re not pulling in the wealthy out-of-staters. And to look at this new, massive, shiny hotel, that’s who you’d think they’re attracting.</p>
<p>How’d it get here? Well, we need to back up a decade or two to answer that. Because as it happens, some investor types decided to defraud the federal government.</p>
<p>They didn’t lead with that, of course. Through a visa program for affluent foreign investors, they had access to a tremendous amount of capital, which they then used to acquire local tourist spots and dramatically overdevelop them. As they did, they promised that all this investment would bring an influx of new businesses, which would mean more people moving to a state starved for both residents and revenue.</p>
<p>As you might have guessed, the money disappeared when these investors got arrested. One visible aftershock was the halted construction projects. In one town up north, there’s a crater the size of a literal city block, because all the new construction these investors promised<span class="w"> —</span> a slew of shops and offices, a new hotel<span class="w"> —</span> disappeared when they did. The investors are gone; the hole isn’t.</p>
<p>It also left some massive, sprawling properties in communities that never had enough foot traffic to properly support them. And here we are, back at this gigantic hotel. When we walk in, we see that the gift shop right off the lobby has been hastily boarded up. One wing of the hotel’s been blocked off to guests. At the check-in desk, the clerk has three registration forms lined up in front of her, one for each of the guests she knows will be checking in that evening. All because some rich, powerful men promised my chronically impoverished corner of the state that the things it had always and desperately needed<span class="w"> —</span> investment, jobs, and people<span class="w"> —</span> were, at long, long last, finally about to arrive.</p>
<hr>
<p>“Artificial intelligence is here to stay. It’s not going anywhere.” I’ve heard some variation on that line many, many times <a href="https://en.wikipedia.org/wiki/ChatGPT">since 2022</a>; I heard it a few more times after <a href="https://ethanmarcotte.com/wrote/against-stocking-frames/">defining “artificial intelligence” as a failed technology</a>. Maybe you’ve heard it too.</p>
<p>Now, regardless of how you or I might feel about “AI,” I think we can both acknowledge just how impressively ahistorical that statement is. In the last two decades, I’ve seen so many things we <em>knew</em> were our future: Internet Explorer, Flash, Web 2.0, jQuery, mobile websites, NFTs<sup class="footnote-ref"><a class="footnote" href="https://ethanmarcotte.com/wrote/the-line-and-the-stream/#fn-nfts" id="fnref-fn-nfts" aria-labelledby="title-footnotes">1</a></sup>, and more. We were told each and every single one of them was immutable, fixed, and unchanging; each and every single one of them, in turn, faded. Some of them even disappeared altogether. I’ve been in this industry long enough to know that predicted futures have a pretty short lifespan.</p>
<p>And that’s the moment in which I’m writing: that vision of an “AI” future feels like it’s shaking. In fact, it might be close to shattering. There’s a growing body of reporting on <a href="https://www.ft.com/content/6cc87bd9-cb2f-4f82-99c5-c38748986a2e">the rampant speculation at the heart of the current boom</a>, with some journalists gaming out <a href="https://www.theatlantic.com/technology/2025/10/data-centers-ai-crash/684765/">what a market crash</a> might look like. <a href="https://au.investing.com/news/stock-market-news/peter-thiel-dumps-entire-nvidia-stake-slashes-tesla-holdings-amid-bubble-fears-4128704">Investors</a> are <a href="https://www.bbc.com/audio/play/p0m7h23s">nervous</a>; leaders of “AI” companies are even starting <a href="https://www.cnbc.com/2025/08/18/openai-sam-altman-warns-ai-market-is-in-a-bubble.html">to sound</a> <a href="https://www.bbc.com/news/articles/cwy7vrd8k4eo">warning bells</a>, presumably to position themselves for government bailouts whenever the bubble finally bursts. In the face of all this instability, stating that “AI isn’t going anywhere” feels even more detached from reality: it’s divorced from how quickly trends shift in tech, and it’s ignoring the growing cracks in the industry’s foundation.</p>
<p>At the same time, I think that unwavering belief is what’s instructive about it.</p>
<p>I mean, look. I’ve been thinking about <em>my</em> future quite a bit. The last decade’s made my sense of the coming decades feel fractured, and the last year has only accelerated that feeling. Friends are aging; family members have passed on; so many things I was taught to rely upon<span class="w"> —</span> jobs, industries, institutions, milestones, even <em>seasons</em><span class="w"> —</span> feel like they’re being upended in front of me. When you’re told to expect a certain broad arc to your life, it’s more than a little terrifying when that map’s redrawn as you’re looking at it.</p>
<p>That’s why I’ve come to realize that statements about the future aren’t predictions: they’re more like spells. When someone describes <code>something</code> to you as the future, they’re sharing a heartfelt belief that this <code>something</code> will be part of whatever comes next. “Artificial intelligence isn’t going anywhere” quite literally involves casting a technology forward into time. How could that be anything else <em>but</em> a kind of magic?</p>
<p>Now. By calling them magical, I’m not attempting to diminish or disparage these kinds of statements. Far from it: when we make these statements, we’re opening up a kind of possibility space. “What <em>if</em> the future looked like this?” By making space to ask questions, we’re making the future feel less fixed, and far less daunting. It gives us an anchor point in what comes next, one we can move toward with intention. On an individual level, incantations like these can be a helpful goal-setting exercise; performed at scale, they can be transformative.</p>
<p>And not necessarily in a good way. Take the “AI” industry: backed by their belief in their (again, <a href="https://ethanmarcotte.com/wrote/against-stocking-frames/">failed</a>) technology <em>and</em> a ghastly amount of capital, they’ve embedded their technology in products, organizations, and governments alike. They’ve begun physical buildouts of <a href="https://www.bloomberg.com/news/newsletters/2025-08-24/why-is-manhattan-being-crushed-by-this-giant-meta-data-center">vast data centers</a> that take <a href="https://westvirginiawatch.com/2025/09/02/enough-devastation-mingo-logan-residents-worry-about-proposed-power-plants-data-centers-in-wv/">a tremendous toll</a> on <a href="https://www.mpbonline.org/blogs/news/construction-on-metas-largest-data-center-brings-600-crash-spike-chaos-to-rural-louisiana/">disenfranchised</a> <a href="https://www.politico.com/news/2025/05/06/elon-musk-xai-memphis-gas-turbines-air-pollution-permits-00317582">communities</a>, and are demanding <a href="https://www.abc.net.au/news/2025-10-28/google-microsoft-restarting-nuclear-plants-for-ai-power/105941378">older, even more dangerous forms of energy</a> to support their expansion. They’ve created <a href="https://www.theguardian.com/technology/2025/sep/11/google-gemini-ai-training-humans">new classes</a> of <a href="https://jacobin.com/2025/06/ai-moderation-ndas-trauma-labor">invisible workers</a> to power their operations, all while selling automation technology that makes work <a href="https://www.bloodinthemachine.com/p/how-ai-is-killing-jobs-in-the-tech-f39">even more precarious</a> for <a href="https://thelocal.to/investigating-scam-journalism-ai/">everyone</a> <a href="https://www.404media.co/teachers-are-not-ok-ai-chatgpt/">else</a>. This is all to say that they’ve moved very, very, very quickly to realize their vision for the future in the last three years. Hell, it’s hard to imagine a future <em>without</em> these platforms, and <a href="https://www.authoritarian-stack.info/">the powerful people</a> who sell them.</p>
<p>But to paraphrase something <a href="https://aworkinglibrary.com/">Mandy Brown</a> once said, there’s not going to be one future<span class="w"> —</span> there will be many. In fact, there’s another future being built right now.</p>
<p>Last fall, the workers at the <a href="https://www.nytimes.com/2024/12/11/business/media/new-york-times-tech-guild-deal.html">New York Times Tech Guild walked off the job for a week</a> and subsequently won their first union contract, one that improved pay, won them <a href="https://en.wikipedia.org/wiki/Just_cause_(employment_law)">“just cause” protections</a>, and more. For the last several months, <a href="https://www.kqed.org/news/12064555/protests-at-microsoft-conference-target-tech-giants-ties-with-israeli-military">workers at Microsoft have waged an effective and relentless pressure campaign</a> against their employer, in order to force the tech giant to stop selling technology that facilitates the genocide in Gaza. Last week, the workers at <a href="https://kickstarterunited.org/">Kickstarter United</a> ended their record-breaking strike, having walked off the job for forty-two days to secure improved pay equity, a contractually guaranteed four-day work week, and job protections from “AI”. This week, the workers at Starbucks United, who <a href="https://www.nocontractnocoffee.org/">began a strike just last week</a>, established <a href="https://bsky.app/profile/sbworkersunited.org/post/3m5yunxybjc2q">a blockade at one of the corporation’s distribution centers</a><span class="w"> —</span> all to bring management back to the table after months of stalled negotiations.</p>
<p>These are just four examples from the last year; there are many, many more. We’re surrounded by a tremendous amount of organizing right now, with workers banding together to build protections at work, to win improved pay and benefits, to advocate for human rights, to build <em>power</em> together. That organizing’s happening at a scale I find tremendously inspiring, and at a pace<span class="w"> —</span> <em>last year; this year; last week; this week</em><span class="w"> —</span> that’s steadily increasing. This is a new future, one being built by quiet conversations with coworkers, by petitions circulated at work, by bargaining sessions, by chants and songs and protests and strikes.</p>
<p>That’s not to say this future is any more assured than the other one: this worker-led future needs significantly more people, including you and your coworkers. But that aside, I’m struck by the contrast between the two. One vision for the future mentions technology and technology alone, neatly eliding right over who might be impacted or harmed if that future should come to pass; the other future is centered around humans, their needs, and their hopes for something better.</p>
<p>Me, I’ve decided which future I want to live in. When I wrote <a href="https://ethanmarcotte.com/books/you-deserve-a-tech-union/">my last book</a>, I said the future of the tech industry <em>is</em> an organized, worker-led labor movement. That’s the quiet hope I’m casting into the months and years and decades that are stretching before me.</p>
<p>Maybe you agree with that future. If you do, well<span class="w"> —</span> let’s you and I start moving toward it.</p>
<hr>
    <div class="footnotes">
    <h2 class="subhed-section subhed-dashed" id="title-footnotes">Footnote</h2>
    <ol><li id="fn-nfts" class="footnote-item"><p>lol <a href="https://ethanmarcotte.com/wrote/the-line-and-the-stream/#fnref-fn-nfts" class="reversefootnote">↩︎</a></p>
</li>
  </ol>
  </div>
      
      <hr>
      <p>This has been “<a href="https://ethanmarcotte.com/wrote/the-line-and-the-stream/">The line and the stream.</a>” a post from <a href="https://ethanmarcotte.com/wrote/">Ethan’s journal.</a></p>
      <p><a href="mailto:listener+rss@ethanmarcotte.com?subject=Reply%20to:%20“The line and the stream.”">Reply via email</a></p>
      
    ]]></description>
      <pubDate>Fri, 21 Nov 2025 04:00:00 +0000</pubDate>
      <link>https://ethanmarcotte.com/wrote/the-line-and-the-stream/</link>
      <dc:creator>My journal — Ethan Marcotte’s website</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5023697298</guid>
    </item>
    <item>
      <title><![CDATA[Steam Machine]]></title>
      <description><![CDATA[<div style="float: left; margin: 0 15px 15px 0; padding: 0; border: 1px solid #000000;"><a href="https://uncrate.com/steam-machine/" rel="bookmark">





    
    



		<img src="https://uncrate.com/assets_c/2025/11/steam-machine-1-thumb-960xauto-186771.jpg" class="t webfeedsFeaturedVisual" alt="Steam Machine" title="Steam Machine" width="960">
	</a></div>Steam takes another swing at the living room with a gaming-focused, compact 6-inch cube PC.<br><br>Visit <a href="https://uncrate.com/steam-machine/">Uncrate</a> for the full post.]]></description>
      <pubDate>Thu, 13 Nov 2025 15:00:01 +0000</pubDate>
      <link>https://uncrate.com/steam-machine/</link>
      <dc:creator>Uncrate</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5014664447</guid>
    </item>
    <item>
      <title><![CDATA[Microsoft™ Ergonomic Keyboard (now sold by Incase)]]></title>
      <description><![CDATA[
<p>For my own long-term reference. </p>



<p>My favorite keyboard is the Microsoft Ergonomic Keyboard. But Microsoft is out of the keyboard hardware game. So apparently they sold the design to Incase, who now <a href="https://www.incase.com/collections/ergonomic-keyboard/products/ergonomic-keyboard">continues to sell it</a> at a perfectly fair price. </p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="930" data-attachment-id="12949" data-permalink="https://chriscoyier.net/2025/10/30/microsoft-ergonomic-keyboard-now-sold-by-incase/screenshot-2025-10-30-at-5-01-01-pm/" data-orig-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/Screenshot-2025-10-30-at-5.01.01-PM.png?fit=1202%2C1092&amp;ssl=1" data-orig-size="1202,1092" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Screenshot 2025-10-30 at 5.01.01 PM" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/Screenshot-2025-10-30-at-5.01.01-PM.png?fit=300%2C273&amp;ssl=1" data-large-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/Screenshot-2025-10-30-at-5.01.01-PM.png?fit=1024%2C930&amp;ssl=1" src="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/Screenshot-2025-10-30-at-5.01.01-PM.png?resize=1024%2C930&amp;ssl=1" alt="Top view of an Incase ergonomic keyboard with a curved design and wrist rest." class="wp-image-12949" srcset="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/Screenshot-2025-10-30-at-5.01.01-PM.png?resize=1024%2C930&amp;ssl=1 1024w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/Screenshot-2025-10-30-at-5.01.01-PM.png?resize=300%2C273&amp;ssl=1 300w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/Screenshot-2025-10-30-at-5.01.01-PM.png?resize=768%2C698&amp;ssl=1 768w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/Screenshot-2025-10-30-at-5.01.01-PM.png?w=1202&amp;ssl=1 1202w" sizes="auto, (max-width: 1000px) 100vw, 1000px"></figure>
]]></description>
      <pubDate>Fri, 31 Oct 2025 00:02:34 +0000</pubDate>
      <link>https://chriscoyier.net/2025/10/30/microsoft-ergonomic-keyboard-now-sold-by-incase/</link>
      <dc:creator>Chris Coyier</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4999043584</guid>
    </item>
    <item>
      <title><![CDATA[Caira AI Mirrorless Camera]]></title>
      <description><![CDATA[<div style="float: left; margin: 0 15px 15px 0; padding: 0; border: 1px solid #000000;"><a href="https://uncrate.com/caira-ai-mirrorless-camera/" rel="bookmark">





    
    



		<img src="https://uncrate.com/assets_c/2025/11/caira-ai-camera-1-thumb-960xauto-186692.jpg" class="t webfeedsFeaturedVisual" alt="Caira AI Mirrorless Camera" title="Caira AI Mirrorless Camera" width="960">
	</a></div>The Caira camera mixes a micro fourth-thirds sensor with a MagSafe mount and AI capabilities.<br><br>Visit <a href="https://uncrate.com/caira-ai-mirrorless-camera/">Uncrate</a> for the full post.]]></description>
      <pubDate>Mon, 10 Nov 2025 23:00:01 +0000</pubDate>
      <link>https://uncrate.com/caira-ai-mirrorless-camera/</link>
      <dc:creator>Uncrate</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5011240945</guid>
    </item>
    <item>
      <title><![CDATA[A new, new logo for the W3C]]></title>
      <description><![CDATA[<p>In an effort to pivot this site into a full on graphic design side business after 2 blog posts about logos in a row (hit me up exclusivly on <a href="https://fishbrain.com/anglers/miketaylr">FB</a> to request a consultation), I thought I would reveal my new, new logo for the W3C.</p>

<p>It turns out they recently launched a new one, but <a href="https://lists.w3.org/Archives/Public/www-archive/2025Oct/thread.html">some folks don’t love it</a>. As an artist, it’s not my job to critique other art, but instead to offer my own compelling vision for the web.</p>

<p><img src="https://miketaylr.com/posts/assets/w3c.png" style="border: 1px solid gray;" alt="a shitty drawing of a w, the word three spelled out, and followed by a period and the letter c"></p>

<p>I shouldn’t have to explain why I went with the classic dark blue and asparagus colors—that much is obvious. And of course, turning c into a file extension as a reminder that NCSA Mosaic was written in C (I didn’t go with <a href="https://en.wikipedia.org/wiki/WorldWideWeb">WorldWideWeb</a> because that was written in Objective C and <code class="language-plaintext highlighter-rouge">.m</code> kinda messes it all up).</p>
]]></description>
      <pubDate>Sat, 25 Oct 2025 04:00:00 +0000</pubDate>
      <link>https://miketaylr.com/posts/2025/10/new-new-logo-for-w3c.html</link>
      <dc:creator>Mike Taylr Dot Com Web Log</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4992628918</guid>
    </item>
    <item>
      <title><![CDATA[]]></title>
      <description><![CDATA[
<p><a href="https://blog.burkert.me/posts/in_praise_of_syndication/">Tom Burkert on controlling what he reads</a>, through RSS of course.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>If I’m in the mood for something lighter, I can just look into my “Fun” folder to check out new stuff from&nbsp;<a href="https://theoatmeal.com/" target="_blank" rel="noreferrer noopener">The Oatmeal</a>&nbsp;or&nbsp;<a href="https://xkcd.com/" target="_blank" rel="noreferrer noopener">xkcd</a>. If I feel like reading something more thoughtful, I’d dive into my “Reads” folder for&nbsp;<a href="https://www.themarginalian.org/" target="_blank" rel="noreferrer noopener">The Marginalian</a>&nbsp;or&nbsp;<a href="https://sentiers.media/" target="_blank" rel="noreferrer noopener">Sentiers</a>. Feeling like catching up on the newest AI research? I can browse the latest research papers from&nbsp;<a href="https://arxiv.org/" target="_blank" rel="noreferrer noopener">arXiv</a>&nbsp;that have specific keywords in the abstracts (such as prompt injection). Or I could just browse everything at once to see what piques my interest. I am the master of what information I consume, how and in what order, and no one can take that away from me by rearranging my feed or tweaking the algorithm.<br></p>
</blockquote>
]]></description>
      <pubDate>Fri, 24 Oct 2025 17:04:18 +0000</pubDate>
      <link>https://chriscoyier.net/2025/10/24/12937/</link>
      <dc:creator>Chris Coyier</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4991865910</guid>
    </item>
    <item>
      <title><![CDATA[A Treatise on AI Chatbots Undermining the Enlightenment]]></title>
      <description><![CDATA[On chatbot sycophancy, passivity, and the case for more intellectually challenging companions]]></description>
      <pubDate>Tue, 05 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://maggieappleton.com/ai-enlightenment/</link>
      <dc:creator>Maggie Appleton</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4902292836</guid>
    </item>
    <item>
      <title><![CDATA[Speeding up my Learning Log process]]></title>
      <description><![CDATA[<p>Dave’s blog post <a href="https://daverupert.com/2025/05/obsidian-link-aggregator/">“How to make a Link Aggregator in Obsidian”</a> inspired me to finally make my Learning Log process a little smarter, starting with the May 2025 log.</p>
<h2 id="what-i-had-been-doing">What I had been doing</h2>
<ol>
<li>Automatically syncing my Reader by Readwise highlights into Obsidian</li>
<li>Going through the new articles manually and adding them to my blog post Markdown (also written in Obsidian)</li>
</ol>
<p>Obsidian enables you to customize to the nth degree, so I figured there was a better way to do this but hadn’t made it a priority. Dave’s post showed me I could do this pretty quickly!</p>
<h2 id="enter-dataview-plugin">Enter Dataview plugin</h2>
<p>I’d already installed the <a href="https://blacksmithgu.github.io/obsidian-dataview/">Dataview plugin</a> for Obsidian for one reason or another and ended up writing a DQL query very similar to Dave’s:</p>
<pre><code>LIST map(rows, (r) =&gt; elink(regexreplace(r.file.frontmatter.url, "\\?utm_.*", ""), r.title))
FROM "Resources/Readwise/Articles"
WHERE file.frontmatter.highlightedDate &gt;= "2025-08-01"
	AND file.frontmatter.highlightedDate &lt; "2025-09-01"
	AND file.frontmatter.url
FLATTEN file.tags AS tag
GROUP BY tag
SORT file.frontmatter.highlightedDate DESC
</code></pre>
<p>What this query does is:</p>
<ul>
<li>Pull all the articles I highlighted in a given month from the directory where I’ve synced my Readwise articles. It filters out any articles that don’t have a URL in the frontmatter, i.e. email newsletters.</li>
<li>Group the linked article titles by the tag I applied in Reader.</li>
</ul>
<p>I copy and paste this query into my blog post draft, update the tag groupings to be headers, and hand-curate the list a little further. I could use Dataview JS to structure this in a fancier way and skip some of this manual bit, but I find it’s worth slowing down at this curation stage.</p>
<p>You’ll notice I do have dates hardcoded in here as well. As we all know, the hardest problems in computer science are:</p>
<ul>
<li>People problems</li>
<li>Naming things</li>
<li>Wrangling dates</li>
</ul>
<p>I kept getting weird errors and issues with data handling, so decided to just throw up my hands and enter the dates manually. I only have to update them once a month, and so <a href="https://xkcd.com/1205/">this XKCD comic</a> seemed apt.</p>
<p>In any case, this was a nice little quality of life update for my blogging process, and I’m now playing with all things Dataview in my Obsidian notes!</p>
]]></description>
      <pubDate>Mon, 18 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://melanie-richards.com/blog/speeding-up-learning-logs/</link>
      <dc:creator>Melanie Richards</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4916582122</guid>
    </item>
    <item>
      <title><![CDATA[Eight years of Jessie]]></title>
      <description><![CDATA[<p>I am currently regretting the posts I didn't make. Yes, as someone who bangs on and on about posting things on your blog first and then elsewhere, I too fail many times. And, it's been like that when it comes to Jessie.</p>
<p>Since we adopted Jessie in 2017, I've been celebrating her adoption aniversary on social media instead of this blog. And it bit me in the arse. I used to have a lovely thread going on for years on Twitter but, of course, I haven't touched that hell space in a very long time. I was lazy and now I am paying the price. For this year's anniversary I still posted on <a href="https://bsky.app/profile/ohhelloana.blog/post/3lw5ds3y2zc2y">BlueSky</a> and <a href="https://front-end.social/@anarodrigues/115011366161406914">Mastodon</a> but, I want to make sure that memory is captured here too.</p>
<p>Sweet Jessie has been with us for eight years now and, fun fact, she is about to turn 14! She was five years old when we adopted her. It's been a priviledge that the majority of her life has now been with us.</p>
<p><img src="https://ohhelloana.blog/assets/posts/jessie/jessie_eight.jpeg" alt="Jessie, a black and white cat, standing by a plate with cat food with a lit candle with the number eight."></p>
<p>Since living with us she has:</p>
<ul>
<li>lived in three different homes;</li>
<li>brought a mouse in only once (and it was a baby so it doesn't count);</li>
<li>went missing once (the worst);</li>
<li>has had dentist appointments and teeth removed;</li>
<li>became a big sister to an human;</li>
</ul>
<p><img src="https://ohhelloana.blog/assets/posts/jessie/jessie_seven-years.jpeg" alt="Collage of seven photos of my cat Jessie standing by cat food with a candle on top. In each photo the candle number changes to represent the adoption anniversary. Except for year 6 as there was no numbered candles available so it is a normal candle."></p>
<p>I really hope I have many more years of posts to make. 💗</p>
]]></description>
      <pubDate>Sat, 16 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://ohhelloana.blog/eight-years-of-jessie/</link>
      <dc:creator>Ana Rodrigues</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4914276715</guid>
    </item>
    <item>
      <title><![CDATA[“Why would anybody start a website?”]]></title>
      <description><![CDATA[<p>Slowly catching up on my RSS feed and I just read <a href="https://daverupert.com/2025/09/why-would-anybody-start-a-website/">Dave's post “Why would anybody start a website?”</a>. Obviously, Dave's post is right up my street but it reminded me of a random notes post on my phone that I wrote a while back.</p>
<p>Why build a website?</p>
<p>Why knit a jumper with your bare hands?</p>
<p>Why cook a homemade meal from scratch?</p>
<p>Why paint in a canvas?</p>
<p>Why fix a broken thing?</p>
<p>Why write a letter?</p>
<p>Why anything really?</p>
<p>We, humans, are driven to touch and craft with our body. Deep down we crave that. Website making is a digital craft.</p>
<p>There, I let out a draft from my phone and it is now in my blog. Why not?</p>
<p>Anyway, I love how the "no thought is original" is real. Turns out, one day before I started this draft on my blog, <a href="https://blog.jim-nielsen.com/2025/why-make-a-website-in-2025/">Jim Nielsen</a> also did a similar comparison. They're smarter than me, listen to them!</p>
<hr>
<a href="https://news.indieweb.org/en" class="u-syndication">
  Also posted on IndieNews
</a>
]]></description>
      <pubDate>Thu, 18 Sep 2025 00:00:00 +0000</pubDate>
      <link>https://ohhelloana.blog/why/</link>
      <dc:creator>Ana Rodrigues</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4949655870</guid>
    </item>
    <item>
      <title><![CDATA[Sizzle Rizzle]]></title>
      <description><![CDATA[
          <video style="display: none" src="https://nerdy.dev/media/sizzle-reel-2025.mp4" alt="A quick peek into projects from my past" height="1080" width="1920">
        <p>A <small>small</small> sample of <strong>UI</strong>, <strong>code</strong>, <strong>tools</strong>, and <strong>design</strong>
from my 20 professional years of webdev.</p>
</video>]]></description>
      <pubDate>Fri, 04 Jul 2025 21:20:40 +0000</pubDate>
      <link>https://nerdy.dev/sizzle-rizzle?utm_source=rss</link>
      <dc:creator>Adam Argyle</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4872246908</guid>
    </item>
    <item>
      <title><![CDATA[WWW Ep212 With Dave Rupert]]></title>
      <description><![CDATA[
          <img style="display: none" src="https://nerdy.dev/media/www-ep212.jpg" alt="Dave Rupert shown as Macho Man Randy Standards" height="1400" width="1400">
        <p><span class="Tag">Ep #212</span><br>
<strong>TalkShop Show w/ Macho Man Randy Standards</strong></p>
<p><a href="https://robbiethewagner.dev">Robbie</a> and I chat with <a href="https://daverupert.com">Dave Rupert</a> about whiskey, web culture, the quirks of building side projects, the shifting landscape of the web, AI-driven development, spec-driven workflows, RSS’s decline, and more.</p>
<p>⤷ <a href="https://whiskey.fm/talkshop-show-w-macho-man-randy-standards">whiskey.fm</a> · <a href="https://www.youtube.com/watch?v=R15mWOB4MH0">youtube</a> · <a href="https://open.spotify.com/episode/5WsfiALuQ6hRU46StSBmJP">spotify</a> · <a href="https://podcasts.apple.com/us/podcast/talkshop-show-w-macho-man-randy-standards/id1552776603?i=1000729691108">apple</a></p>
]]></description>
      <pubDate>Thu, 02 Oct 2025 14:29:04 +0000</pubDate>
      <link>https://nerdy.dev/www-ep212-with-dave-rupert?utm_source=rss</link>
      <dc:creator>Adam Argyle</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4965882100</guid>
    </item>
    <item>
      <title><![CDATA[closedBy=any]]></title>
      <description><![CDATA[
          <img style="display: none" src="https://nerdy.dev/media/bzz.jpg" alt="5th element buzz off scene" height="414" width="944">
        <p>Quick post on <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/closedBy"><code>closedBy="any"</code></a>, a declarative way to add light-dismiss to a dialog:</p>
<pre><code class="language-html"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--shiki-foreground)">&lt;</span><span style="color:var(--shiki-token-string-expression)">dialog</span><span style="color:var(--shiki-token-function)"> closedBy</span><span style="color:var(--shiki-foreground)">=</span><span style="color:var(--shiki-token-string-expression)">"any"</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">  &lt;</span><span style="color:var(--shiki-token-string-expression)">p</span><span style="color:var(--shiki-foreground)">&gt;Hi, I'm a dialog.&lt;/</span><span style="color:var(--shiki-token-string-expression)">p</span><span style="color:var(--shiki-foreground)">&gt;</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">&lt;/</span><span style="color:var(--shiki-token-string-expression)">dialog</span><span style="color:var(--shiki-foreground)">&gt;</span></span></code></pre>
</code></pre>
<p>And just like that, tapping or clicking outside the dialog will close it.</p>

        <h2>
          Oh yeah! Well I bet I can't use it yet.
          <a name="oh-yeah!-well-i-bet-i-can't-use-it-yet." href="https://nerdy.dev/closedby-any?utm_source=rss#oh-yeah!-well-i-bet-i-can't-use-it-yet.">#</a>
        </h2>
       <p>Here's the browser support:</p>
<script type="module">
  import "https://cdn.jsdelivr.net/npm/baseline-status";
</script>

<p><baseline-status featureid="dialog-closedby"></baseline-status></p>
<p>Or a bit-o-js to get you started:</p>
<pre><code class="language-js"><pre class="shiki css-variables" style="background-color:var(--shiki-background);color:var(--shiki-foreground)" tabindex="0"><code><span class="line"><span style="color:var(--shiki-token-constant)">someDialog</span><span style="color:var(--shiki-token-function)">.addEventListener</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">'click'</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-foreground)"> ({target:dialog}) </span><span style="color:var(--shiki-token-keyword)">=&gt;</span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  if</span><span style="color:var(--shiki-foreground)"> (</span><span style="color:var(--shiki-token-constant)">dialog</span><span style="color:var(--shiki-foreground)">.nodeName </span><span style="color:var(--shiki-token-keyword)">===</span><span style="color:var(--shiki-token-string-expression)"> 'DIALOG'</span><span style="color:var(--shiki-foreground)">)</span></span>
<span class="line"><span style="color:var(--shiki-token-constant)">    dialog</span><span style="color:var(--shiki-token-function)">.close</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">'dismiss'</span><span style="color:var(--shiki-foreground)">)</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">})</span></span></code></pre>
</code></pre>

        <h2>
          More resources
          <a name="more-resources" href="https://nerdy.dev/closedby-any?utm_source=rss#more-resources">#</a>
        </h2>
       <p><em>You</em> should write one! </p>
<p>I'm writing this because I haven't seen enough folks using it or talking about it. Pretty nifty feature if ya ask me. So I'll make a 10m post.</p>
<p>If you want more <code>&lt;dialog&gt;</code> goodness, I wrote a fun post about how to make <strong>nice</strong> dialogs cuz the defaults are so poopy. Check it out and <a href="https://nerdy.dev/have-a-dialog">have a dialog</a>.</p>
<br>

<q class="notebook">

<p><a href="https://nerdy.dev/notebook/dialog-starter.html">Also checkout the <strong>Dialog Starter</strong> notebook!</a></p>
</q>]]></description>
      <pubDate>Thu, 16 Oct 2025 04:42:39 +0000</pubDate>
      <link>https://nerdy.dev/closedby-any?utm_source=rss</link>
      <dc:creator>Adam Argyle</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4981955759</guid>
    </item>
    <item>
      <title><![CDATA[The History of Themeable User Interfaces]]></title>
      <description><![CDATA[
<p><small>This post is an excerpt from our comprehensive online course, <a href="https://designtokenscourse.com/">Subatomic: The Complete Guide To Design Tokens</a>. The course digs into <em>everything</em> that goes into creating design token systems and themeable user interfaces to help <a href="https://bradfrost.com/blog/post/the-multi-all-the-things-organization/">Multi All-The-Things organizations</a> meet the multifarious needs of their digital products.</small></p>



<p><a href="https://designtokenscourse.com/">Design tokens</a> may be the latest incarnation, but software creators have been creating themeable user interfaces for quite a long time! As with all things, we can study history to learn from our past to inform our future. So let’s dig in!</p>



<h2 class="wp-block-heading">1970s: The first commercial GUIs</h2>



<p>The <a href="https://en.wikipedia.org/wiki/Graphical_user_interface#History">history</a> of the <a href="https://en.wikipedia.org/wiki/Graphical_user_interface">graphical user interface (GUI)</a> is fascinating and naturally involved a lot of research, iteration, and development before being unleashed upon the world. The first commercial computer featuring a GUI was the <a href="https://en.wikipedia.org/wiki/Xerox_Alto">Xerox Alto</a> in 1973. Behold it in all its glory:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="871" height="1024" src="https://bradfrost.com/wp-content/uploads/2025/05/image-871x1024.png" alt="Xerox Alto GUI" class="wp-image-24057" srcset="https://bradfrost.com/wp-content/uploads/2025/05/image-871x1024.png 871w, https://bradfrost.com/wp-content/uploads/2025/05/image-700x823.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/image-768x903.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/image-1307x1536.png 1307w, https://bradfrost.com/wp-content/uploads/2025/05/image-1742x2048.png 1742w" sizes="auto, (max-width: 871px) 100vw, 871px"></figure>



<p>GUIs ultimately hit the market in 1981 in the form of the <a href="https://en.wikipedia.org/wiki/Xerox_Star">Xerox Star</a>. The Star was similar to bands like <a href="https://en.wikipedia.org/wiki/Can_(band)">CAN</a> and <a href="https://en.wikipedia.org/wiki/Fugazi">Fugazi</a> in that sense that it wasn’t a commercial hit, but was massively influential to everything that followed.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="666" src="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.12.42 PM-1024x666.png" alt="Xerox Star GUI" class="wp-image-24054" srcset="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.12.42 PM-1024x666.png 1024w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.12.42 PM-700x455.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.12.42 PM-768x499.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.12.42 PM-1536x999.png 1536w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.12.42 PM-2048x1332.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></figure>



<p>The 70s also saw the rise of video games, especially with the runaway success of <a href="https://en.wikipedia.org/wiki/Pong">Pong</a>. <a href="https://en.wikipedia.org/wiki/Galaxian">Galaxian by Atari</a> was released in 1979 and was the first successful game that made use of a full-color RGB display.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="666" src="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.19 PM-1024x666.png" alt="Galaxian Atari gameplay" class="wp-image-24055" srcset="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.19 PM-1024x666.png 1024w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.19 PM-700x455.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.19 PM-768x499.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.19 PM-1536x999.png 1536w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.19 PM-2048x1332.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></figure>



<p>Game &amp; computer designers were contending with extremely limited processing/memory resources, so they were forced to get extremely creative in order to support full-color screens. <a href="https://en.wikipedia.org/wiki/Sprite_(computer_graphics)">Sprites</a> were used to manage the graphics and different states for UI elements, and color themes cleverly transformed the same shapes into different characters. Themed UI elements!</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1293" height="1044" src="https://bradfrost.com/wp-content/uploads/2025/08/Sprite.png" alt="Sprite image from Galaxian" class="wp-image-24823" srcset="https://bradfrost.com/wp-content/uploads/2025/08/Sprite.png 1293w, https://bradfrost.com/wp-content/uploads/2025/08/Sprite-700x565.png 700w, https://bradfrost.com/wp-content/uploads/2025/08/Sprite-1024x827.png 1024w, https://bradfrost.com/wp-content/uploads/2025/08/Sprite-768x620.png 768w" sizes="auto, (max-width: 1293px) 100vw, 1293px"></figure>



<h2 class="wp-block-heading">1980s: Color, games, and the PC age</h2>



<p>The 1980s ushered in an era of full-color displays, which opened many new opportunities and challenges for designers. Just look at this glorious sprite showcasing from Nintendo’s 1985 era-defining video game, <a href="https://bradfrost.com/">Super Mario Brothers</a>:</p>



<figure class="wp-block-image size-large"><a href="https://www.spriters-resource.com/nes/supermariobros/asset/50365/"><img loading="lazy" decoding="async" width="1024" height="764" src="https://bradfrost.com/wp-content/uploads/2025/08/NES-Super-Mario-Bros.-Playable-Characters-Mario-Luigi-1-1024x764.png" alt="Mario and Luigi sprite sheet from Super Mario Bros" class="wp-image-24826" srcset="https://bradfrost.com/wp-content/uploads/2025/08/NES-Super-Mario-Bros.-Playable-Characters-Mario-Luigi-1-1024x764.png 1024w, https://bradfrost.com/wp-content/uploads/2025/08/NES-Super-Mario-Bros.-Playable-Characters-Mario-Luigi-1-700x523.png 700w, https://bradfrost.com/wp-content/uploads/2025/08/NES-Super-Mario-Bros.-Playable-Characters-Mario-Luigi-1-768x573.png 768w, https://bradfrost.com/wp-content/uploads/2025/08/NES-Super-Mario-Bros.-Playable-Characters-Mario-Luigi-1-1536x1147.png 1536w, https://bradfrost.com/wp-content/uploads/2025/08/NES-Super-Mario-Bros.-Playable-Characters-Mario-Luigi-1.png 1921w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></figure>



<p><strong>It’s <em>wild</em> that two of the most iconic characters in the history of pop culture — red-clad Mario and green-clad Luigi — are themeable UI elements born from pragmatic ingenuity to overcome technological challenges</strong>. Freaking amazing.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="852" src="https://bradfrost.com/wp-content/uploads/2025/08/mario-and-luigi-design-system-1024x852.png" alt="8-bit Mario and Luigi with the caption &quot;two of the most iconic characters in the history of pop culture are themeable UI elements born from pragmatic ingenuity to overcome technological challenges.&quot;" class="wp-image-24882" srcset="https://bradfrost.com/wp-content/uploads/2025/08/mario-and-luigi-design-system-1024x852.png 1024w, https://bradfrost.com/wp-content/uploads/2025/08/mario-and-luigi-design-system-700x583.png 700w, https://bradfrost.com/wp-content/uploads/2025/08/mario-and-luigi-design-system-768x639.png 768w, https://bradfrost.com/wp-content/uploads/2025/08/mario-and-luigi-design-system.png 1200w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></figure>



<p><strong>This ingenious sprite theming is a masterclass in creative constraints.</strong> One thing you absolutely can’t unsee once you know is learning that <strong><a href="https://www.youtube.com/watch?v=ai7d1K4Yf6A">the clouds and the bushes in Super Mario Bros are the exact same shape, just themed differently!</a></strong></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="576" src="https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.12.30-PM-1024x576.png" alt="" class="wp-image-24886" srcset="https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.12.30-PM-1024x576.png 1024w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.12.30-PM-700x394.png 700w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.12.30-PM-768x432.png 768w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.12.30-PM-1536x864.png 1536w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.12.30-PM.png 2038w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></figure>



<p>To put a finer point on this: the creators established the game’s structure and functionality and understood that <strong>duplicating all of that structure &amp; functionality merely to achieve different aesthetic results would be wasteful, expensive, and imprudent.</strong> <strong>So instead they created a themeable design system to <a href="https://bradfrost.com/blog/post/the-new-separation-of-concerns/">achieve this critical separation of concerns between structure/functionality &amp; aesthetics</a>.</strong> Incredible!</p>



<h3 class="wp-block-heading">The PC Era</h3>



<p>Naturally, full-color displays found their way into personal computer operating systems in the 1980s. Several <a href="https://www.youtube.com/watch?v=euhp8Vn2FTw">full-color computers</a> hit the market in the mid-80s, and the iconic <a href="https://en.wikipedia.org/wiki/Apple_II">Apple II</a> released its first full-color display in 1987.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="666" src="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.36 PM-1024x666.png" alt="" class="wp-image-24070" srcset="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.36 PM-1024x666.png 1024w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.36 PM-700x455.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.36 PM-768x499.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.36 PM-1536x999.png 1536w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.36 PM-2048x1332.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></figure>



<h2 class="wp-block-heading">1990s: OS-level theming, The Web, and CSS</h2>



<p>Microsoft introduced <a href="https://imgur.com/gallery/every-windows-3-1-theme-SsVYqM1#3FNW1hn">color schemes</a> with the release of Windows 3.1 in 1992. Users (including a young me!) could go into their preferences, choose a theme for the Windows UI, and even customize the colors. Easily the best theme was named “<a href="https://blog.codinghorror.com/a-tribute-to-the-windows-31-hot-dog-stand-color-scheme/">Hotdog Stand</a>“:</p>



<figure class="wp-block-image size-large"><a href="https://blog.codinghorror.com/a-tribute-to-the-windows-31-hot-dog-stand-color-scheme/"><img loading="lazy" decoding="async" width="1024" height="666" src="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.42 PM-1024x666.png" alt="Hot Dog Stand Windows 3.1 theme" class="wp-image-24064" srcset="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.42 PM-1024x666.png 1024w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.42 PM-700x455.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.42 PM-768x499.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.42 PM-1536x999.png 1536w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.42 PM-2048x1332.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></figure>



<p>This was<strong> one of the most sophisticated, large-scale theming implementations that truly introduced the concept of real user preference and customization to the masses</strong>. This UI customization and themeability became even more robust with the release of <a href="https://en.wikipedia.org/wiki/Windows_95">Windows 95</a>.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="666" src="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.44 PM-1024x666.png" alt="Windows 95 appearence theme displays" class="wp-image-24065" srcset="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.44 PM-1024x666.png 1024w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.44 PM-700x455.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.44 PM-768x499.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.44 PM-1536x999.png 1536w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.44 PM-2048x1332.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></figure>



<p>These new operating systems unlocked new opportunities for software like <a href="https://en.wikipedia.org/wiki/Winamp">Winamp</a> to give users the ability to create their own music player skins, truly putting the “personal” in “personal computer.”</p>



<figure class="wp-block-image size-large"><a href="https://skins.webamp.org/"><img loading="lazy" decoding="async" width="1024" height="577" src="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-5.55.01 PM-1024x577.png" alt="" class="wp-image-24072" srcset="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-5.55.01 PM-1024x577.png 1024w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-5.55.01 PM-700x395.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-5.55.01 PM-768x433.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-5.55.01 PM-1536x866.png 1536w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-5.55.01 PM-2048x1155.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a><figcaption class="wp-element-caption">A glorious collection of Winamp skins courtesy of the <a href="https://skins.webamp.org/">Winamp Skin Museum</a></figcaption></figure>



<p>Of course, the World Wide Web also exploded onto the scene in the 1990s. 1993 saw the release of <a href="https://en.wikipedia.org/wiki/NCSA_Mosaic">Mosaic</a>, the world’s first commercial internet browser, but it was really the introduction of <a href="https://en.wikipedia.org/wiki/Netscape_Navigator">Netscape Navigator</a> in late 1994 that made the web an absolute <a href="https://www.youtube.com/watch?v=95-yZ-31j9A">phenomenon</a>.</p>



<p>The browser opened a portal into a million worlds, with <strong>each website providing its own unique user experience and interface.</strong> It was during this era that I found myself with my best friend making Dragonball Z fan websites on <a href="https://en.wikipedia.org/wiki/GeoCities">Geocities</a>. I’ve been in love ever since.</p>



<p><small>Note: check out the brilliantly-curated and thoughtful <a href="https://www.webdesignmuseum.org/">Web Design Museum</a> for more web history!</small></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="800" height="600" src="https://bradfrost.com/wp-content/uploads/2025/05/image-1.png" alt="A Dragonball Z Geocities fan website" class="wp-image-24069" srcset="https://bradfrost.com/wp-content/uploads/2025/05/image-1.png 800w, https://bradfrost.com/wp-content/uploads/2025/05/image-1-700x525.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/image-1-768x576.png 768w" sizes="auto, (max-width: 800px) 100vw, 800px"><figcaption class="wp-element-caption">An example of a Dragonball Z Geocities fan website. I wish I could find my own; I remember tiled background images, Goku and Vegeta animated GIFs, fire graphics, hit counter, and a guestbook</figcaption></figure>



<p>Accomplishing custom designs in the early web relied on hard coding styling information into the HTML using elements like <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/font"><code>&lt;font&gt;</code> tag</a>. Thankfully, <strong><a href="https://en.wikipedia.org/wiki/CSS">CSS</a> hit the scene in late 1996</strong>, which provided a dedicated language for styling and revolutionized how we create for the web.</p>



<h2 class="wp-block-heading">2000s: The Web Grows and Native Hits The Scene</h2>



<p>The web continued to take off and the technologies to create it continued to improve. Books like <a href="https://zeldman.com/">Jeffrey Zeldman</a>‘s <em><a href="https://en.wikipedia.org/wiki/Designing_with_Web_Standards">Designing with Web Standards</a></em> and CSS books by pioneers like <a href="https://en.wikipedia.org/wiki/Molly_Holzschlag">Molly Holzschlag</a> and <a href="https://meyerweb.com/eric/books/">Eric Meyer</a> helped the world’s emerging web community create beautiful and expressive web experiences.</p>



<p>In 2003, <a href="https://daveshea.com/">Dave Shea</a> created the <a href="https://csszengarden.com/">CSS Zen Garden</a>, which beautifully demonstrated the concept of <a href="https://en.wikipedia.org/wiki/Separation_of_concerns">separation of concerns</a> as it applies to the languages of the web.</p>



<figure class="wp-block-image size-large"><a href="https://csszengarden.com/"><img loading="lazy" decoding="async" width="1024" height="666" src="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.52 PM-1024x666.png" alt="" class="wp-image-24085" srcset="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.52 PM-1024x666.png 1024w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.52 PM-700x455.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.52 PM-768x499.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.52 PM-1536x999.png 1536w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-3.13.52 PM-2048x1332.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></figure>



<p>Using the <a href="https://csszengarden.com/examples/index">exact same HTML file</a>, designers could use CSS to design radically different aesthetics. For me and countless others, the CSS Zen Garden drove home the importance of the <a href="https://bradfrost.com/blog/post/the-new-separation-of-concerns/">separation of concerns</a> and unlocked the real creative potential for the web as a design medium.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="615" src="https://bradfrost.com/wp-content/uploads/2025/05/css-zen-1-1024x615.png" alt="" class="wp-image-24088" srcset="https://bradfrost.com/wp-content/uploads/2025/05/css-zen-1-1024x615.png 1024w, https://bradfrost.com/wp-content/uploads/2025/05/css-zen-1-700x421.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/css-zen-1-768x462.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/css-zen-1-1536x923.png 1536w, https://bradfrost.com/wp-content/uploads/2025/05/css-zen-1-2048x1231.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></figure>



<p>The web field marched into the <a href="https://en.wikipedia.org/wiki/Web_2.0">Web 2.0 era</a> bursting with creative expression, which was put on full display on websites like <a href="https://en.wikipedia.org/wiki/Myspace">MySpace</a>.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="735" height="597" src="https://bradfrost.com/wp-content/uploads/2025/08/97771356db04bef1b0620a75f8794da0.jpg" alt="" class="wp-image-24839" srcset="https://bradfrost.com/wp-content/uploads/2025/08/97771356db04bef1b0620a75f8794da0.jpg 735w, https://bradfrost.com/wp-content/uploads/2025/08/97771356db04bef1b0620a75f8794da0-700x569.jpg 700w" sizes="auto, (max-width: 735px) 100vw, 735px"></figure>



<p>This customization started entering into more functional software, like then-new tools like Google Personalized Homepages (later renamed <a href="https://en.wikipedia.org/wiki/IGoogle">iGoogle</a>).</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="596" src="https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-27-at-9.58.41-PM-1024x596.png" alt="Google Personalized Homepage featuring themes like &quot;Beach&quot; &quot;Bus Stop&quot; &quot;City Scape&quot; &quot;Sweet Dreams&quot; &quot;Tea House&quot; and &quot;Seasonal Scape&quot;" class="wp-image-24840" srcset="https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-27-at-9.58.41-PM-1024x596.png 1024w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-27-at-9.58.41-PM-700x408.png 700w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-27-at-9.58.41-PM-768x447.png 768w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-27-at-9.58.41-PM-1536x894.png 1536w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-27-at-9.58.41-PM-2048x1193.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></figure>



<p>Up until this point, authoring CSS to achieve sophisticated theming was extremely laborious, hard-coded, and fraught. Adding to the complexity was that the whole industry was thrust into a brand-new mobile era as soon as <a href="https://www.youtube.com/watch?v=MnrJzXM7a6o">Steve Jobs muttered “and one more thing…” in 2007</a>.</p>



<h2 class="wp-block-heading">2010s: Sass, Design Systems, Design Tokens</h2>



<p><a href="https://andreipfeiffer.dev/blog/2022/scalable-css-evolution/part3-css-processors">CSS pre-processors like Sass and Less</a> hit the scene in the mid-2000s, and <a href="https://sass-lang.com/">Sass</a> 2.0 introduced this wild little idea called <a href="https://sass-lang.com/documentation/variables/">variables</a> in 2010. This was embraced by designers and developers who finally had a <a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a> way of defining a design language.</p>



<pre class="wp-block-code"><code>$primary-color: #3498db;

button {
  background-color: $primary-color;
}</code></pre>



<p>This unlocked new opportunities for themeability and more dynamic styling on the web. These new capabilities coincided with the emergence of <a href="https://alistapart.com/article/responsive-web-design/">responsive web design</a>, modular CSS methodologies like <a href="https://github.com/stubbornella/oocss">OOCSS</a>, <a href="https://smacss.com/">SMACSS</a>, <a href="https://getbem.com/">BEM</a>, and others. In May of 2013 I introduced a thing called <a href="https://bradfrost.com/blog/post/atomic-web-design/">Atomic Design</a>. A few months later, <a href="https://react.dev/">React</a> was introduced. The zeitgeist was flexibility, modularity, and component-driven design/dev, which ultimately coalesced under the label “<strong>design systems.”</strong></p>



<p>Design systems like <a href="https://m3.material.io/">Google’s Material Design</a> emerged in 2014-2015 and had themeability in mind right out of the gate.</p>



<figure class="wp-block-image size-large"><a href="https://medium.com/@MartaKakozwa/how-to-choose-colors-for-material-design-app-b72da1ccbd9a"><img loading="lazy" decoding="async" width="1024" height="511" src="https://bradfrost.com/wp-content/uploads/2025/08/1_myRyhLXr5HtniWsxdM9-lw-1024x511.webp" alt="Material Design with theme switching" class="wp-image-24874" srcset="https://bradfrost.com/wp-content/uploads/2025/08/1_myRyhLXr5HtniWsxdM9-lw-1024x511.webp 1024w, https://bradfrost.com/wp-content/uploads/2025/08/1_myRyhLXr5HtniWsxdM9-lw-700x350.webp 700w, https://bradfrost.com/wp-content/uploads/2025/08/1_myRyhLXr5HtniWsxdM9-lw-768x384.webp 768w, https://bradfrost.com/wp-content/uploads/2025/08/1_myRyhLXr5HtniWsxdM9-lw-1536x767.webp 1536w, https://bradfrost.com/wp-content/uploads/2025/08/1_myRyhLXr5HtniWsxdM9-lw.webp 1902w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></figure>



<p>These design systems demonstrated to the world how its possible to deliver a unified design language to multiple products, platforms, and businesses. Holy crap was it ambitious! Still is! In order to support these vast and multifarious product UIs, the concepts, technology, and tooling around theming needed to evolve.</p>



<h3 class="wp-block-heading">Design tokens origin story</h3>



<p>While Sass variables gave designers &amp; developers a great way to define and use a design language, <a href="https://www.jina.me/">Jina Anne</a> and Jon Levine at Salesforce <a href="https://youtu.be/wDBEc3dJJV8?si=jVV8JK68HsvvQOPh&amp;t=730">delivered a talk</a> that gave the concept of these low-level design decisions a more potent name: <strong>design tokens</strong>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Design tokens are the sub atoms — the smallest pieces — of the design system. They’re an abstraction of our UI visual design and store style properties as variables.</p><cite>Jina Anne</cite>
</blockquote>



<p>There are a <a href="https://atlassian.design/tokens/design-tokens">number</a> <a href="https://www.designtokens.org/glossary/">of</a> <a href="https://m3.material.io/foundations/design-tokens/overview">definitions</a> <a href="https://spectrum.adobe.com/page/design-tokens/">and</a> <a href="https://css-tricks.com/what-are-design-tokens/">explainers</a> <a href="https://piccalil.li/blog/what-are-design-tokens/">about</a> what design tokens are, which can be summarized like this. <strong>Design tokens are:</strong></p>



<ul class="wp-block-list">
<li><strong>The smallest (<a href="https://designtokenscourse.com/">subatomic</a>) elements of a design system</strong></li>



<li><strong>Design decisions for a design language</strong></li>



<li><strong>Design properties stored as variables</strong></li>



<li><strong>Implementation/technology agnostic source of truth</strong> that can be converted into any format</li>



<li><strong>A common language for design </strong>used to connect people, disciplines, tools, and systems</li>



<li><strong>The engine of themeable user interfaces</strong></li>
</ul>



<p>In the mid 2010, code tools like <a href="https://github.com/salesforce-ux/theo">Theo</a> from Salesforce and <a href="https://styledictionary.com/">Style Dictionary</a> from Amazon emerged to transform implementation-agnostic JSON or YAML token definitions into technology-specific formats like Sass variables, iOS &amp; Android formats, JS objects, and an important new native web technology called <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties">CSS custom properties</a>. </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.18.55-PM-1024x572.png" alt="Diagram showing how tokens are defined in JSON and then converted into tech-specific formats" class="wp-image-24890" srcset="https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.18.55-PM-1024x572.png 1024w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.18.55-PM-700x391.png 700w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.18.55-PM-768x429.png 768w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.18.55-PM-1536x857.png 1536w, https://bradfrost.com/wp-content/uploads/2025/08/Screenshot-2025-08-28-at-1.18.55-PM-2048x1143.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"><figcaption class="wp-element-caption"><a href="https://designtokenscourse.com/">Our Subatomic design tokens course</a> introduces core concepts for creating token systems, provides sample architecture, and detailed walkthroughs for how to set up a token system in Figma &amp; code then successfully adopt it in your org’s product ecosystem.</figcaption></figure>



<p>CSS custom properties give the web theming superpowers — and to do it live! Seriously, check out this amazing website by <a href="https://abandon.ie/">Abban Dunne</a>:</p>



<figure class="wp-block-video"><video controls="" src="https://bradfrost.com/wp-content/uploads/2025/08/abban-2.mp4"></video></figure>



<p>There’s a whole lot to design tokens! And a whole lot of opportunity  Perhaps that’s why a <a href="https://www.w3.org/community/design-tokens/">Design Tokens W3C Community Group</a> was established in 2019 to help wrangle and standardize some of the structure, naming, and architecture around design tokens.</p>



<h2 class="wp-block-heading">2020s: Multi-All-The-Things, AI, and beyond</h2>



<h3 class="wp-block-heading">Tokens in Figma</h3>



<p>Achieving elegant theming in design tools like Adobe Photoshop/Illustrator/XD, Sketch, and Figma has been elusive until very recently. Styles existed in tools like Sketch and Figma, but were insufficient for the level of customization digital orgs had to account for. In 2022, <a href="https://tokens.studio/">Tokens Studio</a> was released to help bring more sophisticated theming to Figma, and in the summer of 2023, Figma released <a href="https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma">Figma Variables</a>, a tool-native way to define and wield design tokens. This created the opportunity to design UIs that can more easily support multiple themes. The magic trick is really cool!</p>



<figure class="wp-block-video"><video controls="" src="https://bradfrost.com/wp-content/uploads/2025/08/ice-cream-theme-switch-figma-1.mp4"></video></figure>



<h3 class="wp-block-heading">The rise of AI and themeability on demand</h3>



<p>Of course, at the time of this writing, generative AI exploded onto the scene and is introducing a brand-new paradigm that will influence the world, including how to support themeable user interfaces. These tools are still emerging, but already we’ve found many ways AI tooling can help in the creation and adoption of design token systems (we get into all of it in <a href="https://designtokenscourse.com/">our course</a>!). It’s also easy to imagine that thes new technologies can help usher in a new era of hyper-personalized user experiences. My user experience may look and behave wildly differently than yours, which introduces all sorts of fascinating opportunities and challenges.</p>



<h3 class="wp-block-heading">Our Multi-All-The-Things Reality</h3>



<p>It’s been a hell of a journey that’s gotten us here, but we are here. We are alive in a moment where we’re responsible for designing and building for a <a href="https://bradfrost.com/blog/post/the-multi-all-the-things-organization/">Multi All-The-Things</a> reality. </p>



<figure class="wp-block-image size-large"><a href="https://bradfrost.com/blog/post/the-multi-all-the-things-organization/"><img loading="lazy" decoding="async" width="1024" height="575" src="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-9.52.15 PM-1024x575.png" alt="Multi-all-the-things: products, brands, frameworks, platforms, redesigns, rebrands, product families, color modes, subbrands, whitelabeling, campaigns" class="wp-image-24110" srcset="https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-9.52.15 PM-1024x575.png 1024w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-9.52.15 PM-700x393.png 700w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-9.52.15 PM-768x431.png 768w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-9.52.15 PM-1536x863.png 1536w, https://bradfrost.com/wp-content/uploads/2025/05/Screenshot-2025-05-12-at-9.52.15 PM-2048x1150.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></figure>



<p>We have more websites, apps, screens, flows, products, ecosystems, technologies, and paradigms than at any other moment in human history. And it doesn’t show any sign of slowing down anytime soon. </p>



<p>No one can predict the future, but <strong>I strongly feel that organizations that have sturdy foundations, infrastructure, and systems in place will be in a better place to navigate whatever the future may bring.</strong></p>



<h2 class="wp-block-heading">Consider checking out our design tokens course</h2>



<p><strong>There’s SO MUCH technology, architecture, tooling, cross-disciplinary collaboration, orchestration, and gold old-fashioned human processes involved in creating robust, successful, and themeable UI systems.</strong> Achieving balance in these systems is truly an art form: constrained-yet-expressive, systematic-yet-extensible, considered-yet-customizable. </p>



<p>We’ve devoted the last 12 years of our lives to helping teams establish these systems, and spent 6 months distilling all of these concepts, best practices, hard-earned lessons into a comprehensive online course called <a href="https://designtokenscourse.com/">Subatomic: The Complete Guide To Design Tokens</a>. </p>



<figure class="wp-block-image size-large"><a href="https://designtokenscourse.com/"><img loading="lazy" decoding="async" width="1024" height="576" src="https://bradfrost.com/wp-content/uploads/2025/07/subatomic-social-card-1024x576.jpg" alt="Subatomic: The Complete Guide To Design Tokens" class="wp-image-24522" srcset="https://bradfrost.com/wp-content/uploads/2025/07/subatomic-social-card-1024x576.jpg 1024w, https://bradfrost.com/wp-content/uploads/2025/07/subatomic-social-card-700x394.jpg 700w, https://bradfrost.com/wp-content/uploads/2025/07/subatomic-social-card-768x432.jpg 768w, https://bradfrost.com/wp-content/uploads/2025/07/subatomic-social-card.jpg 1200w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></figure>



<p>Our course covers everything your team needs to create &amp; maintain a successful themeable design systems at your organization. <a href="https://designtokenscourse.com/#order">Order our course</a> and you’ll get:</p>



<ul class="wp-block-list">
<li>Over 13 hours of in-depth video </li>



<li>Figma &amp; code sample architecture</li>



<li>Naming &amp; governance workflows</li>



<li>PDF slides with over 150 resources</li>



<li>Access to our Slack community</li>



<li>Certificate of completion</li>



<li>Free updates</li>
</ul>



<p>WHEW! I suppose this post is a history lesson slash infomercial for our course. But walking through this history, I’m struck by the fact that my personal journey on this Earth coincides with much of the history of themeable user interfaces. I am so incredibly grateful that I’m on this planet during such a time of technological innovation and convergence between technology, design, and art. I’ve now spent literally half of my life professionally designing cool-looking things for the World Wide Web. And I think that’s pretty badass.</p>
]]></description>
      <pubDate>Thu, 28 Aug 2025 17:46:10 +0000</pubDate>
      <link>https://bradfrost.com/blog/post/the-history-of-themeable-user-interfaces/</link>
      <dc:creator>Brad Frost</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4927477746</guid>
    </item>
    <item>
      <title><![CDATA[A custom --light-dark() function in CSS that works with any type of value (not just colors!) in just 3 LOC]]></title>
      <description><![CDATA[<p><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2025/09/custom-light-dark-with-color-scheme-bramus.png" alt="" width="560" height="413" class="alignnone size-medium wp-image-35730"></p>
<div class="intro">
<p>In <a href="https://brm.us/at-function-and-if">CSS <code>@function</code> + CSS <code>if()</code> = 🤯</a> I explored combining CSS custom functions and CSS <code>if()</code>, leading to the creation of a custom <code>--light-dark()</code> CSS function that works with any value <em>(not just colors!)</em>.</p>
<p>The built function relied on a style query that query a custom property <code>--scheme</code>, which itself was set to mirror the value of the <code>color-scheme</code> property.</p>
<p>Thanks to the recently resolved <code>color-scheme()</code> function, this indirection is no longer needed, and the code can become much, much simpler.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note note--warning">
<p><strong>⚠️ This post is about an upcoming CSS feature. You can’t use it … yet.</strong></p>
<p>While custom functions with <code>@function</code> and CSS <code>if()</code> are available in Chrome <em>(and only Chrome, for now)</em>, the mentioned <code>color-scheme()</code> only exists on paper right now.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="https://www.bram.us/2025/09/30/css-custom-light-dark/#the-code" name="the-code">#</a> The Code</h3>
<p>If you are here just for the code, here it is:</p>
<pre><code class="language-css" style="tab-size: 2">@function --light-dark(--l, --d) {
  result: if(color-scheme(dark): var(--d); else: var(--l));
}</code></pre>
<p>Usage is similar to the built-in <code>light-dark()</code>, but difference is that it can be used with <em>any</em> type of value:</p>
<pre><code class="language-css" style="tab-size: 2">#element {
  color-scheme: light dark;
  color: light-dark(#333, #e4e4e4);
  background-image: --light-dark(url(light.svg), url(dark.svg));
}</code></pre>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="https://www.bram.us/2025/09/30/css-custom-light-dark/#color-scheme-function" name="color-scheme-function">#</a> The <code>color-scheme()</code> function</h3>
<p>Powering this custom <code>--light-dark()</code> function is the new <code>color-scheme()</code> function. It’s a new addition to CSS which <a href="https://github.com/w3c/csswg-drafts/issues/10577#issuecomment-3329616811">we only recently resolved on adding</a> with the CSS Working Group.</p>
<p>The <code>color-scheme()</code> function allows you to query the <em>used</em> color scheme of an element. The function can be used in both <code>@container</code> queries and [the new <code>if()</code>](https://developer.chrome.com/blog/if-article).</p>
<p>In the following example I am using the function directly on an element <em>(instead of placing it into a custom function)</em>:</p>
<pre><code class="language-css" style="tab-size: 2">#element {
  color-scheme: light dark;
  background-image: if(color-scheme(dark): url(dark.svg); else: url(light.svg));
}</code></pre>
<p>The reason we need this new <code>color-scheme()</code> <em>function</em> is because the <code>color-scheme</code> <em>property</em> only lists which color schemes are supported. When set to <code>light dark</code>, you are indicating the element can adapt its rendering to either a light or dark version. This is controlled by your light/dark setting, with the resulting used value being one of those listed values.</p>
<p>FYI: You can also force a component into a specific mode by listing only 1 value for <code>color-scheme</code>, e.g. <code>color-scheme: light</code> will force the component into <code>light</code> mode, regardless of your OS setting.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="https://www.bram.us/2025/09/30/css-custom-light-dark/#spread-the-word" name="spread-the-word">#</a> Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3m23dckbyuk2x">🦋 BlueSky</a></li>
<li><a href="https://front-end.social/@bramus/115295054674239673">🦣 Mastodon</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on BlueSky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></description>
      <pubDate>Tue, 30 Sep 2025 20:03:56 +0000</pubDate>
      <link>https://www.bram.us/2025/09/30/css-custom-light-dark/</link>
      <dc:creator>Bram.us</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4963567963</guid>
    </item>
    <item>
      <title><![CDATA[The CSS Podcast is back! And I’m a co-host now.]]></title>
      <description><![CDATA[<p><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2025/10/TCP-Libsyn-Profile_Generic-560x560.jpg" alt="" width="560" height="560" class="alignnone size-medium wp-image-35757" srcset="https://www.bram.us/wordpress/wp-content/uploads/2025/10/TCP-Libsyn-Profile_Generic-560x560.jpg 560w, https://www.bram.us/wordpress/wp-content/uploads/2025/10/TCP-Libsyn-Profile_Generic-1120x1120.jpg 1120w, https://www.bram.us/wordpress/wp-content/uploads/2025/10/TCP-Libsyn-Profile_Generic-150x150.jpg 150w, https://www.bram.us/wordpress/wp-content/uploads/2025/10/TCP-Libsyn-Profile_Generic-768x768.jpg 768w, https://www.bram.us/wordpress/wp-content/uploads/2025/10/TCP-Libsyn-Profile_Generic-1536x1536.jpg 1536w, https://www.bram.us/wordpress/wp-content/uploads/2025/10/TCP-Libsyn-Profile_Generic-1568x1568.jpg 1568w, https://www.bram.us/wordpress/wp-content/uploads/2025/10/TCP-Libsyn-Profile_Generic.jpg 1984w" sizes="auto, (max-width: 560px) 100vw, 560px"></p>
<p><a href="https://thecsspodcast.libsyn.com/">The CSS Podcast</a> is back! Together with <a href="https://una.im/">Una</a> I’m co-hosting season 5 of the show, and we have some good episodes coming your way! The first episode of the season – episode 092 of the full show – got published and is about <a href="https://thecsspodcast.libsyn.com/92-css-if-and-custom-functions">CSS <code>if()</code> and CSS Custom Functions (<code>@function</code>)</a>.</p>
<blockquote><p>Welcome back to the new season of the CSS Podcast, where Una and Bramus are your guides, your cohosts, and your CSS best friends.</p>
<p>In this episode we dig into two very powerful new CSS features: inline conditionals with the <code>if()</code> function, and custom functions.</p></blockquote>
<p>It’s a bit of a bittersweet thing to do, because Una used to run the show together with <a href="https://nerdy.dev/">Adam</a> who <a href="https://www.bram.us/2025/04/14/anti-climax/">got laid off back in April</a>. Thankfully the three of us got to meet in person at <a href="https://cssday.nl/">CSS Day</a> back in June, where Adam gave us his blessing to continue the show.</p>
<p><a href="https://thecsspodcast.libsyn.com/">The CSS Podcast →</a><br>
<a href="https://thecsspodcast.libsyn.com/92-css-if-and-custom-functions">The CSS Podcast Episode 092 – →</a></p>
]]></description>
      <pubDate>Tue, 14 Oct 2025 09:18:58 +0000</pubDate>
      <link>https://www.bram.us/2025/10/14/the-css-podcast-is-back-and-im-a-co-host-now/</link>
      <dc:creator>Bram.us</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4979581155</guid>
    </item>
    <item>
      <title><![CDATA[Solved by CSS Scroll State Queries: hide a header when scrolling down, show it again when scrolling up.]]></title>
      <description><![CDATA[<figure><div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-35790-5" width="640" height="412" loop="" autoplay="" muted="" preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2025/10/scroll-state-scrolled.mp4?_=5"><a href="https://www.bram.us/wordpress/wp-content/uploads/2025/10/scroll-state-scrolled.mp4">https://www.bram.us/wordpress/wp-content/uploads/2025/10/scroll-state-scrolled.mp4</a></video></div><figcaption>Recording of <a href="https://codepen.io/bramus/pen/qEboVXG?editors=1100">the demo</a>, recorded in Chrome Canary.</figcaption></figure>
<div class="intro">
<p>There’s a new type of CSS scroll-state query coming: <code>scrolled</code></p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<p>Unlike <a href="https://developer.chrome.com/blog/css-scroll-state-queries#scrollable">the <code>scrollable</code> scroll-state queries</a>, <code>scrolled</code> remembers the last direction you scrolled into, which you can use to build “hidey bars”: when scrolling down (or having scrolled down), the hidey bar hides itself. When then scrolling back up, the hidey bar reveals itself.</p>
<p>Here’s the code that is needed to hide a fixed header when scrolling down:</p>
<pre><code class="lang-css">html {
  container-type: scroll-state;
}

header {
  transition: translate 0.25s;
  translate: 0 0;

  /* Slide header up when last having scrolled towards the bottom */
  @container scroll-state(scrolled: bottom) {
    translate: 0 -100%;
  }
}</code></pre>
<div class="update">
<p>Good suggestion <a href="https://front-end.social/@meduz@m.nintendojo.fr/115418937883458955">by meduz on Mastodon</a>, in case the header contains some interactive content that you can focus:</p>
<blockquote>
<p>Use <code>header:not(:focus-within)</code> to avoid hiding the bar if there’s focus in it.</p>
</blockquote>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<p>Below is a live demo using the code above. You can try it out yourself in Chrome Canary with the experimental Web Platform Features Flag enabled. If you browser does not support <code>scrolled</code> scroll-state queries, the header will remain fixed in place – a nice progressive enhancement if you’d ask me 🙂</p>
<p class="codepen" data-height="640" data-default-tab="result" data-slug-hash="qEboVXG" data-pen-title="Hidey Bar Demo (Hide on Scroll Down, Show on Scroll Up // Scroll State Queries)" data-user="bramus" style="height: 640px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/bramus/pen/qEboVXG"><br>
  Hidey Bar Demo (Hide on Scroll Down, Show on Scroll Up // Scroll State Queries)</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>)<br>
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><script async="" src="https://public.codepenassets.com/embed/index.js"></script></p>
<p>The feature is expected to ship to Chrome Stable in Chrome 144.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
<p>If that demo looks familiar: I featured it here on bram.us before, as a demo to <a href="http://brm.us/hidey-bar">use scroll-driven animations to track and remember the scroll direction</a>. Thanks to <code>scrolled</code> scroll-state queries, that hack is no longer needed 🙂</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></description>
      <pubDate>Wed, 22 Oct 2025 17:03:05 +0000</pubDate>
      <link>https://www.bram.us/2025/10/22/solved-by-css-scroll-state-queries-hide-a-header-when-scrolling-down-show-it-again-when-scrolling-up/</link>
      <dc:creator>Bram.us</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4989179648</guid>
    </item>
    <item>
      <title><![CDATA[Paxos accidentally mints more than twice the global GDP in PayPal stablecoins]]></title>
      <description><![CDATA[
        <img src="https://primary-cdn.web3isgoinggreat.com/entryImages/logos/resized/paxos_300.webp" alt="A group of yellow, green, and blue semi-translucent overlapping blobs with a circle cut out of the middle, followed by &quot;Paxos&quot; in grey capitals" width="300px">
        <p>Paxos, the issuer of PayPal's PYUSD stablecoin, accidentally minted 300 trillion of the supposedly dollar-pegged token. For context, this is approximately 2.5x the global GDP, and around 125x the total number of US dollars actually in circulation.</p><p>Paxos later announced that the mint was an "internal technical error", and that they had burned the excess tokens.</p><p>While PayPal promises its customers that "Reserves are held 100% in US dollar deposits, US treasuries and cash equivalents – meaning that customer funds are available for 1:1 redemption with Paxos," there clearly isn't much in the way of safeguards to ensure that is always the case. As with most stablecoin issuers, Paxos merely issues self-reported and unreviewed portfolio reports, and monthly third-party attestations (not audits) of reserves.</p><p></p>
        <ul>
          <li>
            <a href="https://x.com/whale_alert/status/1978539763301744815">
              Tweet by Whale Alert
            </a> 
          </li>
          <li>
            <a href="https://x.com/Paxos/status/1978565015943950411">
              Tweet by Paxos
            </a> 
          </li>
        </ul>
      ]]></description>
      <pubDate>Wed, 15 Oct 2025 21:35:35 +0000</pubDate>
      <link>https://web3isgoinggreat.com/single/paxos-accidental-mint</link>
      <dc:creator>Web3 is Going Just Great</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4981657971</guid>
    </item>
    <item>
      <title><![CDATA[Linked: In the Future All Food Will Be Cooked in a Microwave, and if You Can’t Deal With That Then You Need to Get Out of the Kitchen]]></title>
      <description><![CDATA[
    <blockquote>
<p><span style="font-size: 19.227997px; -webkit-text-size-adjust: 100%;">One of my chefs mentioned that if they could cook the steak on the grill they could get it right the first time. This is not an acceptable attitude in the microwave era. Chefs have fragile egos and they all seem to enjoy cooking (???) so it’s obvious they’re just too attached to the food. Also they’re worried I’m planning on firing all of them. That’s true but not relevant here.</span></p>
</blockquote>
    <p><a aria-label="Permalink to bookmark" rel="bookmark" title="Permalink to bookmark" href="https://calebhearth.com/linked/a405d33c-cace-4a9d-8700-af878d419b92">⬣</a></p>
  ]]></description>
      <pubDate>Wed, 24 Sep 2025 17:52:33 +0000</pubDate>
      <link>https://www.colincornaby.me/2025/08/in-the-future-all-food-will-be-cooked-in-a-microwave-and-if-you-cant-deal-with-that-then-you-need-to-get-out-of-the-kitchen/</link>
      <dc:creator>Hearthside by Caleb Hearth</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4956600748</guid>
    </item>
    <item>
      <title><![CDATA[Who needs a flying car when you have display: grid]]></title>
      <description><![CDATA[<p>A friend asked me if I could build him a simple website for his new Mechanical Engineering business: just a few pages showing off what they’re about and what they can do.&nbsp;</p>
<p>This is the sort of work I did when I was just starting out with web dev - marketing sites. It’s been 14 years since I worked a gig like this, and I was so struck by how much easier everything is now.&nbsp;</p>
<p>First of all, the capabilities of CSS are absolutely magical. I needed to <a href="https://css-for-js.dev/">do a course</a> to catch up on my knowledge and feel confident again but once I did… putting together layouts is a dream.&nbsp;</p>
<p>At one point my friend asked if I could change all the places where they had yellow to blue… back in the day this would have been annoying but all I had to do was update a single custom property! I made multiple Netlify Deploy previews for him to look at with different shades of blue and it all took me under 10 minutes. </p>
<p>And because of improvements to CSS and the native platform you need so much less JavaScript to achieve specific layouts and functionality. When you do need JS, you can just use the regular JavaScript API, no need for third party libraries like jQuery or MooTools (lol).</p>
<p>Look, I am old and I am comparing this to the Dark Ages when we were still on CSS 2, but I just can't move past how much better and easier everything is!</p>
<p>At the same time I’m cognisant of the fact this isn’t really a job anymore. My friend had no interest in messing with a Squarespace and was willing to pay my hourly rate to build a static site because he knew he could trust me to do a good job. Most clients probably feel differently, and would prefer to either choose these low or no-code tools to make the websites themselves, or employ devs to build bloated React projects.&nbsp;My own day job is building an incredibly complex Web Application. </p>
<p>What a shame that is, as the technology has never been better for developers to build lean, accessible and attractive websites.</p>

      <hr>
      <p>Thanks for reading this post via RSS! Let me know your thoughts by leaving a comment on the <a href="https://rachsmith.com/who-needs-a-flying-car">original post</a> or send <a href="mailto:contact@rachsmith.com?subject=re%3A%20Who%20needs%20a%20flying%20car%20when%20you%20have%20display%3A%20grid">me an email</a>.</p>
      ]]></description>
      <pubDate>Fri, 10 Oct 2025 00:45:37 +0000</pubDate>
      <link>https://rachsmith.com/who-needs-a-flying-car/</link>
      <dc:creator>Rach Smith&#39;s digital garden</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4974854457</guid>
    </item>
    <item>
      <title><![CDATA[Reblogging this here.

A lot of folks on this and other platforms ask if I’m still working on…]]></title>
      <description><![CDATA[<p><a class="tumblr_blog" href="https://jakewyattonline.tumblr.com/post/794690780648390656/a-necropolis-layout-from-last-year-i-still-work" target="_blank">jakewyattonline</a>:</p><blockquote><div class="npf_row"><figure class="tmblr-full" data-orig-height="1680" data-orig-width="2000"><img src="https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s640x960/4b8f03097c85a46c9d7adf3cdcc973be887d8a52.png" data-orig-height="1680" data-orig-width="2000" srcset="https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s75x75_c1/41e3370391a785854b6214c0d00f01f739904e1f.png 75w, https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s100x200/e6fc6d15407b9bfd44dd17a1271a5eef05678c04.png 100w, https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s250x400/68a1b9318ece689c9158a2cc22effaa686544d48.png 250w, https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s400x600/7c7c9e4e0a1a06661fb50d7247e2ade9f783e939.png 400w, https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s500x750/37227259572ce8b6b40ec20c7cf5b1b6a0e94634.png 500w, https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s540x810/9c23e5de91504ba27e27111018582083edc19798.png 540w, https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s640x960/4b8f03097c85a46c9d7adf3cdcc973be887d8a52.png 640w, https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s1280x1920/0dae04ca113d90c0c34a446c1dbaca9164555f60.png 1280w, https://64.media.tumblr.com/b974ae41d918daa333d29e3f3c4f8d48/feb3e486168aee98-40/s2048x3072/00867e5e89f2b39b3a1ed3dbd3d535e24335651a.png 2000w" sizes="(max-width: 1280px) 100vw, 1280px"></figure></div><p>A Necropolis layout from last year. I still work on Necropolis here and there, but anytime I start to pick up the thread, to find my way, the job pulls me out again. The struggle of making television has demanded almost all of my time since July 2020.</p><p><br></p><p>Getting to make cartoons with so many talented people has been a huge, all-consuming privilege. It’s taken me five years on the job just to get my legs under me, just to <i>begin</i> to feel that I know what I’m doing. It’s taken everything I have just to keep my head above water, to get <i>something</i> resembling our crew’s talent and effort and intention onto the screen. For that entire half-decade I have longed to make comics, to write stories for myself, to draw for myself. But there was always something that needed doing. </p><p><br></p><p>Recently I’ve realized that denying that part of myself, telling it to wait its turn through one more script revision, one more review, one more retake, one more episode, one more season–it’s taken a toll that I can no longer afford to pay. I don’t think I can keep going unless I take some time to write and draw and explore outside of the studio production system. As we fight through post on the third season of My Adventures With Superman and start writing the first season of Lantern, I’m looking for ways to work and lead that will get me some of that time back–and my teams are really coming through for me.</p><p><br></p><p>But even if I can find that time, I still need to decide how to spend it, and where to put the things I make. I need a place away from the urgency and deadlines and pressure of the studio. Tumblr has always had the best community, is where I’ve always been the most comfortable, so I’m gonna start here. I will cross-post some of what I do here to other, more hellish parts of the internet, but this is going to be home for a while.</p><p><br></p><p>I hope that those of you who never left don’t mind me coming back. I hope that those of you who’ve nested here in the meantime don’t mind some new-type Oldtype showing up. I just want a place to be myself, instead of another dusty piece of production equipment.</p><p>Thanks,</p><p>Jake</p></blockquote>
<p>Reblogging this here.<br><br>A lot of folks on this and other platforms ask if I’m still working on Necropolis. I am (see above), but <a href="https://www.tumblr.com/necropoliscomic/624470192557375489/necropolis-chapter-three-page-twenty-six-notice?source=share" target="_blank">what started as a short-term, long-shot tv development project</a> has become four seasons of <a href="https://www.youtube.com/watch?v=H8zqDd4bdmE&amp;ab_channel=AdultSwim" target="_blank">television</a>, now spread across two series and five years of my life.</p><p>I have been entirely subsumed by the job. But I’m trying to reconstitute myself.</p><p>I haven’t and won’t quit Necropolis, but before I can fully reengage it, I have to find my way back to making things that aren’t studio television. Once that’s done, I need to determine the correct angle of approach for Necropolis, specifically. And all that is just going to take time.</p><p>If you still care, thank you so much. Sincerely. I hope I’m able to continue (and eventually finish!) this story while you still do. I really like making this comic for you all. I would really like to see it done.</p><p>-Jake</p>]]></description>
      <pubDate>Sun, 14 Sep 2025 19:24:25 +0000</pubDate>
      <link>https://necropoliscomic.tumblr.com/post/794692540241772544</link>
      <dc:creator>Necropolis</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4945192753</guid>
    </item>
    <item>
      <title><![CDATA[Junior Dev Tip: "Scroll Up"]]></title>
      <description><![CDATA[<p>My junior developer just came to me asking for help because their linting setup was broken. I’m writing this very quickly because I want to get all the details down.</p>
<blockquote>
<p>Junior Dev: “My linting is broken, can you help?”<br>
Me: “Sure”<br>
<em>get on a call with junior dev<br>
junior dev runs linting command which ends with lots of “command failed” errors as well as a line that says “17 warnings, 2 errors”</em><br>
Junior Dev: “See?”<br>
Me: “Scroll up.”<br>
<em>junior dev scrolls up and sees 2 errors that say they forgot an attribute</em><br>
Junior Dev: “Oh.”</p>
</blockquote>
<p>I’ve had other experiences recently as well where a different junior developer also couldn’t tell me what was wrong with something. The error was right in front of them and they just didn’t scroll to the right place. They would click everywhere except for the thing that would tell them where the error was.  I recognize that as a junior developers, you’re not as familiar with the tools, but the tools do provide you with information most of the time. You genuinely just need to take a few extra seconds and read what it is saying.</p>
<p><em>Note: I did ask my junior developer if it was ok to share this story. they said yes.</em></p>]]></description>
      <pubDate>Wed, 08 Oct 2025 16:28:48 +0000</pubDate>
      <link>https://alex.party/posts/2025-10-08-junior-dev-tip-scroll-up/</link>
      <dc:creator>Alex.Party</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4973408530</guid>
    </item>
    <item>
      <title><![CDATA[Somewhere Between Lost and Found]]></title>
      <description><![CDATA[<p>A few years ago, I received news of a professor’s passing in the most bizarrely impersonal way we tend to receive news these days: via a twitter mention from a college ex I hadn’t spoken to in years. The memorial service was set for that weekend in Central Massachusetts where he and his family lived. At the time, I was living in Cambridge, which was just a short train ride away. But I never made it past Back Bay.</p>]]></description>
      <pubDate>Thu, 24 Jul 2025 13:23:09 +0000</pubDate>
      <link>https://shortdiv.com/posts/somewhere-between-lost-and-found/</link>
      <dc:creator>&lt;shortdiv /&gt;</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4890398226</guid>
    </item>
    <item>
      <title><![CDATA[DHH Is Way Worse Than I Thought]]></title>
      <description><![CDATA[<p>Have you ever known someone who seemed nice enough and perfectly normal, until you saw one of their social media accounts and realized they were insane?
Like, you became Facebook friends with your uncle, or followed that friend-of-a-friend who's fun at parties on Instagram, and it turns out they constantly post about weird shit like the deep state and demographic replacement and the pedophile ring that Hillary Clinton <em>definitely</em> runs from the basement of a pizza parlor?</p>
<p>Over the past couple weeks, the tech community has been slowly coming to terms with a prominent person like that.
He seems congenial — started a successful open source project, co-founded a reputable company — until you come across his blog filled with unhinged political diatribes.
I’m speaking, of course, of DHH: Ruby on Rails creator David Heinemeier Hansson.</p>
<p>If you, like me, don't pay much attention to this person, the last thing you might remember him from is the fracas a few years ago over his company Basecamp banning political discussions at work.
While I had <a href="https://jakelazaroff.com/words/what-counts-as-politics-in-the-workplace/">my opinions about that</a>, it seemed to fit within the general range of politics you can expect from most people.
I assumed David was just a normal guy with whom I had some political differences, and went on with my life.</p>
<p>That all changed when I heard about <a href="https://joel.drapper.me/p/rubygems-takeover/">the recent hostile takeover of the RubyGems package manager</a>, which appears to have started over a lost sponsorship for giving David a conference speaking slot.
My interest was piqued, so I checked out his recent post "As I remember London".
By the time I finished reading, my jaw was on the floor.</p>
<p><strong>DHH's politics are not normal.</strong></p>
<p>Maybe they used to be, I don't know, but as of right now the dude is <em>way the fuck outside</em> of what most people would consider moral or acceptable.</p>
<p>But don't take my word for it.
We can get it straight from the horse’s mouth.
Let’s go through David’s "<a href="https://web.archive.org/web/20250925050154/https://world.hey.com/dhh/as-i-remember-london-e7d38e64">As I remember London</a>"<sup class="footnote-ref"><a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fn1" id="fnref1">[1]</a></sup> post and see exactly what he’s all about.</p>
<h3>Native Brits</h3>
<p>David's post starts off fairly anodyne:</p>
<blockquote>
<p>As soon as I was old enough to travel on my own, London was where I wanted to go. Compared to Copenhagen at the time, there was something so majestic about Big Ben, Trafalgar Square, and even the Tube around the turn of the millenium. Not just because their capital is twice as old as ours, but because it endured twice as much, through the Blitz and the rest of it, yet never lost its nerve. I thought I might move there one day.</p>
</blockquote>
<p>Yeah, man.
I have cousins not too far away from there, so even though I live across the pond I've been lucky enough to visit a few times.
London is great!</p>
<blockquote>
<p>That was then. Now, I wouldn't dream of it. London is no longer the city I was infatuated with in the late '90s and early 2000s. Chiefly because it's no longer full of native Brits. In 2000, more than sixty percent of the city were native Brits. By 2024, that had dropped to about a third. A statistic as evident as day when you walk the streets of London now.</p>
</blockquote>
<p>The honeymoon is over: Big Ben and Trafalgar Square are only majestic if enough passersby are “native Brits”.</p>
<p>That’s a little vague, but he links "native Brits" to a Wikipedia article called "<a href="https://en.wikipedia.org/wiki/Ethnic_groups_in_London">Ethnic groups in London</a>" so we can see exactly whom he’s talking about:</p>
<blockquote>
<p>Greater London had a population of 8,899,375 at the 2021 census. Around 41% of its population were born outside the UK, and over 300 languages are spoken in the region.</p>
</blockquote>
<p>59% of Londoners were born in the UK!
How could it possibly be that only a third of them are native Brits?</p>
<p>The article’s first section breaks down the demographic data in a table.
The first ethnicity listed?
“White British” at 36.8% as of the 2021 census.</p>
<p>Ah.</p>
<p>As for other ethnic groups: the table rolls up “Asian or Asian British” at 20.8%, “Black or Black British” at 13.5%, “Mixed or British Mixed” at 5.7% and “Other” at 6.3%.<sup class="footnote-ref"><a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fn2" id="fnref2">[2]</a></sup>
No other group is <em>even remotely close</em> to a third.</p>
<p>It turns out that <strong>when DHH says “native Brits”, he’s specifically referring to <em>white</em> Brits.</strong>
That's why it's "a statistic as clear as day when you walk the streets of London": it's his coy way of saying that too many of the 59% of Londoners <em>born and raised in the UK</em> are not white.</p>
<p>So if David means "white Brits", why doesn't he just say that?
Why bother with the innuendo?</p>
<p><em>Because complaining that there aren’t enough white people sounds weird and racist!</em>
David bristles at that label, but there's a reason he's hiding behind euphemisms rather than just saying what he means.
Most people don't go around thinking “boy, all these Black and Asian people make this city so much worse.”</p>
<p>Most people, that is, except for David:</p>
<blockquote>
<p>But I think, what would Copenhagen feel like, if only a third of it was Danish, like London? It would feel completely foreign, of course. Alien, even. So I get the frustration that many Brits have with the way mass immigration has changed the culture and makeup of not just London, but their whole country.</p>
</blockquote>
<p>He thinks that a city that has too many Black people feels “completely foreign”.
That it’s “alien” to see too many Asian people as he walks the streets.
David tries to throw "mass immigration" in there — but as we know, his problem with the "culture and makeup" is <em>how many people are not white</em>, whether or not they're immigrants.</p>
<h3>Unite the Kingdom</h3>
<p>David continues:</p>
<blockquote>
<p>That frustration was on wide display in Tommy Robinson's march yesterday. British and English flags flying high and proud, like they would in Copenhagen on the day of a national soccer match. Which was both odd to see but also heartwarming. You can sometimes be forgiven for thinking that all of Britain is lost in self-loathing, shame, and suicidal empathy. But of course it's not.</p>
</blockquote>
<p>Who's Tommy Robinson?
According to his <a href="https://en.wikipedia.org/wiki/Tommy_Robinson">Wikipedia entry</a>, he’s an “anti-Islam campaigner and one of the UK's most prominent far-right activists with a history of criminal convictions”.</p>
<p>Not a great start!
But maybe Wikipedia just has a left wing bias?<sup class="footnote-ref"><a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fn3" id="fnref3">[3]</a></sup></p>
<p>Well…</p>
<ul>
<li>He’s described himself — verbatim — as <a href="https://www.newsweek.com/tommy-robinson-edl-pegida-uk-423623">opposed to Islam</a>.</li>
<li>He promised to retaliate against — also verbatim — <a href="https://hopenothate.org.uk/2017/05/22/tommy-robinson-far-right-islamophobic-extremist/"><em>every single Muslim</em> in response to a terrorist attack</a>.</li>
<li>He called for the blanket deportation of — you guessed it, verbatim! — <a href="https://www.adl.org/resources/article/tommy-robinson-five-things-know"><em>every adult male Muslim</em> who recently immigrated to the entire EU</a>.</li>
</ul>
<p>How about the march he organized?
HOPE not hate reported on <a href="https://hopenothate.org.uk/2025/09/13/britains-biggest-far-right-protest-more-than-100000-attend-tommy-robinsons-unite-the-kingdom-rally/">what the speakers he invited had to say</a>.</p>
<blockquote>
<p>“It’s not just Britain that is being invaded, it’s not just Britain that is being raped. Every single Western nation faces the same problem: an orchestrated, organised invasion and replacement of European citizens is happening.”</p>
</blockquote>
<p>That one’s Tommy Robinson himself.</p>
<blockquote>
<p>The Dutch far-right commentator Eva Vlaardingerbroek delivered one of the day’s most incendiary speeches, appearing in a t-shirt emblazoned with the words “Generation Remigration”. She said:</p>
<p>“They are demanding the sacrifice of our children on the altar of mass migration. Let’s not beat about the bush — this is the rape, replacement, and murder of our people… Remigration is possible, and it’s up to us to make it happen. We are Generation Remigration.”</p>
</blockquote>
<p>I had to look up the word "remigration".
It means <a href="https://en.wikipedia.org/wiki/Remigration">"ethnic cleansing via the mass deportation of non-white immigrants and their descendants, sometimes including those born in Europe, to their place of racial ancestry"</a>.</p>
<blockquote>
<p>“This is a religious war,” said Brian Tamaki, leader of New Zealand’s Destiny Church. “Islam, Hinduism, Baháʼí, Buddhism — whatever else you’re into — they’re all false. We’ve got to clean our countries up. Get rid of everything that doesn’t receive Jesus Christ. Ban any public expression of other religions in our Christian nations. Ban halal. Ban burqas. Ban mosques, temples, shrines — we don’t want those in our countries.”</p>
</blockquote>
<p>I mean… these people are clearly deranged, right?
You'd think any of this would warrant at least a passing mention, but for some reason David doesn't include a single quote about what the people at this "heartwarming" march actually said.</p>
<p>David is well aware that these people are extremists.
That's why he tries to preempt that accusation:</p>
<blockquote>
<p>The easy way out of this uncomfortably large gathering of perfectly normal, peaceful Brits who've had enough is to tar them all as "far right". That's not just a British tactic, but one used across Europe, and previously in the US as well. It used to work very well, because the historical stigma was so strong, but, like hurling "nazi" and "fascist" at the most middle-of-the-road political figures and positions, it's finally lost its power.</p>
</blockquote>
<p>Note that David never actually addresses the "far right" label on its merits — he just pivots to calling it overused, trying to direct your attention elsewhere like a magician distracting the audience as he performs a trick.
We are meant to believe him that because people sometimes use “far right” and “nazi” and “fascist” too liberally, that must be happening here as well.</p>
<p>But of course, that’s <em>not</em> what’s happening here.
Calling these people far right is "easy" for the same reason it's "easy" to say Joe Biden is liberal: <em>it's obviously true</em>!
These are not "middle-of-the-road" positions — they're literally calling to ban non-Christian religions and to ethnically cleanse non-white citizens.
It takes <em>no stretch of the imagination</em> to figure out why these people are far right.</p>
<h3>Demographic Replacement</h3>
<p>Let's say you wanted to trick me into believing a conspiracy theory.</p>
<p>You'd have to start with a grain of truth, right?
You can't come out of the gate with the COVID vaccine nano-chips that Bill Gates uses to track us through the 5G cell towers.<sup class="footnote-ref"><a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fn4" id="fnref4">[4]</a></sup>
That'd scare me off!</p>
<p>No, the first step is to find some common ground.
Something we can both agree on.
<em>Then</em> you can slowly mix in the crazy stuff.</p>
<p>That in mind, let's continue with David’s post:</p>
<blockquote>
<p>I really feel for the Brits because it's not obvious how they get themselves out of this pickle. They're still reeling from the Pakistani rape gangs that were left free to terrorize cities like Rotherham and Rochdale for years on end with horror-movie-like scenes of the most despicable, depraved abuse of British girls.</p>
</blockquote>
<p>The child sexual abuse scandals were real and horrible.
The perpetrators were mostly British-Pakistani, and the victims were largely white.
No one is disputing that; it's the grain of truth.</p>
<p>But like any good con artist, David has mixed in some other not-quite-so-true things he wants you to believe as well.</p>
<p>For one: David <em>really</em> wants to make sure you know that the perpetrators were largely Pakistani: <em>scary brown foreigners</em>.
He’s insinuating that there’s some connection between their ethnicity and sexually abusing children.
It's not just that many of these abusers <em>happened</em> to be Pakistani; David's implying they did it <em>because</em> they were Pakistani.
(Many of them were also British — but as we know by now, in David’s eyes that only counts if you're white.)</p>
<p>When it comes to the <em>victims</em>, though, David brings out the dog whistles.
He describes them as "British girls" (read: white).
"Barbaric outsiders preying upon innocent white women" is a classic racist trope that would be perfectly at home in <a href="https://jimcrowmuseum.ferris.edu/brute/homepage.htm">the Jim Crow South</a> or <a href="https://www.epoch-magazine.com/post/the-jewish-man-and-the-aryan-woman-in-nazi-propaganda">Nazi Germany</a>.</p>
<blockquote>
<p>I don't know. But I'm glad that there clearly are many Brits who are determined to find out. Unwilling to just let their society wither away while their bobbies chase bad tweets instead of the rampant street thefts or those barbaric rape gangs. Unwilling to resign the rest of the country to the kind of demographic replacement that befell London over the last two decades.</p>
</blockquote>
<p>On top of the "barbaric rape gangs", David also brings up "rampant street thefts" — suggesting that the same <em>scary brown foreigners</em> are responsible.
The problem is that his implication is only backed up by bigotry.
The <a href="https://www.standard.co.uk/news/crime/mobile-phone-theft-london-met-police-b1244212.html">source he links to about the street thefts</a>, for example, never mentions race or ethnicity.
And in spite of the salacious rape gangs story, <a href="https://www.csacentre.org.uk/research-resources/research-evidence/scale-nature-of-abuse/trends-in-official-data/">data show that non-white people in the UK are ever-so-slightly <em>less likely</em> to commit child sexual abuse</a>.</p>
<p>After planting the grain of truth and making lurid insinuations, David finally gets to the crazy stuff.
"Demographic replacement”: a reference to <a href="https://www.pbs.org/newshour/politics/what-is-great-replacement-theory-and-how-does-it-fuel-racist-violence">a debunked conspiracy theory that there’s a plot to replace white people in Western society</a>.
It's the same thing that motivated the deadly Charlottesville Unite the Right<sup class="footnote-ref"><a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fn5" id="fnref5">[5]</a></sup> march's infamous chant: "<a href="https://www.adl.org/unite-right-rallies">you will not replace us!</a>"</p>
<h3>“Far Right”</h3>
<p>David ends with a quote from Danish Prime Minister Mette Frederiksen of the Social Democrats.
Someone, he says, that “nobody could credibly charge with being ‘far right’”:</p>
<blockquote>
<p>There are really a lot of us Danes who believed that when people came to this ‘world’s best country’ and were given such good opportunities, they would integrate. They would become Danish, and they would never, ever harm our society. All of us who thought that way have been wrong.</p>
</blockquote>
<p>Notice how moderate her words are compared to what David says and supports!
Frederiksen is not saying that her country is being "invaded" or "raped", for example.
She's not calling for it to be ethnically cleansed, or accusing of foreign men of being predators.</p>
<p>This is a running theme for David.
He is <em>desperately</em> trying to convince you that he is not "far right", his people are not "far right", his politics are not "far right".
Probably because – for all his bluster about how the label has lost its power — David knows that it's actually a huge red flag.</p>
<p>Personally, I don't think the label matters.
I've been calling these people "far right" because it's convenient and accurate, not because I'm invested in that particular term.
Shit by any other name would smell as foul, and David and his friends are extremely pungent.</p>
<p>Let's ditch the superlatives and review David's post objectively:</p>
<ul>
<li>He thinks that <em>even if you were born in the UK</em>, you only count as British if you're white.</li>
<li>He wouldn't consider living in London <em>specifically because</em> it has too many people of color.</li>
<li>He uses racist tropes to accuse Asian men of being dangerous predators who attack white women.</li>
<li>He pushes debunked conspiracy theories about immigrants replacing white people.</li>
<li>He finds a march where speakers called for banning all non-Christian religions and ethnically cleansing immigrants "heartwarming".</li>
<li>Finally — and maybe most alarmingly — he argues that all of the above is normal and not extreme.</li>
</ul>
<p>You can use whatever word you want to describe all that.
But if you, like me, didn't realize that <em>this is who DHH is</em>, we can probably agree that he's way worse than we thought.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>I'm linking to the archive.org page rather than directly to his site to avoid giving it any more Google juice. <a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>The rest are non-British subcategories of "White”, which come in at a cumulative 17%. <a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>More like <em>Wokipedia</em>, amiright? <a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>This is <a href="https://www.bbc.com/news/53191523">a real thing people believe</a>. <a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>You might notice the name is similar to Tommy Robinson's "Unite the Kingdom" march, which I am skeptical is a coincidence. <a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/#fnref5" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></description>
      <pubDate>Thu, 02 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/</link>
      <dc:creator>jakelazaroff.com</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4965710333</guid>
    </item>
    <item>
      <title><![CDATA[Make accessible carousels]]></title>
      <description><![CDATA[How the features in CSS Overflow 5 can help you create more accessible carousel patterns.]]></description>
      <pubDate>Mon, 29 Sep 2025 07:00:00 +0000</pubDate>
      <link>https://developer.chrome.com/blog/accessible-carousel?hl=en</link>
      <dc:creator>developer.chrome.com: Blog</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4963036196</guid>
    </item>
    <item>
      <title><![CDATA[Should Men Be the Head of Every Household?]]></title>
      <description><![CDATA[<img src="https://images.unsplash.com/photo-1536704231234-beca9772ca68?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDExfHx0aGUlMjBiaWJsZXxlbnwwfHx8fDE3NTcwMDkxNzd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="Should Men Be the Head of Every Household?"><p>Mark Driscoll, the main subject of <a href="https://www.christianitytoday.com/podcasts/the-rise-and-fall-of-mars-hill/?ref=chrisenns.com">The Rise and Fall of Mars Hill podcast</a> and all around awful human being, continues to <a href="https://www.threads.com/@markdriscoll/post/DOJj3aigK48?ref=chrisenns.com">be a troll for God online</a>:</p><figure class="kg-card kg-image-card"><a href="https://www.threads.com/@markdriscoll/post/DOJj3aigK48?ref=chrisenns.com"><img src="https://chrisenns.com/content/images/2025/09/CleanShot-2025-09-03-at-17.20.48@2x.png" class="kg-image" alt="Should Men Be the Head of Every Household?" loading="lazy" width="1236" height="208" srcset="https://chrisenns.com/content/images/size/w600/2025/09/CleanShot-2025-09-03-at-17.20.48@2x.png 600w, https://chrisenns.com/content/images/size/w1000/2025/09/CleanShot-2025-09-03-at-17.20.48@2x.png 1000w, https://chrisenns.com/content/images/2025/09/CleanShot-2025-09-03-at-17.20.48@2x.png 1236w" sizes="(min-width: 720px) 720px"></a></figure><p>Scott Baker, a self-professed theologian and professor, <a href="https://www.threads.com/@thecachinnator/post/DOKGO5LEYAg?xmt=AQF0W0lIm4-_2eWTubJlXcbF_wKpycGk6q4XFJrW7WYM-Q&amp;ref=chrisenns.com">posted the following response in this thread</a>, which I'll quote below:</p><blockquote>Hey, friends! Theologian here. Mark is wrong. About nearly everything, yes, but specifically here. There’s no such thing as “head of household” in a Christian marriage. That’s Rome, not Jesus. We can talk a little about it!&nbsp;<br><br>The Bible is rife with patriarchy, but that patriarchy is neither commanded nor taught. It was simply the way of the world. In much the same way as the Bible deals with slavery, it does not call for an end to the institution within its pages, but it provides the theological guidance and imperatives that will lead to its abolition. The desire to preserve it - like Mark has - is evil akin to wanting to reinstate slavery.<br><br>Now this slip that the patriarchalists like to do, using the phrase “head of household,” is both deliberate and sinister. The phrase “head of household” does not appear in the Bible. Period. Don’t tell me it does, I can read in multiple languages. In Ephesians 5, Paul writes that the husband is the head of the wife, but Paul uses the term “head” to mean source, the way we still use it in “headwater.”<br><br>One way to prove that is to point out every use of the word by Paul being used in that metaphorical sense except when he’s talking abut physiology.  Another is to simply point out that the head (or brain) being responsible for cognitive and executive function was not widely-known in the 1st century. Making a whole metaphor based on information Paul is unlikely to have had - let alone his audience - is nonsensical, and wholly unnecessary unless you’re using it as a proof text.<br><br>Believing that Paul’s use of the word “head” = “patriarchy” is unnecessary and far more complicated than acknowledging the simple fact that the word does not denote authority or leadership. Back to “head of household.”It’s not in the Bible, and the nearest cognate to it is saying something completely different. They know. They know that “head of household” is a more familiar term, so they elude it with “head” from Ephesians, and, voila, they claim the Bible teaches patriarchy.&nbsp;<br><br>You know who does use the phrase “head of household?” Like… a lot? <a href="https://en.wikipedia.org/wiki/Family_in_ancient_Rome?ref=chrisenns.com">Rome. It’s all over their laws. It means not just authority, but *absolute* authority.</a> I am hard-pressed to cite a better example of Scripture being used to say the same exact opposite of what it actually says.<br><br>So, no, Mark. The Bible does not teach men to be the “head of household.”I’m not even going to bother with the dumb bit about Satan. That’s just Dork being a mark… or… whatever. There is a still more excellent way.&nbsp;</blockquote><p><em>Note: I added </em><a href="https://en.wikipedia.org/wiki/Family_in_ancient_Rome?ref=chrisenns.com"><em>the link above to the Wikipedia page on Rome and family structures.</em></a></p><h2 id="gotcha-suckers-%F0%9F%8E%AF">Gotcha suckers! 🎯</h2><p>My point in linking to this thread is not because I believe Scott Baker to be 100% accurate or the final argument against men as the head of a household. Or as a big gotcha to "fight the patriarchy" in a progressive way. But to show how a phrase that you might have heard over and over like "men should be the head of the household" might not be as Bible based as some folks claim it is.  </p><p>It might be something that's done in your faith tradition, or culture, or your specific family. But since there are also a lot of theologians who say it's not necessarily biblical, I think it's worth unpacking what the actual origins of that idea are.</p><p>See also Sheila Way Gregoire's post "<a href="https://substack.com/home/post/p-172866714?ref=chrisenns.com">In the Case of Ties, He Wins</a>" for a deeper look at the impact the phrase can have on marriages:</p><blockquote>...after four different surveys, of just about 40,000 primarily evangelical respondents, looking into their marital and sexual quality,&nbsp;I can tell you very strongly that these different expectations have real-world consequences.</blockquote><h2 id="its-a-slippery-slope-from-this-to-%F0%9F%98%B1">It's a slippery slope from this to... 😱</h2><p>Slippery slope arguments are often, but not always, a <a href="https://www.scribbr.com/fallacies/slippery-slope-fallacy/?ref=chrisenns.com">slippery slope towards a fallacy</a>.</p><p>Digging into an idea such as "men as head of household" shouldn't shake your faith in God or Jesus, but it should cause you to question what people are teaching if they throw around phrases like "men should be the head of the household" as if it were words spoken directly by Jesus. I'd suggest that it's worth investigating whether things like that are actually in the Bible, or if it's just something that's been repeated at you enough that you assume it's 100% true. </p><p>You can even continue to believe that men must be the head of every household if you and your household are ok with that. But don't say "...because it's biblical..." as justification for it.</p><h2 id="what-about-the-replies-%F0%9F%A4%A3">What about the replies? 🤣</h2><p>Predictably, Driscoll's post has stirred up a lot of conversation. But more importantly, a lot of good jokes poking fun at the idea:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@johnstone.gregory/post/DOJ0jdpkXId?xmt=AQF0GhgF4ttlkSwIvuf8st11Jd91y4FYW23uxdmVNOavtw&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Greg Johnstone (@johnstone.gregory) on Threads</div><div class="kg-bookmark-description">I wouldn’t call her that to her face mate</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-12.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/338720288_729317825533151_8529865467402915127_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@matthew_podszus/post/DOMtrWHgEyM?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Matthew Podszus (@matthew_podszus) on Threads</div><div class="kg-bookmark-description">Men, if you’re not the commissioner of your fantasy football league, Satan is!</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-2.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/358354306_3516838191902254_7971465821450300097_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@deric_cahill/post/DOLrff7jum_?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Deric Cahill (@deric_cahill) on Threads</div><div class="kg-bookmark-description">Satan and I are co-leading, thank you very much.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-3.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/357786862_1460504591351314_1236057075709075083_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@godless_mom/post/DOKeK3hkZkH?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Courtney Heard (@godless_mom) on Threads</div><div class="kg-bookmark-description">No, sweetie, Satan isn’t in charge. Mommy just asked you to do the dishes for once. I can totally see, tho, how you might confuse “shared responsibilities” with “demonic possession.”</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-4.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/424963179_1422562171800499_5479011086497191214_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@jrbillin/post/DOMqA9-DsWq?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Dr. Jennifer Billinson (@jrbillin) on Threads</div><div class="kg-bookmark-description">There are no men in my household, it’s 100% all Satan all the time</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-5.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/540472184_17925353922102121_6497668006045213408_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@thelesliegaar/post/DONF9tjjIHx?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Leslie Gaar (@thelesliegaar) on Threads</div><div class="kg-bookmark-description">Look if Satan takes out the trash and remembers to put the seat down, we have a deal.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-6.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/446111579_1164401737919164_4621257121041250157_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@statementteesandthings/post/DOJ0WNEEu0I?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Statement Tees &amp; Things™ (@statementteesandthings) on Threads</div><div class="kg-bookmark-description">When men think like you do, Satan already is the man of the house.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-7.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/328210417_3315642682043230_528900117872687604_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@rowanjetteknox/post/DOKfqT6DQ1e?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Rowan Jetté Knox (@rowanjetteknox) on Threads</div><div class="kg-bookmark-description">Amazing. Satan can pay my bills then.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-8.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/515082002_17914149771110454_3560518555295642471_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@wigglemachine/post/DOOTLOmDdgx?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">JB (@wigglemachine) on Threads</div><div class="kg-bookmark-description">As the only person in my household, I guess that makes me the Satan 👿</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-9.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/358346640_263371319762667_1153635844893073905_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@jon.mce/post/DOMrkZlEVXt?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Jon McEwen (@jon.mce) on Threads</div><div class="kg-bookmark-description">Mark sometimes you gotta let go and let Satan take the wheel.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-10.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/501346750_17909896137109807_362669913748433851_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.threads.com/@jeremywest15/post/DONDkODkft2?xmt=AQF0dAp0_fzceNpnshRyNAz8h9oxTZOmH2-TSwjpzHt_RQ&amp;ref=chrisenns.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Jeremy West (@jeremywest15) on Threads</div><div class="kg-bookmark-description">Satan and I are roommates and run the house as equals, thank you very much!</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://chrisenns.com/content/images/icon/Pcnemah90K8-11.png" alt="Should Men Be the Head of Every Household?"><span class="kg-bookmark-author">Threads</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://chrisenns.com/content/images/thumbnail/496879838_17878126872321853_8279740683335295736_n.jpg" alt="Should Men Be the Head of Every Household?" onerror="this.style.display = 'none'"></div></a></figure>]]></description>
      <pubDate>Fri, 05 Sep 2025 19:09:27 +0000</pubDate>
      <link>https://chrisenns.com/2025/09/should-men-be-the-head-of-every-household/</link>
      <dc:creator>Faraway, So Close</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4935942281</guid>
    </item>
    <item>
      <title><![CDATA[Impact of AI on Tech Content Creators]]></title>
      <description><![CDATA[
<p><a href="https://syntax.fm/show/922/pre-commit-hooks-requestanimationframe-code-reviews-and-more#t=25:16">Wes on Syntax</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>I write content. That content is consumed by people. But a lot of it has been used to train AIs for people to get a very quick answer. You can see the amount of bots visiting websites has been going up significantly. You ask a question about JavaScript and they go suck in 40 pages and it distills it down. From a user perspective I love it. I don’t want to read your life story, I want to get straight to the answer. How often do you just read the Google Summary and just close the tab?</p>



<p>For those people that [create content] for money, that business is going to be significantly disrupted. What happens to the people that rely on that money? There’s no shortage of people putting content on the internet right now. Will that stop? I don’t know.</p>
</blockquote>



<p>It’ll stop when there is no longer any incentive to do so. Those incentives are various! Money is one. That was a <em>partial</em> motivator for me. Being able to support myself and my family partially through advertising on content was important. But I also found it to be fun and mentally rewarding, like it gave my life purpose. Even if the money wanes, those incentives may endure for many. </p>



<p>Promoting other things can be another incentive. If you can still manage to get eyes on you, the value doesn’t have to come from traditional advertising. It could be because you’re working for a company, and there is value in the DevRel. You could be taking pre-orders for your next book. You could offer a training course for sale. Your superfans can pay for superchats and supermemberships on your Twitch or whatever.</p>



<p>As long as there are incentives left to create technical content, people will. And AIs will continue to train on it. That does frame it as an adversarial relationship, which is a bummer (something something capitalism). </p>



<p>Wes <a href="https://syntax.fm/show/922/pre-commit-hooks-requestanimationframe-code-reviews-and-more#t=27:20">specifically wondered about me!</a></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>He spent a good chunk of his life. His legacy was putting out very helpful information. CSS-Tricks is a huge swath of information. He was able to that because he was able to make money. The next CSS-Tricks isn’t going to be able to make that much money. The AIs are just going to gobble it up and contribute to our brain rot. </p>
</blockquote>



<p>I certainly wrote a lot of content for that site. And so did a <em>ton</em> of <a href="https://css-tricks.com/authors/">other authors</a>, who still do to this day. And AIs have slurped and increasing reslurp it up. My main concerns with the AI-slurp-age are:</p>



<ol class="wp-block-list">
<li>It’s <a href="https://chriscoyier.net/2023/04/21/the-secret-list-of-websites/">rude</a>. Nobody asked me (or the other authors) if that’s OK. Now that’s the bar and they never will.</li>



<li>The business model is akin to <a href="https://chriscoyier.net/2024/10/21/a-penny/">stealing pennies</a>.</li>



<li>AI interfaces are incentivized to <em>not</em> credit sources or link out. They want you to think that they are the brain, and you need not go anywhere else. </li>
</ol>



<p>I’m <em>slightly</em> less concerned that AI slurpage will disincentivize all content creation. Humans love other humans, and we’ll always want to connect with each other. We want to learn together and laugh together and play together, and, as weird as it is, sharing technical content with each other is a niche in there. </p>



<p>Wes wondered if I “got out” at the right time. I sorta think I did. It was not premeditated, though. At the time, I was much more focused on advertising. For years leading up to the sale, I invested more and more money into the site with the goal of growth, only to see traffic stay flat. It wasn’t perfectly correlated, but flat traffic doesn’t help advertising revenue. Ramping up the amount of work for the same traffic and same money wasn’t feeling great. At the time, I assumed it was just a temporary slump, but now with enough distance, it kinda wasn’t. Fortunately, Digital Ocean didn’t really need the advertising, which is why I thought it was a perfect buyer. They had other incentives. I have no idea what they think of the purchase now, but I would hope it’s quite positive. There was a weird slump, but with <a href="https://geoffgraham.me/">Geoff</a> still over there, I think they are doing awesome.</p>



<p>I feel compelled to mention that my content creation career is far from over and takes many forms:</p>



<ul class="wp-block-list">
<li><em>This</em> site has loads of blog posts.</li>



<li>The <a href="https://blog.codepen.io/">CodePen blog</a> has loads of blog posts, and the <a href="https://blog.codepen.io/radio/">PODCAST IS BACK</a>! Not to mention: THE ENTIRE CODEPEN is a massive trove of content, by far the biggest I’ve been a part of building. I’ve written a ton of public code there, which is like 0.000001% of what is there.</li>



<li>CSS-Tricks had lots of writing, but also video content. </li>



<li>I’ve guest-posted all over the place.</li>



<li>I’ve got GitHub repos. </li>



<li>I <a href="https://frontendmasters.com/blog/author/chriscoyier/">write at Frontend Masters</a>, which is my CSS-Tricks energy replacement, and whose business model around content is perfect.</li>



<li><a href="https://shoptalkshow.com/">ShopTalk Show</a> has over 10 years of weekly podcasts.</li>



<li>I’ve always been at least moderately active on the most appropriate social media platforms throughout my entire career.</li>
</ul>



<p>So. Much. Content. </p>



<p>I still think it’s fun and has value and plan to continue doing it, even if the incentives around doing it are constantly being battered down. I’ll need to continue to evolve how I get value out of it. I do enjoy it so much I’ll probably be explaining <code>border-radius</code> tricks into the dirt with a stick after the apocalypse. </p>



<p>Wes also said of AI:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>As a user, I love it.</p>
</blockquote>



<p>I feel that, too. </p>



<p>AI companies do slimy shit, and <em>they know it</em>. But I don’t wanna just take my ball and go home. </p>



<p>There is some real user benefit coming out of AI products right now. It’s fun to be a part of. I’m experiencing genuine productivity boosts from using AI for coding work. The evolution of user interfaces around it is fascinating.</p>



<p>Perhaps ironically, there is an awful lot of user content <em>about</em> AI — lolz.</p>
]]></description>
      <pubDate>Wed, 23 Jul 2025 15:16:23 +0000</pubDate>
      <link>https://chriscoyier.net/2025/07/23/impact-of-ai-on-tech-content-creators/</link>
      <dc:creator>Chris Coyier</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4889597948</guid>
    </item>
    <item>
      <title><![CDATA[The $200 Yamaha]]></title>
      <description><![CDATA[
<p><a href="https://daverupert.com/2025/07/always-buy-the-yamaha/">Dave Rupert</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Talk to any guitarist you know who has been playing awhile and they’ll have a story about a $200 Yamaha and how good it sounds relative to the price.</p>
</blockquote>



<p>Boy do I. I own a $5,000 guitar (what I originally paid for straight-grain Brazilian Rosewood Martin replica by Dennis Overton) and <a href="https://chriscoyier.net/2020/01/03/the-case-of-the-missing-guitar-and-the-idiot-who-didnt-even-realize-it-was-missing/">I didn’t even notice it was gone for <em>years</em></a><em>.</em> Now that’s mostly me being an idiot and not playing guitar as my primary instrument, but still, it’s very telling a cheap ass Yamaha was to me an un-noticable difference. I wrote then:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>I still feel like an idiot for not realizing for so long. Honestly, that little $150 Yamaha sounds pretty damn good if you ask me.</p>
</blockquote>
]]></description>
      <pubDate>Wed, 30 Jul 2025 22:08:39 +0000</pubDate>
      <link>https://chriscoyier.net/2025/07/30/the-200-yamaha/</link>
      <dc:creator>Chris Coyier</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4896536032</guid>
    </item>
    <item>
      <title><![CDATA[Danger Gently]]></title>
      <description><![CDATA[
<p><a href="https://www.daringentry.com/danger-gently.html">Danger Gently</a> is the name of the band I occasionally get a seat in here in lovely Bend, Oregon. </p>



<p>We played at the High Desert Museum the other week for their “Art in the West” event. </p>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-9d6595d7 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
		<figure class="wp-block-jetpack-videopress jetpack-videopress-player" style="">
			<div class="jetpack-videopress-player__wrapper"> <iframe title="VideoPress Video Player" aria-label="VideoPress Video Player" width="421" height="750" src="https://videopress.com/embed/8aKdDS6W?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0" frameborder="0" allowfullscreen="" data-resize-to-parent="true" allow="clipboard-write"></iframe><script src="https://v0.wordpress.com/js/next/videopress-iframe.js?m=1739540970"></script></div>
			
			
		</figure>
		</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
		<figure class="wp-block-jetpack-videopress jetpack-videopress-player" style="">
			<div class="jetpack-videopress-player__wrapper"> <iframe title="VideoPress Video Player" aria-label="VideoPress Video Player" width="421" height="750" src="https://videopress.com/embed/qidltQ93?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0" frameborder="0" allowfullscreen="" data-resize-to-parent="true" allow="clipboard-write"></iframe><script src="https://v0.wordpress.com/js/next/videopress-iframe.js?m=1739540970"></script></div>
			
			
		</figure>
		</div>
</div>



<p>We play at The Cellar every Wednesday night (I make it to as many as I can). Here’s a couple of tunes from a couple weeks ago that Jason Chinchen shot:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="“Cotton Eye Joe” OTAF Fiddle Tune" width="500" height="281" src="https://www.youtube.com/embed/0j2psRguM-k?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
</div><figcaption class="wp-element-caption">I was on mandolin here.</figcaption></figure>



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



<p>Sometimes we busk, typically in downtown Bend. One night I brought my camera to catch the band doing their thing:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="Chilly Winds (Danger Gently - Downtown Bend - Sep 12, 2025)" width="500" height="281" src="https://www.youtube.com/embed/KfH3Zrl_GUw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
</div></figure>



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



<p>Here’s a few grabs from when I’ve gotten to join:</p>



<figure class="wp-block-embed is-type-rich is-provider-instagram wp-block-embed-instagram"><div class="wp-block-embed__wrapper">
<blockquote class="instagram-media" data-instgrm-captioned="" data-instgrm-permalink="https://www.instagram.com/reel/DPDB9dmiVoB/?utm_source=ig_embed&amp;utm_campaign=loading" data-instgrm-version="14" style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:500px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"><div style="padding:16px;"> <a href="https://www.instagram.com/reel/DPDB9dmiVoB/?utm_source=ig_embed&amp;utm_campaign=loading" style=" background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%;" target="_blank"> <div style=" display: flex; flex-direction: row; align-items: center;"> <div style="background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;"></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 100px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;"></div></div></div><div style="padding: 19% 0;"></div> <div style="display:block; height:50px; margin:0 auto 12px; width:50px;"><svg width="50px" height="50px" viewBox="0 0 60 60" version="1.1" xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(-511.000000, -20.000000)" fill="#000000"><g><path d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"></path></g></g></g></svg></div><div style="padding-top: 8px;"> <div style=" color:#3897f0; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:550; line-height:18px;">View this post on Instagram</div></div><div style="padding: 12.5% 0;"></div> <div style="display: flex; flex-direction: row; margin-bottom: 14px; align-items: center;"><div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);"></div> <div style="background-color: #F4F4F4; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;"></div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);"></div></div><div style="margin-left: 8px;"> <div style=" background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;"></div> <div style=" width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #f4f4f4; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)"></div></div><div style="margin-left: auto;"> <div style=" width: 0px; border-top: 8px solid #F4F4F4; border-right: 8px solid transparent; transform: translateY(16px);"></div> <div style=" background-color: #F4F4F4; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);"></div> <div style=" width: 0; height: 0; border-top: 8px solid #F4F4F4; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);"></div></div></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center; margin-bottom: 24px;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 224px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 144px;"></div></div></a><p style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;"><a href="https://www.instagram.com/reel/DPDB9dmiVoB/?utm_source=ig_embed&amp;utm_campaign=loading" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;" target="_blank">A post shared by Chris Coyier (@chriscoyier)</a></p></div></blockquote><script async="" src="//platform.instagram.com/en_US/embeds.js"></script>
</div></figure>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1022" height="1024" data-attachment-id="12813" data-permalink="https://chriscoyier.net/2025/10/01/danger-gently/img_5797/" data-orig-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/IMG_5797.jpeg?fit=1320%2C1323&amp;ssl=1" data-orig-size="1320,1323" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;1&quot;}" data-image-title="IMG_5797" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/IMG_5797.jpeg?fit=300%2C300&amp;ssl=1" data-large-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/IMG_5797.jpeg?fit=1022%2C1024&amp;ssl=1" src="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/IMG_5797.jpeg?resize=1022%2C1024&amp;ssl=1" alt="A band performing on the back of a truck at night, with instruments like a guitar, violin, and double bass visible. The setting includes streetlights and surrounding trees." class="wp-image-12813" srcset="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/IMG_5797.jpeg?resize=1022%2C1024&amp;ssl=1 1022w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/IMG_5797.jpeg?resize=300%2C300&amp;ssl=1 300w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/IMG_5797.jpeg?resize=150%2C150&amp;ssl=1 150w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/IMG_5797.jpeg?resize=768%2C770&amp;ssl=1 768w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2025/10/IMG_5797.jpeg?w=1320&amp;ssl=1 1320w" sizes="auto, (max-width: 1000px) 100vw, 1000px"><figcaption class="wp-element-caption">Playing in the back of Darin’s truck was the best.</figcaption></figure>



<p>We played a show at The Silver Moon during Bend Roots Revival and the sound guy recorded and sent us his “Board Mix” and it sounds pretty good to me! I was also on mandolin in this show.</p>



<figure class="wp-block-audio"><audio controls="" src="https://chriscoyier.net/wp-content/uploads/2025/10/Danger_Gently_BoardMix_Amplified.mp3"></audio></figure>



<p>We also played a show at River’s Place last month and since Dale Atkin’s was playing and brought his nice PA, we recorded from that as well. Here’s our opening tune “Breaking up Christmas” from that show:</p>



<figure class="wp-block-audio"><audio controls="" src="https://chriscoyier.net/wp-content/uploads/2025/10/01-Breaking-up-Christmas.mp3"></audio></figure>
]]></description>
      <pubDate>Wed, 01 Oct 2025 21:44:00 +0000</pubDate>
      <link>https://chriscoyier.net/2025/10/01/danger-gently/</link>
      <dc:creator>Chris Coyier</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4965021280</guid>
    </item>
    <item>
      <title><![CDATA[Clap on the off beat]]></title>
      <description><![CDATA[
<p>Clapping on the on-beat sounds weird and wrong on (most?) songs. In (most?) 4/4 songs, that means clapping on the 1 and 3 sounds bad and 2 and 4 sounds good/normal. </p>



<p>But an audience of a bunch of random folks just getting excited can get it wrong! </p>



<p>This video of Harry Connick Jr. extending a bar just one extra beat to adjust the audience to clapping on the correct beat is extremely friggin cool.</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="friends don't let friends clap on 1 and 3." width="500" height="281" src="https://www.youtube.com/embed/mI-CU2VTVic?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
</div></figure>



<p>(via <a href="https://www.alanwsmith.com/">Alan Smith</a>)</p>
]]></description>
      <pubDate>Wed, 08 Oct 2025 15:41:22 +0000</pubDate>
      <link>https://chriscoyier.net/2025/10/08/clap-on-the-off-beat/</link>
      <dc:creator>Chris Coyier</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4973210640</guid>
    </item>
    <item>
      <title><![CDATA[A letter to myself: Strategies]]></title>
      <description><![CDATA[<ol>
<li>When you put something in the oven and need to remember to take it out, or put washing in the machine and need to remember to hang it out, or need to do anything else time-sensitive like take medication, set a reminder for it. And not just any alarm or reminder, the kind that <a href="https://www.dueapp.com/">bugs you over and over</a> until you do it.</li>
<li>When packing for family trips, use your lists. Keep the lists updated. Refer back to the lists every time you pack. Don't assume it will be fine because you've done it 20 times before. You <em>will</em> forget to pack yourself underwear or worst of all, one of the 5 sleep aids your child needs to fall asleep.</li>
<li>Any important appointment/meeting needs 4 alerts: the day before, the morning of, one hour before and when you need to get in the car/get on the Zoom. Especially on work days, when you are most likely to get sucked in to a focus in which you forget everything else in your life.</li>
</ol>
<p>Do you recall all those years you were a hot mess? Life was a series of chaotic mistakes and mishaps until out of necessity (having dependants) you developed an extensive set of strategies. The strategies work well, but are annoying to manage, so you are sometimes tempted in to thinking you're a fully functional adult now who can "just remember" to do important things. This is folly, and will only lead to disappointment and burnt potatoes.</p>

      <hr>
      <p>Thanks for reading this post via RSS! Let me know your thoughts by leaving a comment on the <a href="https://rachsmith.com/strategies">original post</a> or send <a href="mailto:contact@rachsmith.com?subject=re%3A%20A%20letter%20to%20myself%3A%20Strategies">me an email</a>.</p>
      ]]></description>
      <pubDate>Sun, 05 Oct 2025 04:55:58 +0000</pubDate>
      <link>https://rachsmith.com/strategies/</link>
      <dc:creator>Rach Smith&#39;s digital garden</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4968897048</guid>
    </item>
    <item>
      <title><![CDATA[Design Dialects: Breaking the Rules, Not the System]]></title>
      <description><![CDATA[
					<!-- wp:paragraph -->
<p><em>"Language is not merely a set of unrelated sounds, clauses, rules, and meanings; it is a totally coherent system bound to context and behavior."</em> — Kenneth L. Pike</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The web has accents. So should our design systems.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading"><strong>Design Systems as Living Languages</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Design systems aren't component libraries—they’re living languages. <a href="https://m3.material.io/foundations/design-tokens/overview"><strong>Tokens</strong></a> are phonemes, <a href="https://atlassian.design/components"><strong>components</strong></a> are words, <a href="https://designsystem.university/glossary#pattern"><strong>patterns</strong></a> are phrases, <a href="https://www.figma.com/blog/design-systems-102-how-to-build-your-design-system/#use-layout-grids-and-spacing-to-create-visual"><strong>layouts</strong></a> are sentences. The conversations we build with users become the stories our products tell.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>But here’s what we've forgotten: the more fluently a language is spoken, the more accents it can support without losing meaning. English in Scotland differs from English in Sydney, yet both are unmistakably English. The language adapts to context while preserving core meaning. This couldn’t be more obvious to me, a Brazilian Portuguese speaker, who learned English with an American accent, and lives in Sydney.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Our design systems must work the same way. Rigid adherence to visual rules creates brittle systems that break under contextual pressure. Fluent systems bend without breaking.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Consistency becomes a prison</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The promise of design systems was simple: consistent components would accelerate development and unify experiences. But as systems matured and products grew more complex, that promise has become a prison. <a href="https://adobe.design/stories/design-for-scale/designing-design-systems-supporting-implementation-and-adoption">Teams file “exception” requests by the hundreds</a>. Products launch with workarounds instead of system components. Designers spend more time defending consistency than solving user problems.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Our design systems must learn to speak <strong>dialects</strong>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>A design dialect is a systematic adaptation of a design system that maintains core principles while developing new patterns for specific contexts.</strong> Unlike one-off customizations or brand themes, dialects preserve the system’s essential grammar while expanding its vocabulary to serve different users, environments, or constraints.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading"><strong>When Perfect Consistency Fails</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>At <a href="https://booking.com">Booking.com</a>, I learned this lesson the hard way. We A/B-tested everything—color, copy, button shapes, even logo colors. As a professional with a graphic design education and experience building brand style guides, I found this shocking. While everyone fell in love with Airbnb’s pristine design system, Booking grew into a giant without ever considering visual consistency.&nbsp;&nbsp;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The chaos taught me something profound: <strong>consistency isn’t ROI; solved problems are.</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>At Shopify. Polaris (<a href="https://polaris-react.shopify.com/">https://polaris-react.shopify.com/</a>) was our crown jewel—a mature design language perfect for merchants on laptops. As a product team, we were expected to adopt Polaris as-is. Then my fulfillment team hit an “Oh, Ship!” moment, as we faced the challenge of building an app for warehouse pickers using our interface on shared, battered Android scanners in dim aisles, wearing thick gloves, scanning dozens of items per minute, many with limited levels of English understanding.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Task completion with standard Polaris: <strong>0%</strong>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Every component that worked beautifully for merchants failed completely for pickers. White backgrounds created glare. 44px tap targets were invisible to gloved fingers. Sentence-case labels took too long to parse. Multi-step flows confused non-native speakers.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>We faced a choice: abandon Polaris entirely, or <em>teach it to speak warehouse</em>.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading"><strong>The Birth of a Dialect</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We chose evolution over revolution. Working within Polaris’s core principles—clarity, efficiency, consistency—we developed what we now call a <strong>design dialect</strong>:</p>
<!-- /wp:paragraph -->

<!-- wp:table -->
<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td><strong>Constraint</strong></td><td><strong>Fluent Move</strong></td><td><strong>Rationale</strong></td></tr><tr><td>Glare &amp; low light</td><td>Dark surfaces + light text</td><td>Reduce glare on low-DPI screens</td></tr><tr><td>Gloves &amp; haste</td><td>90px tap targets (~2cm)</td><td>Accommodate thick gloves</td></tr><tr><td>Multilingual</td><td>Single-task screens, plain language</td><td>Reduce cognitive load</td></tr></tbody></table></figure>
<!-- /wp:table -->

<!-- wp:paragraph -->
<p><strong>Result:</strong> Task completion jumped from <strong>0% to 100%</strong>. Onboarding time dropped from three weeks to one shift.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>This wasn’t customization or theming—this was a <strong>dialect</strong>: a systematic adaptation that maintained Polaris’s core grammar while developing new vocabulary for a specific context. Polaris hadn’t failed; it had learned to speak warehouse.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading"><strong>The Flexibility Framework</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>At <a href="http://www.atlassian.com">Atlassian</a>, working on the Jira platform—itself a system within the larger Atlassian system—I pushed for formalizing this insight. With dozens of products sharing a design language across different codebases, we needed systematic flexibility so we built directly into our ways of working. The old model—exception requests and special approvals—was failing at scale.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>We developed the <strong>Flexibility Framework</strong> to help designers define how flexible they wanted their components to be:</p>
<!-- /wp:paragraph -->

<!-- wp:table -->
<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td><strong>Tier</strong></td><td><strong>Action</strong></td><td><strong>Ownership</strong></td></tr><tr><td><strong>Consistent</strong></td><td>Adopt unchanged</td><td>Platform locks design + code</td></tr><tr><td><strong>Opinionated</strong></td><td>Adapt within bounds</td><td>Platform provides smart defaults, products customize</td></tr><tr><td><strong>Flexible</strong></td><td>Extend freely</td><td>Platform defines behavior, products own presentation</td></tr></tbody></table></figure>
<!-- /wp:table -->

<!-- wp:paragraph -->
<p>During a navigation redesign, we tiered every element. Logo and global search stayed Consistent. Breadcrumbs and contextual actions became Flexible. Product teams could immediately see where innovation was welcome and where consistency mattered.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading"><strong>The Decision Ladder</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Flexibility needs boundaries. We created a simple ladder for evaluating when rules should bend:</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Good:</strong> Ship with existing system components. Fast, consistent, proven.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Better:</strong> Stretch a component slightly. Document the change. Contribute improvements back to the system for all to use.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Best:</strong> Prototype the ideal experience first. If user testing validates the benefit, update the system to support it.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The key question: “Which option lets users succeed fastest?”</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Rules are tools, not relics.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading"><strong>Unity Beats Uniformity</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Gmail, Drive, and Maps are unmistakably Google—yet each speaks with its own accent. They achieve unity through shared principles, not cloned components. One extra week of debate over button color costs roughly $30K in engineer time.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Unity is a brand outcome; <strong>fluency is a user outcome.</strong> When the two clash, side with the user.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading"><strong>Governance Without Gates</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>How do you maintain coherence while enabling dialects? Treat your system like a living vocabulary:</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Document every deviation</strong> – e.g., dialects/warehouse.md with before/after screenshots and rationale.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Promote shared patterns</strong> – when three teams adopt a dialect independently, review it for core inclusion.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Deprecate with context</strong> – retire old idioms via flags and migration notes, never a big-bang purge.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>A living dictionary scales better than a frozen rulebook.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading"><strong>Start Small: Your First Dialect</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Ready to introduce dialects? Start with one broken experience:</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>This week:</strong> Find one user flow where perfect consistency blocks task completion. Could be mobile users struggling with desktop-sized components, or accessibility needs your standard patterns don’t address.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Document the context:</strong> What makes standard patterns fail here? Environmental constraints? User capabilities? Task urgency?</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Design one systematic change:</strong> Focus on behavior over aesthetics. If gloves are the problem, bigger targets aren’t “"breaking the system”"—they’re serving the user. Earn the variations and make them intentional.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Test and measure:</strong> Does the change improve task completion? Time to productivity? User satisfaction?</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><strong>Show the savings:</strong> If that dialect frees even half a sprint, fluency has paid for itself.</p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading"><strong>Beyond the Component Library</strong></h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We’re not managing design systems anymore—<strong>we’re cultivating design languages.</strong> Languages that grow with their speakers. Languages that develop accents without losing meaning. Languages that serve human needs over aesthetic ideals.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The warehouse workers who went from 0% to 100% task completion didn’t care that our buttons broke the style guide. They cared that the buttons finally worked.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Your users feel the same way. Give your system permission to speak their language.</p>
<!-- /wp:paragraph -->				]]></description>
      <pubDate>Fri, 26 Sep 2025 16:48:12 +0000</pubDate>
      <link>https://alistapart.com/article/design-dialects-breaking-the-rules-not-the-system/</link>
      <dc:creator>A List Apart: The Full Feed</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4959083786</guid>
    </item>
    <item>
      <title><![CDATA[Motomuzi Off-Road Camper Van Concept]]></title>
      <description><![CDATA[<div style="float: left; margin: 0 15px 15px 0; padding: 0; border: 1px solid #000000;"><a href="https://uncrate.com/motomuzi-off-road-camper-van-concept/" rel="bookmark">





    
    



		<img src="https://uncrate.com/assets_c/2025/09/motomuzi-camper-concept-1-thumb-960xauto-185787.jpg" class="t webfeedsFeaturedVisual" alt="Motomuzi Off-Road Camper Van Concept" title="Motomuzi Off-Road Camper Van Concept" width="960">
	</a></div>Motomuzi's off-road camper van concept is sleek on the inside but has a military vibe on the exterior.<br><br>Visit <a href="https://uncrate.com/motomuzi-off-road-camper-van-concept/">Uncrate</a> for the full post.]]></description>
      <pubDate>Fri, 26 Sep 2025 19:00:01 +0000</pubDate>
      <link></link>
      <dc:creator>Uncrate</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4959267426</guid>
    </item>
    <item>
      <title><![CDATA[Matt Webb: “Gin &amp; tonic was invented by the East India Company...]]></title>
      <description><![CDATA[
        <a href="https://interconnected.org/home/2025/09/19/filtered">Matt Webb</a>: “<a href="https://en.wikipedia.org/wiki/Gin_and_tonic">Gin &amp; tonic was invented by the East India Company</a> to keep its colonising army safe [from malaria] in India. Bet the covid vaccine would have been more popular if it got you drunk.”

        

        

         <p>💬 <a href="https://kottke.org/25/09/0047550-matt-webb-gin-tonic">Join the discussion on kottke.org</a> →</p>

    ]]></description>
      <pubDate>Fri, 19 Sep 2025 20:11:41 +0000</pubDate>
      <link>https://kottke.org/25/09/0047550-matt-webb-gin-tonic</link>
      <dc:creator>kottke.org</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/4951236091</guid>
    </item>
  </channel>
</rss>
