<?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/a22c4101980b055d688e90512b083e8d.xml" rel="self" type="application/rss+xml"/>
    <link>https://feedbin.com/</link>
    <item>
      <title><![CDATA[Evaluating a Success Criterion]]></title>
      <description><![CDATA[<figure class="right">
 <img loading="lazy" src="https://adrianroselli.com/wp-content/uploads/2026/08/holmes-w3c.jpg" alt="Gouache and watercolor drawing of a white man in a deer stalker cap and long coat leans forward in a private train car seat, pointing to papers in his hand, while across from him sits a white man in a suit, frock coat, and derby, leaning back to listen while holding a pipe." width="494" height="494" class="alignnone size-full wp-image-51411" srcset="https://adrianroselli.com/wp-content/uploads/2026/08/holmes-w3c.jpg 494w, https://adrianroselli.com/wp-content/uploads/2026/08/holmes-w3c-300x300.jpg 300w, https://adrianroselli.com/wp-content/uploads/2026/08/holmes-w3c-150x150.jpg 150w, https://adrianroselli.com/wp-content/uploads/2026/08/holmes-w3c-200x200.jpg 200w" sizes="(max-width: 494px) 100vw, 494px">
 <figcaption>Sidney Paget illustration of Sherlock Holmes from <cite>Strand Magazine</cite>, December 1892, p.646, but I added papers from his March 1893 (p.281 of <cite>Strand Magazine</cite>) illustration and then two versions of the W3C logo to the front page.</figcaption>
</figure>
<!-- Silver Blaze -->
<!-- The Adventure of the Stockbroker's Clerk -->
<!-- https://www.bestofsherlock.com/ref/black-peter-christies-2014.htm -->

<p>
When performing a WCAG evaluation (or review or audit), it’s easy to get caught up in the intent versus meaning of a particular Success Criterion. Especially when that SC was written before the interface pattern you’re poking was conceived.
</p>

<p>
Until, and even when, you have built your own (potentially localized) references for how to apply an SC to whatever situation, it’s best to have a plan for how to interpret the uninterpretable.
</p>

<h2 id="Steps">Steps</h2>

<p>
This is my progressive series of steps for getting the W3C’s most current (and soon-to-be-current) position on any SC.
</p>

<ol>
 <li>Read the W3C’s normative text (the “official” requirements) in the <a href="https://www.w3.org/TR/WCAG22/">WCAG spec</a>. 
  <ul>
   <li>Always look at the latest, which is WCAG 2.2 at the time of this writing.</li>
   <li>Make sure you’re at a w3.org site, not some scammy wcag.com or similar domain.</li>
  </ul>
 </li>
 <li>Read the <a href="https://www.w3.org/WAI/WCAG22/Understanding/">W3C Understanding document</a> (the supporting material) for the SC you’re evaluating.
  <ul>
   <li>These often have linked references in the form of…</li>
  </ul>
 </li> 
 <li><a href="https://www.w3.org/WAI/WCAG22/Techniques/">W3C Techniques documents</a> collect known failures along with passing and bonus methods.
  <ul>
   <li>Some are out of date or refer to technologies that have been superseded or failed to work as hoped, so be cautious.</li>
   <li>A single Technique document might apply to multiple SCs.</li>
  </ul>
 </li>
 <li>Look at <a href="https://github.com/w3c/wcag/issues">WCAG GitHub issues</a>. 
  <ul>
   <li>Pay attention to dates, linked issues &amp; PRs, embedded meeting minutes, and decisions.</li>
   <li>Look for the closed issues, as those are often decided answers (the issues that aren’t from spambots).</li>
  </ul>
 </li>
 <li>Look at <a href="https://github.com/w3c/wcag/pulls">WCAG GitHub pull requests</a>, even if they never merge. 
  <ul>
   <li>Pay attention to dates, linked issues &amp; PRs, and embedded meeting minutes.</li>
   <li>Look for the closed PRs, as the ones that <em>didn’t</em> merge often have good discussions</li>
  </ul>
 </li>
 <li>Look at <a href="https://github.com/w3c/wcag/discussions">GitHub WCAG discussions</a>, few though there are.
  <ul>
   <li>If you find an unanswered one that maps to your question, maybe comment.</li>
  </ul>
 </li>
 <li>Look at personal blogs of trusted experts, some of whom are often those working on the specs and who give their own insights.
  <ul>
   <li>Be cautious of blogs promoting an agenda or arguing for an SC interpretation without supporting evidence. <a href="https://youtu.be/sFDQUd_CjDE">Pat goes into detail in a 2025 talk</a> and<a href="https://patrickhlauke.github.io/wcag-interpretation/"> more recent slides</a>.</li>
   <li>This applies to those justifying the bare minimum to pass an SC via loopholes or gaps, in some cases as adversarial or malicious conformance. <a href="https://adrianroselli.com/tag/adversarial">I have a few posts on that</a>.</li>
  </ul>
 </li>
</ol>

<p>
No matter what outcome you find, test everything anyway. New releases are constant, regressions common, new support happens, and user expectations shift.
</p>

<p>
It’s possible you find that a W3C answer to a failure condition creates a worse experience when remediated (<a href="https://adrianroselli.com/2026/04/wcag3-contrast-as-of-april-2026.html">orange</a>). At which point, all the research you’ve done will make it easier to justify if you need to break from the W3C position.
</p>


]]></description>
      <pubDate>Tue, 01 Sep 2026 13:47:16 +0000</pubDate>
      <link>https://adrianroselli.com/2026/09/evaluating-a-success-criterion.html</link>
      <dc:creator>Adrian Roselli</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5360666245</guid>
    </item>
    <item>
      <title><![CDATA[CSS is Still Awesome]]></title>
      <description><![CDATA[<figure>
    <img src="https://jasonsantamaria.com/media/pages/blog/css-is-still-awesome/f7a86e214c-1788193132/grid.jpg" alt="Photo of a wall with two rows of four evenly spaced rectangluar frames. Large, bright, colorful paint strokes overlap everything at freeform angles.">

    <figcaption class="img-caption">
    Making and breaking grids  </figcaption>
  </figure>
<p>I’ve been doing more front-end coding this year than I have in a while and every time I come back to write markup and CSS, it feels like there are new tricks. We’re closing in on the 30th anniversary of CSS (already a little past 30 years since Håkon Wium Lie proposed it) and it still feels like actual magic.</p><p>When I was first making websites (I’m just skipping over the part where I put a year on that, thanks), I used tables for layout and a pile of spacer gifs. Then I would make them look nice with the little CSS I knew. I was mostly slapping things together back then, not fully understanding semantics, page weight, or the cascade. In time I learned how to do things properly from Eric Meyer’s <a href="https://meyerweb.com/eric/books/css-tdg/" target="_blank"><em>Definitive Guide</em></a>, and <a href="https://simplebits.com/about/" target="_blank">multiple CSS books</a> by Dan Cederholm. I typed <code>color: red;</code> and a thing turned red. No compiling or build steps. It was declarative and made me feel like a wizard (it was a much lower bar for wizardry back then).</p><p>Over the years since then, and especially in the last decade-ish, CSS has exploded! Flexbox, grid, many kinds of viewport units, variables, and on and on. So many things we used to patch in to extend CSS or turn to Javascript for are now just part of the spec. The ease with which you can create a flexible type scale or size elements and spacing responsively with <code>clamp</code>… the screen has really matured into its own unique design medium with tailored affordances that aren’t just translations from another medium (print). CSS finally gave the web <a href="https://frankchimero.com/blog/2015/the-webs-grain/" target="_blank">its own grain</a>.</p><p>And the real crazy part is that my bad <em>decades-old</em> code still renders fine in today’s latest web browsers! I would be sad if it didn’t, but I also wouldn’t be surprised. How is this stuff still backwards compatible!?</p><p>How much of your software from 20 years ago runs on your devices today? I realize CSS is actually “software”, but it is a kind of user interface for making stuff. Most things in tech get worse as time goes on, filled with upsells and ads, or sparkle icons to try out some rad new agent surreptitiously shoved into an app. CSS gets better <em>with age and</em> as more features are added. That’s bananas!</p><p>Of course this is because of wonderful people continually making it better. Like <a href="https://rachelandrew.co.uk/" target="_blank">Rachel Andrew</a>, <a href="https://www.miriamsuzanne.com/" target="_blank">Miriam Suzanne</a>, <a href="https://jensimmons.com/" target="_blank">Jen Simmons</a>, <a href="https://lea.verou.me/" target="_blank">Lea Verou</a>, and folks on the <a href="https://www.w3.org/groups/wg/css/" target="_blank">CSS Working Group</a> who continue refining and expanding the CSS spec, and many others at companies making the browsers that run our code.</p><p>Though, I’ve got my eye on some browsers blithely <a href="https://wil.to/posts/googles-prompt-api/" target="_blank">bellyflopping</a> into <a href="https://techcrunch.com/2026/02/02/firefox-will-soon-let-you-block-all-of-its-generative-ai-features/" target="_blank">enshittification</a>. I know many things are shit in the world right now, and there is a lot in tech making outsized contributions to that. But CSS continues to be a surprising light to me, and I want to celebrate something incredible that keeps improving because people care. CSS is a goddamned marvel.</p><hr><p><a href="mailto:jasonsantamaria@gmail.com?subject=Reply+to%3A+CSS+is+Still+Awesome">Reply to this post via email</a></p>]]></description>
      <pubDate>Mon, 31 Aug 2026 16:20:00 +0000</pubDate>
      <link>https://jasonsantamaria.com/blog/css-is-still-awesome</link>
      <dc:creator>Jason Santa Maria</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5359737430</guid>
    </item>
    <item>
      <title><![CDATA[The story of this blog so far: 'AI', the death of web dev, and feeling like an outsider]]></title>
      <description><![CDATA[<hr>
<p>This is the second of two parts on media creation. You can find the first part over here: <a href="https://www.baldurbjarnason.com/2026/deciding-what-to-do/"><em>Creating media with an uncooperative mind</em></a></p>
<hr>
<h2 id="infinite-negatives" tabindex="-1">Infinite negatives <a class="header-anchor" href="https://www.baldurbjarnason.com/2026/the-story-of-this-blog-so-far/#infinite-negatives">#</a></h2>
<p>In trying to figure out what I should write and talk about, I could spend the rest of my life outlining all the topics I <em>don’t</em> care about. The world is larger than our attention can encompass.</p>
<p>Even if I limited myself to going over the topics you might expect somebody like me – based on the public image I’ve chosen to project online – to reasonably cover but won’t, that’d still require me to write more words on subjects I dislike than on the ones I like.</p>
<p>This blog has always been a practical venue. I had blogs before – I’ve been writing online in one place or another since the mid- to late nineties – but this was the first place I specifically used to position myself for a chosen career.</p>
<p>I’d focused on ebooks for my PhD, given up on them after graduation because they seemed dead in the water in 2006.</p>
<p>Then as soon as I settled into a web dev job, ebooks finally became a thing with the Kindle, iPhone, and competitors all popping into the market from 2007 onwards. So, after waffling about for a couple of years, watching from the outside as the topic I’d studied for my PhD finally took off, I decided to make a venue where I could, effectively, put up a sign saying:</p>
<p>“Are you interested in ebooks and publishing? I know a bunch of stuff about that! Have degrees and everything.”</p>
<p>This website was born. First as a straight website, as I was mostly blogging on Tumblr in those days, but later I turned it into proper blog, with an RSS feed and everything.</p>
<p>As the ebook side of the publishing industry went through its own bubble and bust, with the inevitable fraud, collusion, and antitrust convictions (because this was back when cheating the public occasionally had consequences) I tried to reposition myself more on the crossroads between publishing and web development.</p>
<p>As I became a senior web developer, the gap between the understanding of most managers I’d dealt with and the realities of software development became more and more obvious and acute. I figured I might be able to help bridge that gap by writing specifically to try and explain how uniquely software development interacts with management practices and theory. That way I could target two different audiences with a single topic:</p>
<ol>
<li>Software developers who both needed to explain to their managers how their work, well, <em>worked</em> and needed to better understand what their managers were dealing with.</li>
<li>Managers who found themselves managing software projects but didn’t necessarily have visibility into some of the unique problems with software development.</li>
</ol>
<p>That led to the book <a href="https://softwarecrisis.baldurbjarnason.com/"><em>Out of the Software Crisis</em></a> and related blog posts.</p>
<p>The book was both a product designed to help people do their jobs and a tool to help me position myself as a freelance consultant beyond the more narrow publishing and media space where I’d previously planted my software development flag.</p>
<p>It did result in a number of contract leads, but fewer than you’d expect. Next to none of those leads survived the first “weeding”. Most managers who exhibited an interest in the problem of managing software development seemed to be major fans of extremely distasteful people with very poor judgement and warped priorities.</p>
<p><em>Somebody who keeps praising people like the 37 Signals manager who led their company into repeated crises by prioritising extremist politics over good management is unlikely to be a pleasant person to work with, even if you naively disregard their likely politics.</em></p>
<p>Once I weeded out the “37 Signals are great managers” sympathisers and related crowd – they seemed thick on the ground in software even after that company’s first <a href="https://www.baldurbjarnason.com/2021/you-are-what-you-do/">“racism is good, actually” implosion</a> – the remaining leads were old contacts from the publishing industry who had gone “oh, this means Baldur is a freelancer now” and got in touch. A significant portion, though not a majority, of these projects turned out to be related to corporate web developer education and training, which I enjoyed.</p>
<p>I did consider trying to turn the “momentum” of the book into other products – a course, mentorship, or the like – but, while I had experience as a lead dev, I’d left all of my ambitions of becoming a manager of some sort behind and I think there’s a limit to what somebody who has never been a manager can teach people with ambitions in that direction.</p>
<p>I didn’t feel I could teach a field that I wasn’t interested in practicing myself, especially since I was still deep in my Long COVID brain fog that had hit me just before I began work on the book.</p>
<p>Then, as the worst of fog began to inch away, the “AI” Bubble happened.</p>
<h2 id="i-never-intended-to-write-about-ai" tabindex="-1">I never intended to write about “AI” <a class="header-anchor" href="https://www.baldurbjarnason.com/2026/the-story-of-this-blog-so-far/#i-never-intended-to-write-about-ai">#</a></h2>
<p>I’ve had an on and off interest in “AI” and Machine Learning for most of my career so, when Copilot and ChatGPT started to get traction, looking into it was a logical move.</p>
<p>What I discovered appalled me. All of those management principles I’d been trying to teach in my work and writing, all the development practices I’d been trying to promote, weren’t just being disregarded by these tools.</p>
<p>They were, and still are, the antithesis of sound software development and good management.</p>
<p>They are broken by design, all the way from the fractured ground up to the wobbly top.</p>
<ul>
<li>The error rates are unacceptable.</li>
<li>The variability and volatility is unacceptable.</li>
<li>The copyright ambiguity they create in the generated code is unacceptable.</li>
<li>The disregard of consent, regarding both the personal data and creative works they derive their models from, is unacceptable.</li>
<li>The abuses they enable are unacceptable.</li>
<li>The environmental impact is unacceptable.</li>
<li>Their political collusion with extremists is unacceptable.</li>
<li>The sheer scale of destruction they inflict on the knowledge ecosystem and creative industries is unacceptable.</li>
</ul>
<p>And, fundamentally, they make good management impossible. The way they function is incompatible with organisation, structure, quality, and variation control.</p>
<p>Any form of integration with your work will compromise your work, irrespective of the costs, origin, point of execution, or ethics of creation. The damage may not be apparent at first, but these systems, due to their design, will hurt you in the long run.</p>
<p>I and others have compared it with asbestos – a miracle material with genuinely amazing properties that is so incredibly harmful that it has even been banned in the United States. <a href="https://www.asbestos.com/mesothelioma-lawyer/legislation/">For the most part.</a> That final attempt to close the gap on an asbestos ban took place in 2024, with the preceding debate coinciding with the initial growth of the “AI” Bubble.</p>
<p>It isn’t a coincidence that the asbestos analogy has been popular among those who criticise “AI”. It was at the top of the mind of pretty much anybody who was concerned about safety and consumer protection while ChatGPT was blowing up.</p>
<p>But I now prefer to compare generative models to leaded petrol:</p>
<p><em>It poisons everything, increases crime, harms your brain, makes the world genuinely worse in multiple ways, and could have been replaced with safer and more effective solutions at pretty much any point in its history.</em></p>
<p><strong><em>The only reason people didn’t – why they chose to knowingly poison their children and their own societies – was to save a little bit of money in amounts that never came even remotely close to outweighing the immense costs to society.</em></strong></p>
<p>This is a technology that should never <em>ever</em> have been shipped.</p>
<p>I’m genuinely disappointed in every single person who keeps promoting it in their blog posts, videos, or social media.</p>
<p><a href="https://illusion.baldurbjarnason.com/"><em>The Intelligence Illusion</em></a>, where I wrote about the risks generative models create for businesses, was motivated by how appalled I became as I researched the technology and how it interacted with the theory and practice of management.</p>
<p>This both worked and backfired in ways that I never expected.</p>
<h2 id="no-freelance-career-yet" tabindex="-1">No freelance career yet <a class="header-anchor" href="https://www.baldurbjarnason.com/2026/the-story-of-this-blog-so-far/#no-freelance-career-yet">#</a></h2>
<p>I did get a couple of interviews in the early days, as I was one of the first to push out a book on the topic, but once I got quoted out of context in a fairly awful New York Times piece I began to deny interview requests that were specific to “AI”. (There are a couple of outlets I’d decided in advance I’d say yes to if they asked, but they never asked.) I’d recommend people, and I offered to speak on background, helping the reporter understand what they were covering.</p>
<p>Only one took me up on that offer, which was a lot of fun, and the interview requests stopped after a while. Once reporters stop seeing your name in the venues they keep an eye on, they drop you from their list.</p>
<p>There was never any chance of the book leading to freelance opportunities. Every time I’ve received queries from companies on helping them form an “AI” strategy or policy, I replied with a short message saying that my advice is simple:</p>
<p><em>Avoid the tech until the bubble pops, then reassess. Most of the biggest web companies in the world today got into it or were founded after the dot-com bubble popped. Being late is a strategic advantage. If you want, I could help you outline how that strategy would work in detail – how to prepare for when the bubble pops – but otherwise my advice is simple: <em>Wait</em>.</em></p>
<p>I always thought this was a nice and neutral reply. Focused on the strategic benefit to the company. No mention of pesky ethics or environmentalism that might scare the punters off or disturb the unholy soulless inner workings of an MBA.</p>
<p>But the communications always ended at that point.</p>
<p>Almost all of my freelance clients continued to come from former publishing industry contacts.</p>
<p>The other problem was that the book itself and the associated blog posts arguably backfired.</p>
<h2 id="be-careful-in-explaining-your-adversary-s-mistakes-they-might-learn-from-it" tabindex="-1">Be careful in explaining your adversary’s mistakes; they might learn from it <a class="header-anchor" href="https://www.baldurbjarnason.com/2026/the-story-of-this-blog-so-far/#be-careful-in-explaining-your-adversary-s-mistakes-they-might-learn-from-it">#</a></h2>
<p>The book helped the right people. But it also helped the wrong people.</p>
<p>The best way to explain it is to put it in the context of a different societal and economic transformation: the deindustrialisation of the US with outsourcing.</p>
<p>Companies all over the US were destroying local jobs, closing factories, and deindustrialising their country by shifting work abroad. If you believed that outsourcing weakened society, led to the downfall of the economy, and exposed the companies to hard-to-manage risks, one of the worst things you could do would be to focus on the risks to the companies.</p>
<p>If you were living in that era and had the skills and interest to tackle the issue, you could take the humanities and social sciences approach and outline the effects on the social fabric, local communities, education, and public health. If you were an academic or a journalist, this would be the approach that made the most sense and your position would give you enough standing to make people listen.</p>
<p>Or, you could have put on your policymaker or economist hat and dug into the fractures this created in your economy and the sociopolitical risks to the state. Anybody who had the ear of the lawmakers would have been a fool not to take this tack on the topic.</p>
<p>If your skill sets were management theory, systems-thinking, and the organisation and structure of work, you might have be tempted to cover how outsourcing specifically harmed the companies themselves, their function and ability to operate, in an attempt to talk them out of it.</p>
<p><em>This would have been a mistake.</em></p>
<p>These companies were deliberately setting out to weaken the bargaining power of the workforce. They already felt that the risks were worth it. All you could have accomplished by going over the specific risks of outsourcing they themselves were facing – the communications, the longer iterations, the disconnect between teams, etc. – would have been to help them mitigate the impact of those risks and further their goal of destroying labour.</p>
<p>Offshoring – despite being on a macro level one of the more catastrophic strategic errors in US history since the end of the Cold War – happened nonetheless. It made the companies and their owners richer even as it rotted their countries from within.</p>
<p>“AI” is flowing into those same dried riverbeds. It’s the same social, economic, and strategic mistake except at an even larger scale and putting even more at risk.</p>
<p>My book was helpful to many readers. I still occasionally get emails thanking me for getting their defences against LLMs up in the early days of the bubble. Not often, but occasionally.</p>
<p>But to some readers it was helpful in ways that I, the writer, did not enjoy or appreciate.</p>
<p>Many of the glowing recommendations and positive buzz around the first edition of the book came from consultants who then used it as a playbook for furthering “AI” adoption in business by managing the risks deemed important by the business. I even got an email from a Scandinavian VC and “AI entrepreneur” thanking me effusively for the book.</p>
<p>If you ever wondered why I stopped covering specific organisational and work-related issues caused by “AI” – the flaws specific to the tech and how they interacted with work – now you know.</p>
<p>If you wondered why I published a second edition despite the risks being fundamentally unchanged and most of the research continuing to come to the same conclusions, this is why. I wanted to reposition the book to make it untenable to the consultancy crowd.</p>
<p>This worked, I think. At least I don’t get emails from management consultants or VCs any more.</p>
<p>My writing here shifted mostly to three topics:</p>
<ul>
<li>Tech scepticism. That is, advocating the idea that you should be careful about trusting the tech industry and its products.</li>
<li>The personal impact of “AI”.</li>
<li>Web development.</li>
</ul>
<p>(Also photography, but that’s just for fun. Nothing practical about it.)</p>
<p>My tech scepticism netted me what you could describe as a <a href="https://en.wikipedia.org/wiki/Backbencher">“backbencher”</a> position in the online tech scepticism community. Not one of the frontbencher who gets called on for interviews and the like, but I occasionally get to write a polemic that rouses and motivates other backbenchers.</p>
<p>There isn’t much more to it than that at the moment, and that’s fine.</p>
<p>But when I turned back to writing about web development, I discovered that the field was gone. Web dev training, specifically, has been wiped out by “AI”.</p>
<h2 id="where-did-everybody-go" tabindex="-1">Where did everybody go? <a class="header-anchor" href="https://www.baldurbjarnason.com/2026/the-story-of-this-blog-so-far/#where-did-everybody-go">#</a></h2>
<p>While I was busy pushing the “AI” book I’d failed to notice that all my training and education projects had dropped off one by one. Most of my peers who had been selling courses and training were shutting down their businesses. The demand for web dev ebooks had cratered.</p>
<p>So, my attempt to shift away from the “AI” book back to web dev training was a bit like exiting a bunker with a hot dog cart in tow and try to sell a nice juicy hot dog in a blasted post-apocalyptic landscape full of ash and radiation.</p>
<p>Software developers haven’t all switched to using agents for their coding, but they do seem to have all switched to error-prone, backwards-facing, nondeterministic chatbots for what now passes as web dev “education”.</p>
<p>Every time I thought to myself “maybe it isn’t that bad, those two are still selling their courses and ebooks so there obviously continues to be a market for this” I’d find out a while later that their business had indeed collapsed and that they had been coasting on fumes. Then they would give up as well and quit the industry.</p>
<p>Moreover, the field of web development has regressed overall. React and framework-oriented development is back in full force. Novelty and research now focuses exclusively on some variation of getting a Large Language Model to regurgitate some old thing into the gutted, hollowed-out skinsuit of some other old thing. “Let’s use an agent to port it to Rust! Let’s make a WordPress replacement in JavaScript! Let’s port this project to a more convenient license!”</p>
<p>Dependencies are getting less and less reliable as everybody’s toolchain gets rotted open with insecure coding agents and bad practices are automated into petrification.</p>
<p>Design is nonexistent.</p>
<p>User research is nonexistent.</p>
<p>Product development is nonexistent.</p>
<p>I don’t enjoy my field any more <em>because the field is gone</em>. Assuming that there’s still something there is a mistake. That feeling you have isn’t a frustration with changes to your chosen field. It’s nostalgia for a field that disappeared overnight. What passes as web dev today at large bears no resemblance to the field we built over the years. Everything we learned has been abandoned because companies assume it’s all been encoded into their chosen Large Language Model.</p>
<p>I’m still doing web development work, but navigating the flood of misguided pull requests and nonsense “documentation” is getting harder, not easier. I love the work when I get to do it without dealing with nonsense, but I don’t think finding those opportunities is going to get any easier from now on.</p>
<p>It’s frustrating and depressing and the more I experience the “new world order” of web development the less I want to have to do with any of it.</p>
<p>Employment in tech seems done. All the major employers have revealed their hand and that hand is just one “fire more; hire less” card after another.</p>
<p>You could stay and fight for your survival, but it would be foolish to expect those who remain to have a glorious career. You’re basically in the same position as a non-union factory worker in a post-outsourcing, deindustrialised economy. You might be one of the few who gets to keep your job but what little bargaining power you had against your employers is gone. That never ends well for the worker.</p>
<p>With web dev gone and tech scepticism not being anything that resembles a career path – you’re a Patreon- and book-subsidised commentator at best, not nothing, but still not a long-term career – it’s hard to figure out where I should position myself.</p>
<p>This blog has been a personal journal, a venue for hashing out my thoughts and ideas. It’s also played a big role in developing my career.</p>
<p>Software development and tech have been where I positioned myself, for most of the past decade or so. Everything else has fallen into the “personal” category. Stuff that I cover because I needed a venue for the thoughts, but never represented the brunt of this site.</p>
<h2 id="outside-and-downplayed" tabindex="-1">Outside and downplayed <a class="header-anchor" href="https://www.baldurbjarnason.com/2026/the-story-of-this-blog-so-far/#outside-and-downplayed">#</a></h2>
<p>A necessary part of surviving having an online presence is artful omission. Parts of your life get to be visible online. Other parts are downplayed. The rest is offline.</p>
<p>The part of my life I’ve constantly downplayed online is my work, education, and experience in media and publishing.</p>
<p>I’ve worked in radio, TV (vision mixer), did a short stint in Iceland’s then brand-new film school, wrote and self-published novellas, and helped a number of people publish their books both digitally and in print. I’ve helped artists put together their art books and assisted with their installations. My first degree was in comparative literature. The rest were at an Art, Media, and Design faculty. For years, I worked just as much in After Effects and Premiere as I did in Director, Flash, or web dev. I taught both at an art, media, and design faculty in Bristol and design and photography students at a junior college level here in Iceland. I <a href="https://www.baldurbjarnason.com/books/kotturinn-og-eg/">published a graphic novel a few months ago.</a></p>
<p>It’s the part of my career that I’ve enjoyed the most and, for a while, spent the most time on.</p>
<p>But it came with two problems:</p>
<ol>
<li>It generally doesn’t pay. At least not compared to web dev work.</li>
<li>I always felt like an outsider in those fields.</li>
</ol>
<p>As I’ve grown older I’ve come to realise just how much the second issue affected me.</p>
<p>For example, one memory that still sits with me and played a big role in driving me away from fiction writing was as I sat listening to a friend of mine struggle to say something positive about a novella I had written. He settled on “the typography is excellent.”</p>
<p>The typography of an <em>ebook</em>. This, coming from a British publishing professional, if you know what they’re like you know what that means, felt devastating.</p>
<p>I completely gave up on writing fiction shortly afterwards, and I’m only now coming to terms with how much of that came from how rejected I felt by my friends and acquaintances in the publishing and media industries. At the time it felt like the people who I thought would cheer me on unprompted just all fell silent. I always assumed it was because they couldn’t think of anything nice to say.</p>
<p>That I worked at the crossroads between tech and publishing – ebooks, content management systems, reading systems, and the like – also seemed to mark me as an outsider.</p>
<p>Another defining memory from that period was sitting in the Unbound offices (this is before we all found out <a href="https://en.wikipedia.org/wiki/Unbound_(publisher)">what was going on there</a>) and chatting with others around the lunch table about our jobs. As you do. When I mentioned something I was working on, one of my coworkers turned to me and said something along the lines of “I’ve never read an ebook and never will” and that was the end of that. In the end, the only topic of conversation that seemed tenable for me to the rank and file of Unbound was to answer questions about Iceland and tourism in Iceland.</p>
<p>On more than one occasion, I had people at publishing conferences immediately turn away and leave the conversation as soon as I mentioned that I was working on the tech side of things.</p>
<p>Seeking approval from people who were never going to approve of you is never healthy.</p>
<p>Some people were great – still are – but those also tended to be treated like outsiders by the rest of the industry.</p>
<p>Over the past decade I’ve actively downplayed the scope of my media and publishing career because I’ve always assumed that your average person in tech would be at least as big a dick as the average publishing person and would be just as dismissive of anybody with a hybrid skill set.</p>
<p>I don’t think I was wrong about that – “we’re major dicks!” seems to be a point of pride for many in tech – but I do think I underestimated the potential of connecting with other outsiders. People with hybrid skill sets, expertise and experience in both print or media and in tech – or even just experience in one and a dream of another – and who feel alienated from both sides of the crossroads they work at.</p>
<p>I think I underestimated how many of us there are and, now that the “AI” Bubble has managed to alienate big swathes of society, I think there are more of us than ever before.</p>
<p>So, more than a topic, I have a feeling I want to capture. A spark of an emotion I want to keep alive:</p>
<p><em>I don’t want any of us who stand at the crossroads between fields to feel like outsiders any more. With the world heading where it’s heading, us outsiders need to band together.</em></p>
<p>The actual topics, I think, should be more defined by what this ad hoc group of people – wherever I find them – need solving than anything predefined, but I think there might be a few broad areas I could tackle.</p>
<ul>
<li>Creative processes. Specifically coming from one process to another and how you adapt your thinking and approach when transitioning to a new venue or field.</li>
<li>Getting over the hurdles of doubt and worries about the ugliness of your work.</li>
<li>Getting things done and productivity are not the same thing. In fact, I’d say they were close to opposites.</li>
<li>Discovering yourself is a key requirement for sustainable achievements in any creative field.</li>
<li>Appreciating the ugliness of both creative and tech work.</li>
<li>Understanding the context of media and how tech affects it.</li>
<li>How to deal with the way tech affects our work and media.</li>
<li>Understanding how the grammar and structure of social media affects what you can say and how you can say it.</li>
<li>Working with the hybrid media landscape we live in.</li>
</ul>
<p>The topics are endless, but the feeling defines the worldview and angle of approach.</p>
<p>I <em>think</em> I can figure out a way to be useful for this audience.</p>
<p>And the next step is pretty much just to go ahead and try it.</p>
<p>At least, that’s the theory. We’ll have to see how it turns out in practice.</p>
]]></description>
      <pubDate>Thu, 27 Aug 2026 13:05:30 +0000</pubDate>
      <link>https://www.baldurbjarnason.com/2026/the-story-of-this-blog-so-far/</link>
      <dc:creator>Baldur Bjarnason&#39;s Notes on the Web</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5355964311</guid>
    </item>
    <item>
      <title><![CDATA[A look at the geolocation HTML element and how it works]]></title>
      <description><![CDATA[<p>The <code>&lt;geolocation&gt;</code> HTML element does exactly what you might think it does. It’s a dedicated element that gets the user’s location, either once or continuously. The options are set using HTML attributes instead of JavaScript, but JavaScript is still needed. However, <code>&lt;geolocation&gt;</code> requires <em>fewer lines</em> of JavaScript and also offers superior error and permission handling. In fact, the <code>&lt;geolocation&gt;</code> element started off as an all-purpose <code>&lt;permission&gt;</code> element, but is now dedicated to handling geolocation.</p>
<p>Fewer lines of code is always a good thing, but it’s the permission prompt that’s key here. When using the aging Geolocation JavaScript API, denying permission at any point can lock the user out with no way to recover unless they change the browser or operating system permissions manually. This can be a painful experience, especially for users that aren’t tech-savvy. Plus, as developers, we have to get the Permissions API involved.</p>
<p>Whereas, the <code>&lt;geolocation&gt;</code> element offers:</p>
<ul>
<li>Better error handling, and where applicable, recovery handholding</li>
<li>User-controlled permission prompting, even if the user denied permission previously</li>
<li>Auto-location if the user granted permission previously</li>
<li>A styleable granted state via the <code>:granted</code> CSS pseudo-class</li>
</ul>
<p><code>&lt;geolocation&gt;</code> is better in every way except browser support (it requires Chrome 144+), so in this article, I’ll explain how to use it alongside the older current Geolocation JavaScript API so that users get the better experience, if the new element is available.</p>
<p></p>
<h2>Requesting the user’s location using <code>&lt;geolocation&gt;</code></h2>
<p>The <code>&lt;geolocation&gt;</code> element accepts several attributes that largely correspond to the options of the <code>getCurrentPosition()</code> and <code>watchPosition()</code> methods of the aging Geolocation API.</p>
<p>Firstly, the <code>accuracymode</code> attribute accepts two values — <code>approximate</code> (which is the default value) and <code>precise</code>.</p>
<p><code>accuracymode=approximate</code> is equivalent to the <code>enableHighAccuracy: false</code> option (again, the default) from the Geolocation API, whereas <code>accuracymode=precise</code> is equivalent to <code>enableHighAccuracy: true</code>, which provides a more accurate location if the device is able to get one.</p>
<p>The <code>autolocate</code> boolean attribute attempts to get the user’s location automatically, assuming that they’ve granted the website permission previously.</p>
<p>The <code>watch</code> boolean attribute is equivalent to calling the <code>watchPosition()</code> method instead of the <code>getCurrentPosition()</code> method. <code>getCurrentPosition()</code> fetches the user’s location once, whereas <code>watchPosition()</code> tracks their location over time. Like <code>accuracymode=precise</code> and <code>enableHighAccuracy: true</code>, this drains the battery faster.</p>
<p>The <code>onlocation</code> event handler attribute can be used to execute JavaScript whenever location data or error information is passed to the browser.</p>
<p>What the <code>&lt;geolocation&gt;</code> element doesn’t offer, though, is the ability to specify the <code>timeout</code> (how long the browser should wait for a response) or <code>maximumAge</code> (how old a cached location can be). Instead the browser handles these as it sees fit, which is actually a good thing, because choosing the right values on a case-by-case basis is a complexity that we just don’t need.</p>
<p>In practice, this is how you might use <code>&lt;geolocation&gt;</code>:</p>
<pre><code>&lt;!-- Precisely autolocate and follow the user --&gt;
&lt;geolocation accuracymode="precise" autolocate watch&gt;
  &lt;!-- Render this when &lt;geolocation&gt; is unsupported --&gt;
  &lt;button id="fallbackButton"&gt;Use precise location&lt;/button&gt;
&lt;/geolocation&gt;
</code></pre>
<p>That is, of course, not including the JavaScript side of <code>&lt;geolocation&gt;</code> nor the Geolocation (JavaScript) API fallback. If you just want the full code then you’re looking for the <code>getCurrentPosition()</code> version and <code>watchPosition()</code> version. Note that geolocation doesn’t work in insecure contexts, so while the logic is sound, the CodePen demos won’t actually work. They’re raw-logic templates anyway, not fully working demonstrations.</p>
<p>Anyway, let’s get into the JavaScript of it all, starting with the JavaScript that sits on the other side of that <code>&lt;geolocation&gt;</code> markup.</p>
<h2>Handling the <code>&lt;geolocation&gt;</code> data with JavaScript</h2>
<p>First, we want to make sure that <code>&lt;geolocation&gt;</code> is supported with <code>if ("HTMLGeolocationElement" in window)</code>. If it is, then we select it with <code>const geoElement = document.querySelector("geolocation")</code>.</p>
<p>Unfortunately, we then have to dive into the most complex part of <code>&lt;geolocation&gt;</code> — it’s validity — where the <code>isValid</code> property returns <code>true</code> or <code>false</code> and the <code>invalidReason</code> property returns <code>""</code> (an empty string) or an enumerated value stating the reason.</p>
<p>These reasons mostly come down to developer oversight, so users <em>shouldn’t</em> encounter these ‘blockers’, but if they do, the <code>&lt;geolocation&gt;</code> button will be disabled. Here are the scenarios in which that can happen (note that the blockers are ordered by severity, and that <code>invalidReason</code> only returns the most severe one):</p>
<ul>
<li><code>illegal_subframe</code>: the <code>&lt;geolocation&gt;</code> element is nested within a <code>&lt;fencedframe&gt;</code> or insecure <code>&lt;iframe&gt;</code> (the latter of which is why the CodePen demos don’t work)</li>
<li><code>unsuccessful_registration</code>: the page has more than three <code>&lt;geolocation&gt;</code> elements</li>
<li><code>recently_attached</code>: the <code>&lt;geolocation&gt;</code> element has only recently been attached to the DOM (this blocker expires fairly quickly)</li>
<li><code>intersection_changed</code>: the <code>&lt;geolocation&gt;</code> element is moving</li>
<li><code>intersection_out_of_viewport_or_clipped</code> : the <code>&lt;geolocation&gt;</code> element isn’t within the viewport fully</li>
<li><code>intersection_occluded_or_distorted</code>: something is obscuring the <code>&lt;geolocation&gt;</code> element</li>
<li><code>style_invalid</code>: the <code>&lt;geolocation&gt;</code> element is styled in a way that isn’t allowed (more on this later)</li>
</ul>
<p>As you can see, these blockers are permanently avoidable as long as we catch and fix them. Let’s have a proper look at how <code>invalidReason</code> reports these blockers, though.</p>
<p>As the JavaScript comment below describes, <code>isValid</code> always returns <code>false</code> at first, while <code>invalidReason</code> returns <code>recently_attached</code>. This basically disables the <code>&lt;geolocation&gt;</code> button for a fraction of a second to prevent clickjacking. That is, unless a blocker of higher severity applies. We don’t actually need the line below, it’s there just to show you how <code>&lt;geolocation&gt;</code> will never be valid when the page loads.</p>
<pre><code>/* At first, isValid === false and invalidReason === "recently_attached"
unless isValid === false and invalidReason === "a reason of higher severity" */
console.warn(`isValid: ${geoElement.isValid}, invalidReason: ${geoElement.invalidReason}`);
</code></pre>
<p>If <code>&lt;geolocation&gt;</code> then becomes invalid for a more permanent reason, <code>isValid</code> will obviously remain <code>false</code>, so we can’t use the <code>validationstatuschange</code> event listener here. However, we can wrap the line in <code>setTimeout()</code> (as below). This will either log a new blocker into the console, or log that <code>isValid</code> is <code>true</code> (in which case <code>invalidReason</code> will be an empty string).</p>
<pre><code>/* After ~300ms, when &lt;geolocation&gt; is no longer ‘recently attached’ (to the DOM)
either isValid === true and invalidReason === "" (an empty string)
or isValid === false and invalidReason === "the reason with the highest severity" */
setTimeout(() =&gt; {
  console.log(`isValid: ${geoElement.isValid}, invalidReason: ${geoElement.invalidReason}`);
}, 300);
</code></pre>
<p>If the validation status changes later, <em>then</em> we can use the <code>validationstatuschange</code> event listener (again, as below). Note that if there are no persistent blockers, this will fire almost immediately as <code>invalidReason</code> switches from <code>recently_attached</code> to an empty string.</p>
<pre><code>/* If the validity of &lt;geolocation&gt; changes */
geoElement.addEventListener("validationstatuschange", () =&gt; {
  if (geoElement.isValid) {
    /* &lt;geolocation&gt; is valid (if there aren’t any blockers,
    it will become valid after it’s no longer ‘recently attached’ */
  } else {
    console.error(`&lt;geolocation&gt; invalid: ${geoElement.invalidReason}`);
  }
});
</code></pre>
<p>Now that you know how to debug the validation status and permanently fix any persistent blockers, let’s talk about the <em>permission</em> status.</p>
<p>We’re given a few properties and events to work with:</p>
<ul>
<li><code>initialPermissionStatus</code>: a property that returns <code>denied</code>, <code>granted</code>, or <code>prompt</code> (i.e., neither) based on the permission status when the page first loaded</li>
<li><code>permissionStatus</code>: the <em>current</em> permission status</li>
<li><code>promptaction</code>: an event that fires when the user denies or grants permission from the <code>&lt;geolocation&gt;</code> permission prompt dialog</li>
<li><code>promptdismiss</code>: fires when the user dismisses the dialog, in which case the <code>permissionStatus</code> remains unchanged</li>
</ul>
<pre><code>/* Determine the initial permission status */
if (geoElement.initialPermissionStatus === "denied") {
  /* The user previously denied permission */
} else if (geoElement.initialPermissionStatus === "granted") {
  /* The user previously granted permission */
} else if (geoElement.initialPermissionStatus === "prompt") {
  /* The user hasn’t made a choice */
}

/* If the user denies or grants permission */
geoElement.addEventListener("promptaction", () =&gt; {
  if (geoElement.permissionStatus === "denied") {
    /* The user denied permission */
  } else if (geoElement.permissionStatus === "granted") {
    /* The user granted permission */
  }
});

/* If the user dismisses the prompt */
geoElement.addEventListener("promptdismiss", () =&gt; {
  if (geoElement.permissionStatus === "denied") {
    /* The permission state remained denied */
  } else if (geoElement.permissionStatus === "granted") {
    /* The permission state remained granted */
  } else if (geoElement.permissionStatus === "prompt") {
    /* The permission state remained prompt */
  }
});
</code></pre>
<p>That being said, I honestly don’t know what we’d need any of that for. If the user previously denied permission, for example, the <code>&lt;geolocation&gt;</code> permission prompt dialog would enable the user to recover from that automatically:</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/geolocation-1.png" alt="Two confirmation boxes. One reads &quot;You previously didn't allow location for this site&quot; and the other reads &quot;To use your location on this site, give Chrome access&quot;"></p>
<p>This is in contrast to the older Geolocation API, which is unlikely to help users recover. In this case we need to read the permission status, manage the state of the component accordingly, and provide recovery instructions so that users can grant access manually, but <code>&lt;geolocation&gt;</code> takes care of all of that.</p>
<p>What you <em>will</em> need is the new <code>location</code> event, which fires whenever the browser passes location data (<code>geoElement.position</code> in this case) or error information (<code>geoElement.error</code>) to us.</p>
<p>Then, in the event listener callback, assuming that <code>geoElement.position</code> is truthy, we can access <code>position.coords</code> and <code>position.timestamp</code>, synthesizing the location data like this:</p>
<pre><code>/* If the browser passes location data or error information */
geoElement.addEventListener("location", () =&gt; {
  /* If location data */
  if (geoElement.position) {
    /* Synthesize the data */
    const {
      latitude,
      longitude,
      altitude,
      accuracy,
      altitudeAccuracy,
      heading,
      speed
    } = geoElement.position.coords;

    const timestamp = geoElement.position.timestamp;
  } else if (geoElement.error) {
    /* If error information */
  }
});
</code></pre>
<p>However, if <code>geoElement.error</code> is truthy, we can access <code>error.message</code> (that’s for us to log into the console) and <code>error.code</code>, which is much more suitable for error handling.</p>
<p>In short, there are three possible error codes, each with an associated constant so that we don’t need to remember what each error code represents. So <code>1</code> represents <code>PERMISSION_DENIED</code>, <code>2</code> represents <code>POSITION_UNAVAILABLE</code>, and finally, <code>3</code> represents <code>TIMEOUT</code>, and then we just evaluate them like this:</p>
<pre><code>/* If the browser passes location data or error information */
geoElement.addEventListener("location", () =&gt; {
  if (geoElement.position) {
    /* If location data */
  } else if (geoElement.error) {
    /* If error information */
    console.error(`&lt;geolocation&gt; error: ${geoElement.error.message}`);

    if (geoElement.error.code === geoElement.error.PERMISSION_DENIED) {
      /* No HTTPS or server misconfiguration */
    } else if (geoElement.error.code === geoElement.error.POSITION_UNAVAILABLE) {
      /* No location source (GPS satellite or nearby Wi-Fi network/cellular tower) */
    } else if (geoElement.error.code === geoElement.error.TIMEOUT) {
      /* Location source detection or hardware took too long */
    }
  }
});
</code></pre>
<p><code>PERMISSION_DENIED</code> means that the website isn’t being served over HTTPS (the browser denied permission), or that there’s some kind of server misconfiguration (the server denied permission), but those two errors are permanently fixable and shouldn’t occur in production.</p>
<p>The only way to deny permission (as far as I’m aware) is to block location access from the browser settings, then click on the <code>&lt;geolocation&gt;</code> button, then choose to continue denying. In my opinion, that’s not likely to happen and doesn’t warrant an error message anyway. There isn’t a denial mechanism for OS-level blocks so as not to make the impression that the browser can enforce one. In short, I don’t think we need to do anything for <code>PERMISSION_DENIED</code>.</p>
<p>And to clarify, because <code>&lt;geolocation&gt;</code> is user-invoked, <code>&lt;geolocation&gt;</code> itself never sets the permission status to denied (again, as far as I know).</p>
<p><code>POSITION_UNAVAILABLE</code> means that the device can’t detect a location source (nearby Wi-Fi networks and cellular towers as well as GPS satellites) to determine the location. Using a VPN or visiting the website via an in-app browser could cause this error too, so this is the trickiest error to convey to users.</p>
<p><code>TIMEOUT</code> means that the location source detection or hardware took too long, and that users should try again.</p>
<p>How you communicate errors to users is totally up to you.</p>
<p></p>
<h2>Falling back to the Geolocation JavaScript API</h2>
<p>Ready for round two? Now we’re going to do the same thing but with the aging Geolocation API, which is supported in every browser, but kind of a headache.</p>
<p>This is where we’re at currently:</p>
<pre><code>/* If &lt;geolocation&gt; is supported */
if ("HTMLGeolocationElement" in window) {
  /* What we covered in the previous section */
} else {
  /* What we’re focusing on now (the fallback) */
}
</code></pre>
<p>Within that <code>else</code> block, which runs when <code>&lt;geolocation&gt;</code> isn’t supported, we start off by selecting the fallback button (<code>const fallbackButton = document.querySelector("#fallbackButton")</code>). If you recall, this is nested within <code>&lt;geolocation&gt;</code> so that it’s ignored when <code>&lt;geolocation&gt;</code> <em>is</em> supported.</p>
<p>After that we create a function (<code>updateState()</code>) that manages the component state and provides recovery instructions. As arguments we supply the <code>state</code> as a string (<code>‌granted</code>, <code>‌prompt</code>, or <code>‌denied</code>, corresponding with the <code>permissionStatus</code>), and optionally, <code>statusMessage</code>, which’ll be used to convey status messages to the user. I don’t want to make any assumptions about your component, so how you convey the <code>statusMessage</code> and expand upon <code>updateState()</code> is up to you.</p>
<p>If <code>state === "denied"</code>, we disable the button with <code>fallbackButton.disabled = true</code> and provide some kind of recovery instruction of which should be passed as the second argument of the function. The reason why we disable the button is that <em>this</em> Geolocation API isn’t user-invoked, so to protect the user from spam requests, the browser can suppress requests and send the API straight to jail without passing go, triggering <code>PERMISSION_DENIED</code>. Additionally, if we make the API user-invoked (as we have), users can spam the button themselves and basically shadowblock themselves, but disabling the button fixes that.</p>
<p>If <code>state === "granted"</code> or <code>state === "prompt"</code>, we can enable the button (<code>fallbackButton.disabled = false</code>).</p>
<p>Now is a good time to mention that the earlier JavaScript code for the <code>&lt;geolocation&gt;</code> element works regardless of whether the element has the <code>watch</code> attribute or not. However, when using this older Geolocation API, there are two additional things that we need to take care of when trying to keep track of the user’s location continuously. The first thing is the watcher ID, which is returned by the <code>watchPosition()</code> method. Knowing this ID enables us to clear the watcher before registering a new one, which is a must-do for performance reasons.</p>
<p>So we <code>let watcherID = null</code> for now, and then we create the function that attempts to get the location (<code>getLocation()</code>), and the first thing that we do within that function, assuming that <code>watcherID !== null</code> (meaning that it’s been set before), is clear the watcher using <code>navigator.geolocation.clearWatch(watcherID)</code> and make <code>watcherID = null</code> again:</p>
<pre><code>/* A function for getting the location */
const getLocation = () =&gt; {
  /* If a watcher has already been registered */
  if (watcherID !== null) {
    /* Unregister it */
    navigator.geolocation.clearWatch(watcherID);
    watcherID = null;
  }
}
</code></pre>
<p>Then we disable the button using <code>fallbackButton.disabled = true</code> to, again, prevent spam clicks. If you want to bake some kind of loading indicator in, feel free to, but <code>&lt;geolocation&gt;</code> doesn’t.</p>
<p>After that we call <code>navigator.geolocation.watchPosition()</code>, setting <code>watcherID</code> to the returned watcher ID. This method has three parameters — success, error, and options.</p>
<pre><code>watcherID = navigator.geolocation.watchPosition(
  (position) =&gt; {
    /* Success */
  },
  (error) =&gt; {
    /* Error */
  },
  {
    /* Options */
  }
);
</code></pre>
<p>For the success callback function we synthesize the location data similarly to last time, then call <code>updateState("granted")</code>.</p>
<p>For the error callback function (optional but highly recommended) we clear the watcher and, again, make <code>watcherID = null</code>, but otherwise run the same error handling logic that <code>&lt;geolocation&gt;</code> runs. However, there are more circumstances in which the errors can occur.</p>
<p>For example, because <code>watchPosition()</code> and <code>getCurrentPosition()</code> aren’t necessarily user-invoked, there are more scenarios in which users are able to deny access, triggering the <code>PERMISSION_DENIED</code> error. Accordingly, we should call <code>updateState("denied", "Permission denied (try this or that)")</code>, offering a useful status message and clear recovery instructions.</p>
<p>Similarly, <code>POSITION_UNAVAILABLE</code> can also be triggered by an OS-level block, since not all web browsers catch this during the permission prompt dialog. <code>updateState("prompt", "Position unavailable (try this or that)")</code> is what we’re looking for this time.</p>
<p>Finally, another scenario that doesn’t occur with <code>&lt;geolocation&gt;</code> but does with <em>this</em> Geolocation API, is that if the browser prompts the user to grant permission at the OS-level, but then the user cancels their request, that can trigger the <code>TIMEOUT</code> error. Either way, call <code>updateState("prompt", "Request timed out (try this or that)”)</code>, once again tweaking it to your liking.</p>
<p>As you can see, the error reporting isn’t the best. Sometimes the error isn’t identified correctly, and even when it is, the error can occur for various reasons, which makes it difficult for us to convey a useful status message and clear recovery instructions. <code>&lt;geolocation&gt;</code> handles this better — the errors are identified correctly, and the nature of <code>&lt;geolocation&gt;</code> ensures that certain errors never occur to begin with. But why the error categories? Why not tell us exactly what went wrong?</p>
<p>Well, the reason is to make fingerprinting more difficult, and while we can totally put in the extra work to pinpoint the exact problem, simply stating what happened and what the user should do next is perfectly fine. Of course, <code>error.message</code> tells us more than <code>error.code</code> does, but the messages can be a bit vague and differ in every web browser, so we can’t read them or even output them reliably.</p>
<p>Anyway, the optional third parameter expects an object where we can set:</p>
<ul>
<li><code>enableHighAccuracy</code>: <code>true</code> or <code>false</code></li>
<li><code>timeout</code>: <code>20000</code> (20 seconds) is reasonable if <code>enableHighAccuracy: true</code>, <code>3000</code> - <code>5000</code> otherwise</li>
<li><code>maximumAge</code>: <code>0</code> for turn-by-turn navigation, <code>5000</code> - <code>10000</code> (5-10 seconds) for live tracking, <code>300000</code> - <code>600000</code> (5-10 minutes) for frequent updates (e.g., weather)</li>
</ul>
<p>I’d rather that the web browser choose the <code>timeout</code> and <code>maximumAge</code> for us, especially considering the impact that they have on the <code>TIMEOUT</code> error and device battery, but of course, that’s exactly what <code>&lt;geolocation&gt;</code> does.</p>
<pre><code>/* Attempt to get the location and store the returned ID */
watcherID = navigator.geolocation.watchPosition(
  /* If location data is passed (like before) */
  (position) =&gt; {
    /* Synthesize the data (again, like before) */
    const {
      latitude,
      longitude,
      altitude,
      accuracy,
      altitudeAccuracy,
      heading,
      speed
    } = position.coords;

    const timestamp = position.timestamp;

    /* And update the state */
    updateState("granted");
  },

  /* If error information is passed (yep, like before) */
  (error) =&gt; {
    console.error(`Geolocation error: ${error.message}`);

    /* Again, unregister the watcher (if necessary) */
    if (watcherID !== null) {
      navigator.geolocation.clearWatch(watcherID);
      watcherID = null;
    }

    if (error.code === error.PERMISSION_DENIED) {
      /* No HTTPS, server misconfiguration, or the user denied access */
      updateState("denied", "Permission denied (try this or that)");
    } else if (error.code === error.POSITION_UNAVAILABLE) {
      /* No location source (GPS satellite or nearby Wi-Fi network/cellular tower) or OS-level access */
      updateState("prompt", "Position unavailable (try this or that)");
    } else if (error.code === error.TIMEOUT) {
      /* Location source detection or hardware took too long, or the user canceled their request */
      updateState("prompt", "Request timed out (try this or that)");
    }
  },
  {
    enableHighAccuracy: true,
    timeout: 20000 /* 20 seconds because enableHighAccuracy: true */,
    maximumAge: 0 /* 0 seconds because we’re demanding high accuracy */
  }
);
</code></pre>
<p>After that we need to query the permission status using the Permissions API (<code>navigator.permissions.query({ name: "geolocation" }).then((permissionStatus) =&gt; { /* ... */ })</code>), and then execute all of the aforementioned logic based on that.</p>
<p>If <code>permissionStatus.state === "granted"</code>, we call <code>getLocation()</code>, which is essentially what the <code>autolocate</code> attribute does. If you don’t want autolocation, simply delete this part. If it’s anything else, we basically determine the initial state by calling <code>updateState(permissionStatus.state)</code>.</p>
<pre><code>/* Query the permission status */
navigator.permissions.query({ name: "geolocation" }).then((permissionStatus) =&gt; {
  /* Equivalent to the autolocate attribute */
  if (permissionStatus.state === "granted") {
    getLocation();
  } else {
    /* React accordingly */
    updateState(permissionStatus.state);
  }
});
</code></pre>
<p>Finally, make the button respond to clicks:</p>
<pre><code>/* If the user clicks the fallback button */
fallbackButton.addEventListener("click", () =&gt; {
  getLocation();
});
</code></pre>
<p>If we only want to get the user’s location once (rather than watch it continuously), these are the modifications that we need to make:</p>
<ul>
<li>Remove everything related to the <code>watcherID</code></li>
<li>Swap <code>watchPosition()</code> for <code>getCurrentPosition()</code></li>
<li>Provide different settings for the options parameter</li>
</ul>
<p>Note that the <code>change</code> event, which could help us update the state whenever the user changes their browser-level permission status, doesn’t fire in Safari. Besides, the standard browser behavior is to ask users to refresh the page, so let’s stick with that.</p>
<p>One more thing — I wanted the button to say “Refresh precise location” and “Getting precise location…” at certain points, but <code>&lt;geolocation&gt;</code> doesn’t do this, so I didn’t either. That’s up to you, though.</p>
<p></p>
<h2>Styling the <code>&lt;geolocation&gt;</code> element</h2>
<p><code>&lt;geolocation&gt;</code> has a <code>:granted</code> pseudo-class. If we throw <code>:not(:granted)</code> into the mix, we can target the prompt/denied states too:</p>
<pre><code>geolocation {
  &amp;:granted {
    /* Permission granted */
  }

  &amp;:not(:granted) {
    /* Permission not granted */
  }
}
</code></pre>
<p>With the Geolocation API, we can toggle classes as needed to achieve the same effect, but honestly, I’ve never felt compeled to style such a button in this way. Personally, I’d like to be able to change the icon and text (maybe this is something that you can bake into the <code>updateState()</code> function), but this is one of the things that the <code>&lt;geolocation&gt;</code> element outright forbids.</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/geolocation-2.png" alt="A button element labelled &quot;use precise location&quot;"></p>
<p>Before we dive into all <em>that</em>, here’s a rundown of <code>&lt;geolocation&gt;</code> rules stated by Google’s explainer and Mozilla’s explainer:</p>
<ul>
<li>There must be sufficient color contrast</li>
<li>The alpha channel must resolve to <code>1</code> (so no transparency)</li>
<li>The minimum and maximum width, height, and font size must be respected</li>
<li>Negative margins and outline offsets aren’t allowed either</li>
<li>Distortion effects — including linear gradients — are banned</li>
</ul>
<p>It’s also worth noting that some of these rules function like guardrails (for example, you physically can’t style the height above 50px), whereas others cause <code>invalidReason</code> to return <code>style_invalid</code> and the <code>&lt;geolocation&gt;</code> button to be disabled.</p>
<p>Some of the rules seem to have changed (or are currently bugged). <code>&lt;geolocation&gt;</code> doesn’t respect <code>prefers-color-scheme</code> either, but it’s a developing feature, so I’m not going to comment on all of that too much. I’ll just state what I do and don’t like:</p>
<ul>
<li>I don’t like that the button becomes disabled because of a style (guardrails are better, but I don’t really like either)</li>
<li>I don’t like the forced icon and text, and I’m also not too happy about not being able to use gradients or <code>corner-shape</code></li>
<li>I can make peace with everything else because I’d normally design within those guardrails anyway</li>
<li>The text being localized into different languages is very cool, but it’s a first for web standards and I don’t currently enjoy that it’s a thing for only these permission buttons</li>
</ul>
<p>While I obviously support preventing web authors from tricking users into giving away their location, I think it’d be better if <code>&lt;geolocation&gt;</code> were fully styleable and the permission prompt dialog be ultra clear about what the user is about to commit to.</p>
<p>Even though I can style the background, color, border, border radius, and box shadow how I’d want them (as in the image below, which is most of what I’d want), this feels like a step back towards unstyleable controls. Having said that, this approach means well (<em>extremely</em> well), so I can’t wait to see where this goes (and frankly, to be able to throw the aging Geolocation API in the bin).</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/geolocation-3.png" alt="A purple button element with white text, labelled &quot;use precise location&quot;"></p>
<h2>What now? What else?</h2>
<p>When all web browsers support <code>&lt;geolocation&gt;</code>, the user experience and developer experience will improve dramatically.</p>
<p>But that’s not all!</p>
<p>What was once the <code>&lt;permission&gt;</code> element is now the <code>&lt;geolocation&gt;</code> element, <code>&lt;install&gt;</code> element (which facilitates the installation of Progressive Web Apps), <code>&lt;usermedia&gt;</code> element (which facilitates access to the user’s camera and/or microphone), and <code>&lt;camera&gt;</code> and <code>&lt;microphone&gt;</code> elements (which facilitates access to them individually).</p>
<p>All of these are being trialed in Chrome as part of a larger initiative to improve the process and experience of requesting permission, which is bloody great, in my opinion.</p>
        
        ]]></description>
      <pubDate>Thu, 20 Aug 2026 11:55:00 +0000</pubDate>
      <link>https://piccalil.li/blog/a-look-at-the-geolocation-html-element-and-how-it-works/?ref=main-rss-feed</link>
      <dc:creator>Piccalilli - Everything</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5349017575</guid>
    </item>
    <item>
      <title><![CDATA[Working with ::highlight() using progressive enhancement]]></title>
      <description><![CDATA[<p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Pseudo-elements#highlight_pseudo-elements">Highlighting in CSS</a> has been beneficial for applying a highlight on specific text or text fragments during user selection, emphasizing a piece of information on a website, or visually emphasizing a text for the sake of branding. Particularly, this is pretty helpful when users want to scan your document from top to bottom because the <a href="https://www.nngroup.com/articles/concise-scannable-and-objective-how-to-write-for-the-web/">majority of people don’t read your document initially; they scan</a>.</p>
<p>On the web, text highlights are a good way to lay emphasis on text fragments through good ol’ CSS. From <code>::selection</code> for styling selected text to <code>::target-text</code> which styles highlighted text from Google searches, and in my opinion, CSS’ most powerful pseudo-element for highlighting: <code>::highlight()</code> which applies a custom highlight to a text fragment.</p>
<p>In this article, we will look into how the <code>::highlight()</code> pseudo-element works, the API behind this pseudo-element, and explore a fallback feature for this technology because it relies <strong>heavily on JavaScript (JS).</strong></p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/XJjwNap">::highlight() demo: pure text-shadows</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p></p>
<h1>The CSS Custom Highlight API</h1>
<p>Typically, a highlight or highlighted text is what you’d see during web searches or when you what to select a text fragment to copy, or even when you make a mistake in a word processor (the squiggly red underlines)<em>.</em> In CSS, you can achieve these through the highlight pseudo-elements like<code>::search-text</code>, <code>::selection</code>, <code>::spelling-error</code> , and <code>::grammar-error</code> . But, what about plain highlights like the demo above? That’s where the CSS Custom Highlight API comes in.</p>
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API">CSS Custom Highlight API</a> is an API for text highlighting on a range of text using JavaScript and CSS. It extends the pseudo-elements for highlighting (<code>::search-text</code> , <code>::selection</code>, <code>::spelling-error</code> ) and lets you customize text fragments with <code>::highlight()</code> and JavaScript. <code>::highlight()</code> is what will be our focus for this article, and how we can programmatically highlight text and its fragments using JavaScript and CSS.</p>
<h1>How ::highlight() works</h1>
<p>To create highlighted text like in the initial example, you need to know 4 total steps, which include:</p>
<ol>
<li>Creating the highlight buckets by creating instances of the <code>Highlight()</code> class</li>
<li>Register each highlight instance into <code>CSS.highlights</code></li>
<li>Create <code>Range()</code> objects each with different points on the text for highlighting and add <code>Range()</code> objects to its highlight instances</li>
<li>Style with <code>::highlight()</code> pseudo-element</li>
</ol>
<p>Before we move on with an example, let me explain something because this is where it might get tricky. What we do around here is <strong>build with <a href="https://piccalil.li/blog/its-about-time-i-tried-to-explain-what-progressive-enhancement-actually-is/">progressive enhancement</a> in mind first</strong>. So the first question you should ask before anything is “what happens <a href="https://piccalil.li/blog/a-handful-of-reasons-javascript-wont-be-available/"><em>when</em> JS fails</a>?”. Well, I’m glad you asked.</p>
<p>Let’s say, for our example, we want to create two highlight objects and use that to style a simple poetic text that reads “<em>fire and ice live inside every word.</em>” Because we care about our users, we set each text for highlight with the <code>&lt;mark&gt;</code> HTML tag setting the <code>class</code> attribute to either <code>gold</code> or <code>ice</code> , depending on the highlighting style we wish the text to have. This acts as a fallback highlight style in case our JavaScript fails or the browser doesn’t support custom highlighting.</p>
<p>We also apply an <code>id</code> to them in case JS is available too for our CSS Custom Highlight API. With all that in mind, our HTML would look like this:</p>
<pre><code>&lt;main&gt;
  &lt;h1&gt;Two Highlights Demo&lt;/h1&gt;
  &lt;p id="line"&gt;
    &lt;mark id="fire" class="gold"&gt;fire&lt;/mark&gt; and
    &lt;mark id="ice" class="ice"&gt;ice&lt;/mark&gt; live inside 
    &lt;mark id="every" class="ice"&gt;every&lt;/mark&gt; 
    &lt;mark id="word" class="gold"&gt;word&lt;/mark&gt;
  &lt;/p&gt;
&lt;/main&gt;
</code></pre>
<p>Then, we proceed to query each tagged word in our JS applying the <code>firstChild</code> property to each of them of get the element’s first child node which we will use later:</p>
<pre><code>const fireNode = document.querySelector("#fire").firstChild;
const everyNode = document.querySelector("#every").firstChild;
const iceNode = document.querySelector("#ice").firstChild;
const wordNode = document.querySelector("#word").firstChild;
</code></pre>
<p>Finally, we can proceed with the steps on creating a custom highlight in CSS.</p>
<p></p>
<h2>Create instances of <code>Highlight()</code> class</h2>
<p>In order to create a custom highlight, the first step is to create an instance of the <code>Highlight()</code> class which will house the highlight styling we want a text or text fragment to have. For our demo, we’ll be creating two highlight objects named using the <code>Highlight()</code> class. One to give a golden color representing fire and the other to give a blue color representing ice:</p>
<pre><code>const goldHL = new Highlight();
const iceHL = new Highlight();
</code></pre>
<h2>Register each highlight instance in CSS.highlights</h2>
<p>Next, we register the created highlight instances in the <code>HighlightRegistry</code> via <code>CSS.highlights</code> <code>set()</code> method. We map a valid CSS identifier to the instance for CSS styling later.</p>
<pre><code>CSS.highlights.set("hl-gold", goldHL);
CSS.highlights.set("hl-ice", iceHL);
</code></pre>
<h2>Create Range() objects each with different points on the text for highlighting</h2>
<p>In this step, we will be creating a <code>Range()</code> object for each text fragment we queried earlier for highlighting. We will then apply the highlight we want on each selected text. For the first word “fire”, we create a <code>Range()</code> object called <code>r1</code> , and we set the start node to the first letter “f” using <code>setStart</code> on <code>r1</code> . <code>setStart()</code> accepts two values. It accepts the node we’re targeting (in our case for “fire”, its <code>fireNode</code> ) and the index of the text on the node.</p>
<p>Now, because we want to target the whole text “fire”, we have to also set where the range will stop. And this will be set using <code>setEnd()</code> . <code>setEnd()</code> accepts two values like <code>setStart()</code> on the range object (<code>r1</code>). It accepts the node we’re targeting (<code>fireNode</code> ) and the index of the end text “e” (as in the “e” in “fire”) using <code>fireNode.textContent.length</code> - 1 which gives us the last index of the text.</p>
<p>Finally, we add the range object into the set instance.</p>
<pre><code>const r1 = new Range();
r1.setStart(fireNode, 0);
r1.setEnd(fireNode, fireNode.textContent.length - 1);
goldHL.add(r1);
</code></pre>
<p>This step is repeated for <code>everyNode</code>, <code>iceNode</code>, and <code>wordNode</code>. Typically, you’d want to use a loop for this, but because this is really small, writing it in a specific manner will suffice, especially to help you understand how this works too.</p>
<pre><code>const r2 = new Range();
r2.setStart(wordNode, 0);
r2.setEnd(wordNode, wordNode.textContent.length - 1);
goldHL.add(r2);

const r3 = new Range();
r3.setStart(iceNode, 0);
r3.setEnd(iceNode, iceNode.textContent.length - 1);
iceHL.add(r3);

const r4 = new Range();
r4.setStart(everyNode, 0);
r4.setEnd(everyNode, everyNode.textContent.length - 1);
iceHL.add(r4);
</code></pre>
<h2>Style with ::highlight() pseudo-element</h2>
<p>Remember how we said we should <strong>think progressive enhancement first?</strong> Well, in order to achieve that for this demo in particular, we need to style the <code>&lt;mark&gt;</code>ed highlighted text first, then, we style the <code>::highlight()</code> pseudo-element. For that to work, we styled text marked with the <code>gold</code> class to be golden in <code>oklch()</code> with a glowy text shadow of similar color. We style text <code>&lt;mark&gt;</code> ed with the <code>ice</code> class to be blueish in <code>oklch()</code> with a glowy text shadow of similar color:</p>
<pre><code>mark {
  background: none;
}

mark.gold {
  color: oklch(88% 0.16 75);
  text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
}

mark.ice {
  color: oklch(82% 0.1 215);
  text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/azBoBBo">::highlight() demo: two highlights without `::highlight()`</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>Viola! (or how do they say it?) It looks amazing! 🤩</p>
<p>Even without the styled <code>::highlight()</code> , it works out pretty well. But, that’s not our only aim though. We still need to add the styling for both our highlight objects.</p>
<pre><code>::highlight(hl-gold) {
  color: oklch(88% 0.16 75);
  text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
}

::highlight(hl-ice) {
  color: oklch(82% 0.1 215);
  text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
}
</code></pre>
<p>Done! It’s pretty much the same styling we did for our <code>&lt;mark&gt;</code> tag classes and in case JS fails or the browser doesn’t support it, we wrap the <code>::higlight()</code> pseudo-elements in a <code>@supports</code> container:</p>
<pre><code>@supports selector(::highlight(h1-gold)) {
  ::highlight(hl-gold) {
    color: oklch(88% 0.16 75);
    text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
  }

  ::highlight(hl-ice) {
    color: oklch(82% 0.1 215);
    text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
  }
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/qEqWZYN">::highlight() demo: two highlights with fallback</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p></p>
<h2>Will you be using ::highlight()?</h2>
<p>I know for sure I will be using this feature in my work. This article explained highlighting in CSS, the CSS Custom Highlight API, and how <code>::higlight()</code> works in CSS with a fallback provision, for when JavaScript isn’t available or the browser doesn’t support it.</p>
<p>If you’re looking to read up more about the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API">CSS Custom Highlight API</a>, MDN has a good guide on the topic for you.</p>
        
        ]]></description>
      <pubDate>Thu, 06 Aug 2026 11:55:00 +0000</pubDate>
      <link>https://piccalil.li/blog/working-with-highlight-using-progressive-enhancement/?ref=main-rss-feed</link>
      <dc:creator>Piccalilli - Everything</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5335569489</guid>
    </item>
    <item>
      <title><![CDATA[Introducing Microlighter]]></title>
      <description><![CDATA[<p>I made a smol client-side syntax highlighter that uses the CSS Custom Highlights API called <a href="https://davatron5000.github.io/microlighter/">MicroLighter</a>. I’ll talk about <em>why I made it</em> in a bit but first I wanted to… <em>ahem</em>… highlight… some of the features.</p>
<ul>
<li>Zero-dependencies</li>
<li>~2kb minifed+gzipped</li>
<li>Uses CSS <code>::highlight(token-name)</code>, not spans</li>
<li>Leverages <a href="https://macromates.com/manual/en/language_grammars">Textmate’s language grammars</a></li>
<li>Human-readable <code>light-dark()</code> themes</li>
<li>All languages/grammars loaded on-demand</li>
<li>Moves all non-highlight functionality to a <a href="https://davatron5000.github.io/microlighter/custom-element.html"><code>&lt;micro-lighter&gt;</code> custom element</a></li>
</ul>
<p>At some point I broke syntax highlighting on my Jekyll blog. I’ve used a handful of syntax highlighters over the years (Highlight.js, PrismJS, Rouge, Shiki, etc, etc) and I’ve felt the trade-offs between different client-side and server-side implementations. Faced with picking another, I knew I wanted to explore <a href="https://www.bram.us/2024/02/18/custom-highlight-api-for-syntax-highlighting/">Bramus’s technique of syntax highlighting with the CSS Custom Highlights API</a>.</p>
<p>There’s some limitations with the CSS <code>::highlight()</code> pseudo; no italics, no bold, no font swapping – but otherwise it’s pretty cool syntax to idiomatically express “I want to highlight this token” via CSS instead of injecting spans everywhere. Using the Highlight API means I avoid any DOM mutation and the scope of the library shrinks down to: scan code blocks using regex patterns and send <code>CSS.highlights.set(category, textRanges)</code> to highlight the code blocks.</p>
<p>I don’t need much syntax highlighting on this site. Not all posts have code and my code samples are a whole fifteen lines long at best. My struggle is that I swap languages often. I’ll do HTML, CSS, and JavaScript all in the same post. A little <code>bash</code> here, a little <code>ruby</code> there, some <code>markdown</code> as a treat. Using so all those languages grew the complexity beyond the limits of my regex-fu, so I decided to lean on Textmate’s established collections of patterns used by VS Code. And before I knew it, my little highlighter could do almost any language.</p>
<p>Knowing I use different languages often, one principle I established was that all language grammars should be auto-loaded on-demand to reduce configuration and bundle size. That way you only pay for what you use.</p>
<p>Inspired by PrismJS’s simplified token categories I flattened down Textmate’s granular token categories to a more human-friendly set, making it easier to style. In addition to that, one enormous nit-pick I have with codeblock styling is that light and dark themes are separate entities, so I merged them into one theme using <code>light-dark()</code>.</p>
<p>The last big opinion I baked in was that I wanted the syntax highlighter to do one job: infer language and highlight code in that language. With that a guideline, I moved all extra functionality (like line-numbers, etc) over to a web component. The vanilla web component adds about ~1 KiB in size, but co-locating UI into a UI primitive like native custom elements feels right and the ShadowDOM encapsulation makes it easy to separate the code from the presentational UI.</p>
<p>Obviously, I’m a web component pervert but it feels like a great separation of concerns versus trying to cram everything into core library.</p>
<h2 id="try-it-out">Try it out<a href="https://daverupert.com/2026/08/microlighter/#try-it-out" aria-label="Link to heading 'Try it out'" data-heading-content="Try it out" class="anchor"></a></h2>
<p>To get started on your site, I’d use the self-initializing minified bundle, but I’m also shipping ESM and a web component.</p>
<pre><code class="language-bash">npm install microlighter
</code></pre>
<pre><code class="language-html">&lt;script type="module" src="path/to/microlighter/microlighter.min.js"&gt;&lt;/script&gt;
</code></pre>
<p>Like I said above, not all my posts have syntax highlighting, so I even wait to import the script unless I know there’s a page with code on it.</p>
<pre><code class="language-js">if(document.querySelector('pre&gt;code').length) {
	import('path/to/microlighter/microlighter.min.js');
}
</code></pre>
<p>You can use the ESM version if you’re going to do something fancy yourself:</p>
<pre><code class="language-js">import { highlightAll } from 'microlighter'

highlightAll({
	selector: 'pre.onlyTheseGetHighlights'
})
</code></pre>
<p>And you can also use the web component if you want those extra features I talked about:</p>
<pre><code class="language-html">&lt;micro-lighter data-syntax-theme="github" line-numbers controls="copy"&gt;
  &lt;pre&gt;&lt;code&gt;Code goes here&lt;/code&gt;&lt;/pre&gt;
&lt;/micro-lighter&gt;
</code></pre>
<p>Web component classes are pretty extendible too so if I don’t support something you <em>need</em>, you can “fork” it by extending the base class and adding your own features.</p>
<p>Lastly, you can use one of the pre-supplied themes or roll your own. The basic structure is:</p>
<pre><code class="language-css">/**
 * Setup semantic `--syntax-*` tokens 
 * @value background | foreground | comment | keyword | 
 * operator |string | constant | function | type | variable |
 * property | tag | selector | inserted | deleted
 */
[data-syntax-theme="my-theme-name"] {
  color-scheme: light dark;

  /* Code block tokens */
  --syntax-background: light-dark(#f8f8f8, #3a3a3a);
  --syntax-foreground: light-dark(#3a3a3a, #f8f8f8);
  
  /* Highlight tokens */
  --syntax-comment: light-dark(#6e7781, #8b949e);
	--syntax-function: light-dark(#8250df, #d2a8ff);
  /* ...etc... */
}

[data-syntax-theme="my-theme-name"] pre:has(code) {
	background-color:var(--syntax-background);
	color:var(--syntax-foreground)
}

::highlight(comment) { color: var(--syntax-comment) }
::highlight(function) { color: var(--syntax-function) }
/* ...etc... */
</code></pre>
<p>And that’s MicroLighter. If you end up using it and trying it out, let me know what you think.</p>
]]></description>
      <pubDate>Wed, 19 Aug 2026 15:22:00 +0000</pubDate>
      <link>https://daverupert.com/2026/08/microlighter/</link>
      <dc:creator>daverupert.com</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5348119481</guid>
    </item>
    <item>
      <title><![CDATA[My HTML boilerplate in 2026]]></title>
      <description><![CDATA[
                                <p>Every element I use for the basic structure of a HTML document, with explanations why.</p><p>Five years ago, I shared the <a href="https://www.matuzo.at/blog/html-boilerplate/">HTML boilerplate I use</a> for most of my projects. Since then, a lot has happened in HTML. It's time to give it an update.</p>
<h2 id="my-boilerplate">My boilerplate</h2>
<p>This is the final document. Scroll down for details.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en" class="no-js"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width"&gt;
  &lt;meta name="text-scale" content="scale"&gt;

  &lt;title&gt;Unique page title - My Site&lt;/title&gt;

  &lt;script src="/js/render-blocking-scripts.js"&gt;&lt;/script&gt;
  &lt;script&gt;
    document.documentElement.classList.replace('no-js', 'js');
  &lt;/script&gt;

  &lt;link rel="stylesheet" href="/css/styles.css"&gt;
  &lt;link rel="stylesheet" href="/css/print.css" media="print"&gt;

  &lt;script src="/js/non-render-blocking-script.js" type="module"&gt;&lt;/script&gt;

  &lt;link rel="icon" href="/favicon.ico"&gt;
  &lt;link rel="icon" href="/favicon.svg" type="image/svg+xml"&gt;
  &lt;link rel="apple-touch-icon" href="/apple-touch-icon.png"&gt;
  &lt;link rel="manifest" href="/site.webmanifest"&gt;

  &lt;link rel="canonical" href="https://web.site/page"&gt;
  &lt;link rel="alternate" type="text/markdown" href="https://web.site/page.md"&gt;
  &lt;link rel="me" href="https://mastodon.social/@username"&gt;
  &lt;link rel="site.standard.publication" href="at://[DID]/site.standard.publication/[RKEY]"&gt;

  &lt;meta name="description" content="Page description"&gt;
  &lt;meta property="og:image" content="https://web.site/sm.jpg"&gt;
  &lt;meta property="og:url" content="https://web.site/page"&gt;
  &lt;meta name="theme-color" content="#FF5302"&gt;
  &lt;meta property="fediverse:creator" content="@user@mastodon.social" &gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;!-- Content --&gt;
  &lt;script src="/js/non-render-blocking-script.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<!--
## Boilerplate generator
This, of course, is only a standard minimal setup I use. Your needs and requirements are very likely different. That's why I've built a generator you can use to construct your own boilerplate.
<hni-cta>
[Launch generator](https://boilerplate.htmhell.dev)
</hni-cta>
-->
<h2 id="line-by-line-explanation">Line by line explanation</h2>
<h3 id="doctype-required">Doctype <span class="pill pill--required">required</span></h3>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;</code></pre>
<p>Since HTML is a living standard, we no longer need different doctypes, but we still have to define one for  <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Quirks_Mode_and_Standards_Mode">compatibility</a>.</p>
<h3 id="natural-language-required">Natural language <span class="pill pill--required">required</span></h3>
<pre><code class="language-html">&lt;html lang="en"&gt;</code></pre>
<p>The <code>lang</code> attribute is one of the most important attributes in HTML, because it’s powerful and responsible for many things. You can read more about it in <a href="https://adrianroselli.com/2015/01/on-use-of-lang-attribute.html">On Use of the Lang Attribute</a> and <a href="https://www.matuzo.at/blog/lang-attribute/">The lang attribute: browsers telling lies, telling sweet little lies</a>. Applied to the <code>html</code> element, it defines page's natural language. It contains a single “language tag” in the format defined in <a href="https://datatracker.ietf.org/doc/html/rfc5646">Tags for Identifying Languages (BCP47)</a>, for example, <code>en</code> for English, <code>de</code> for German, or <code>fr</code> for French.</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang">lang Language tag syntax MDN</a></li>
</ul>
<h3 id="selector-for-no-javascript-environments-optional">Selector for no JavaScript environments <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;html class="no-js"&gt;</code></pre>
<p>I use the <code>no-js</code> class in case I want to apply styling to specific components in browsers that don’t support JavaScript or in browsers where the user has disabled JavaScript. This class will be removed in browsers that support and execute JavaScript. Even if you don't believe that websites should work without JavaScript in 2026, this class can still be useful for optimizing the rendering of web components before they're defined.</p>
<h3 id="character-encoding-required">Character encoding <span class="pill pill--required">required</span></h3>
<pre><code class="language-html">&lt;meta charset="UTF-8"&gt;</code></pre>
<p>This attribute declares the document’s character encoding. Leaving it off might cause specific characters to display incorrectly in some browsers.</p>
<p>It must come before the <code>&lt;title&gt;</code> element to avoid faulty characters in the page title.</p>
<h3 id="viewport-essential">Viewport <span class="pill pill--essential">essential</span></h3>
<pre><code class="language-html">&lt;meta name="viewport" content="width=device-width"&gt;</code></pre>
<p>The viewport meta tag allows us to adjust the viewport width, which is necessary for responsive web design. <code>width=device-width</code> sets the viewport width to the device's width. (That's a simplified explanation. For details, see the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/name/viewport#viewport_width_and_screen_width">MDN page for this meta tag</a>.)</p>
<p>Setting <code>initial-scale=1</code>, which controls the zoom level when the page is first loaded, shouldn't be necessary anymore. We only needed it for older versions of iOS and Android. The thing is, though, people keep telling me they think there are still edge cases where it's needed, but they don't remember which ones. Anyway, I don't think it's necessary, but if you want to play it safe, it doesn't hurt to add it.</p>
<pre><code class="language-html">&lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;</code></pre>
<p>The <code>viewport</code> meta tag should come as early as possible in the document <a href="https://github.com/joshbuchea/HEAD#recommended-minimum">to ensure proper document rendering</a>.</p>
<h3 id="text-scaling-optional">Text scaling <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta name="text-scale" content="scale"&gt;</code></pre>
<p>By default, mobile browsers don't respect the text size set in the operating system. This new meta tag changes that.</p>
<p>Don't just copy and paste this line. It might break your layouts. You have to test whether the content still looks okay when you increase the text size on your mobile operating system.</p>
<ul>
<li><a href="https://www.joshtumath.uk/posts/2026-01-27-try-text-scaling-support-in-chrome-canary/">Try text scaling support in Chrome Canary - Josh Tumath</a></li>
<li><a href="http://localhost/blog/2026/text-scaling-meta-tag">A new meta tag for respecting text scaling on mobile</a></li>
</ul>
<h3 id="the-page-title-required">The page title <span class="pill pill--required">required</span></h3>
<pre><code class="language-html">&lt;title&gt;Unique page title - My Site&lt;/title&gt;</code></pre>
<p>The unique title of the page. It’s displayed in many places, for example, on the browser tab, in search engine results, when you save a page as a bookmark, etc.</p>
<ul>
<li><a href="https://www.w3.org/WAI/tips/writing/#provide-informative-unique-page-titles">Provide informative, unique page titles</a></li>
<li><a href="https://hidde.blog/accessible-page-titles-in-a-single-page-app/">Accessible page titles in a Single Page App</a></li>
</ul>
<h3 id="general-render-blocking-js-optional">General render-blocking JS <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;script src="/js/render-blocking-scripts.js"&gt;&lt;/script&gt;</code></pre>
<p>Render-blocking JavaScript for the site. It comes before synchronous CSS because CSS blocks loading of the JavaScript. Put JavaScript without <code>async</code>, <code>defer</code>, or <code>type="module"</code> only in the head if you need the JavaScript parsed and ready before the rest of the page loads.</p>
<h3 id="javascript-support-optional">JavaScript support <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;script&gt;
  document.documentElement.classList.replace('no-js', 'js');
&lt;/script&gt;</code></pre>
<p>As mentioned earlier, I use this technique to create a dedicated class for the page state when JavaScript is disabled or unsupported. This solution is <a href="https://ryanmulligan.dev/blog/detect-js-support-in-css/#issues">more reliable than the scripting meta tag</a> and can be used alongside <code>&lt;noscript&gt;</code>.</p>
<h3 id="general-css-optional">General CSS <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;link rel="stylesheet" href="/css/styles.css"&gt;</code></pre>
<p>Render-blocking CSS for the site.</p>
<h3 id="print-css-optional">Print CSS <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;link rel="stylesheet" href="/css/print.css" media="print"&gt;</code></pre>
<p>People still print web pages, and I consider serving a print style sheet good UX. A good print stylesheet also saves paper and ink.</p>
<ul>
<li><a href="https://www.matuzo.at/blog/i-totally-forgot-about-print-style-sheets/">I totally forgot about print style sheets</a></li>
</ul>
<h3 id="general-non-render-blocking-js-optional">General non-render-blocking JS <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;script src="/js/non-render-blocking-script.js" type="module"&gt;&lt;/script&gt;</code></pre>
<p><code>type=module</code> implicitly defers the execution of the script after the document has been parsed, but before firing DOMContentLoaded event. For good performace try to default to this instead of adding render-blocking scripts.</p>
<h3 id="favicon-essential">Favicon <span class="pill pill--essential">essential</span></h3>
<pre><code class="language-html">&lt;link rel="icon" href="/favicon.ico"&gt;</code></pre>
<p>A 32×32px favicon for legacy browsers (Safari supports SVG favicons <a href="https://caniuse.com/link-icon-svg">starting with 26.0</a>). The favicon.ico should be located at the root of your website. Usually that's enough for browsers to pick it up automatically. You don't need the link element, but based on my tests with iOS 18, as soon as you define an SVG icon, the browser no longer falls back to the favicon.ico file in the root; it just doesn't display any icon. If you want to ensure that older browsers still show the ICO file while newer browsers get the SVG, you have to define both link elements. If you don't care about older versions of Safari, it's fine to just link the SVG.</p>
<pre><code class="language-html">&lt;link rel="icon" href="/favicon.svg" type="image/svg+xml"&gt;</code></pre>
<p>Most modern browser support <a href="https://caniuse.com/link-icon-svg">SVG favicons</a>. The benefits of the <code>favicon.svg</code> are that it looks better when it’s scaled, because it’s a vector and not raster image, and we can add HTML and CSS to the SVG, which means that we can support dark mode.</p>
<pre><code class="language-html">&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"&gt;
  &lt;style&gt;
    path {
      --fill: #153a51;
      fill: var(--fill);

      @media (prefers-color-scheme: dark) {
        --fill: #FFFFFF;
      }
    }
  &lt;/style&gt;
  &lt;path d="…"/&gt;
&lt;/svg&gt;</code></pre>
<pre><code class="language-html">&lt;link rel="apple-touch-icon" href="/apple-touch-icon.png"&gt;</code></pre>
<p>The 180×180px icon Apple devices will use if you add the page to your home screen. According to my tests, simply having a file named apple-touch-icon.png at the root of your project is enough. You don't need the link element, but I've only tested this with a simulated iOS 18 and a real device running iOS 26. I don't know about other operating systems and devices. So it's probably best to just keep it in your HTML.</p>
<ul>
<li><a href="https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs">How to Favicon in 2026: Three files that fit most needs</a></li>
</ul>
<h3 id="manifest-essential">Manifest <span class="pill pill--essential">essential</span></h3>
<pre><code class="language-html">&lt;link rel="manifest" href="/site.webmanifest"&gt;</code></pre>
<p>For Android devices we need a <code>.webmanifest</code> file, which provides browsers with the information where the icons needed for the home screen, dialogs, and the splash screen for <abbr title="Progressive Web Apps">PWAs</abbr> or regular sites added to the home screen are located.</p>
<pre><code class="language-js">{
    "name": "My Website",
    "icons": [
        { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
        { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" },
        { "src": "/icon-512_maskable.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" }
    ]
}</code></pre>
<p>The maskable variation of the icon is used when the icon is not displayed in a rectangular shape but, for example, in a circular one. The rule here is that your icon lives in a <a href="https://w3c.github.io/manifest/#icon-masks">centered safe zone</a>, which is approximately 80% of the size.</p>
<h3 id="canonical-url-essential">Canonical URL <span class="pill pill--essential">essential</span></h3>
<pre><code class="language-html">&lt;link rel="canonical" href="https://www.mywebsite.com/page"&gt;</code></pre>
<p>Use the <code>canonical</code> link element to prevent SEO issues caused by duplicate content by specifying the original source for pages that are available on multiple URLs.</p>
<h3 id="link-to-markdown-optional">Link to Markdown <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;link rel="alternate" type="text/markdown" href="https://web.site/page.md"&gt;</code></pre>
<p>Link to a Markdown version of your page. LLMs work better with Markdown than with HTML, and serving content as Markdown significantly reduces token usage.</p>
<ul>
<li><a href="https://evilmartians.com/chronicles/how-to-make-your-website-visible-to-llms">Making your site visible to LLMs: 6 techniques that work, 8 that don't</a></li>
</ul>
<h3 id="verification-on-mastodon-optional">Verification on Mastodon <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;link rel="me" href="https://mastodon.social/@username"&gt;</code></pre>
<p>This link tag can be used to verify that you actually own a page you've listed on your Mastodon profile.</p>
<p>That's not the only way to do it. You can also add <code>rel="me"</code> to a link to your profile anywhere on your website. Then you wouldn't need the link tag anymore.</p>
<pre><code class="language-html">&lt;a rel="me" href="https://mastodon.social/@username"&gt;Mastodon&lt;/a&gt;</code></pre>
<h3 id="standard-site-optional">Standard site <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;link rel="site.standard.publication" href="at://[DID]/site.standard.publication/[RKEY]"&gt;</code></pre>
<p>The link tag you need to publish on the atmosphere.</p>
<ul>
<li><a href="https://piccalil.li/blog/publishing-on-the-atmosphere-with-standardsite/">Publishing on the Atmosphere with Standard.site - Piccalilli</a></li>
</ul>
<h3 id="page-description-essential">Page description <span class="pill pill--essential">essential</span></h3>
<pre><code class="language-html">&lt;meta name="description" content="Page description"&gt;</code></pre>
<p>The unique description of the page, for example, displayed on search result pages. It can be any length, but search engines truncate snippets to ~155–160 characters.</p>
<p>These days, search engines don't guarantee they'll use your description. They may use <a href="https://developers.google.com/search/docs/appearance/snippet#how-snippet-created">different content or create their own summary</a>.</p>
<h3 id="preview-image-for-social-media-chats-etc-essential">Preview image for social media, chats, etc. <span class="pill pill--essential">essential</span></h3>
<pre><code class="language-html">&lt;meta property="og:image" content="https://web.site/sm.jpg"&gt;</code></pre>
<p>The image displayed when you share the link to a page on social media, chat applications, or other sites that scrape URLs.</p>
<ul>
<li><a href="https://ogp.me">The Open Graph protocol</a></li>
</ul>
<h3 id="canonical-url-for-social-media-essential">Canonical URL for social media <span class="pill pill--essential">essential</span></h3>
<pre><code class="language-html">&lt;meta property="og:url" content="https://web.site/page"&gt;</code></pre>
<p>The canonical URL of the page. A required property for valid Open Graph pages. This tag is necessary to ensure that platforms don't cache separate entities of your websites when links include GET parameters. If someone shares a link with, for example, <code>?utm_source=</code> it must not be treated as a separate entity. It's hard to tell wether there is a fallback to the canonical meta tag if the <code>og:url</code> meta tag doesn't exist. That's why I would include it. It's the same value as for the canonical meta tag.</p>
<h3 id="theme-optional">Theme <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta name="theme-color" content="#FF5302"&gt;</code></pre>
<p><code>theme-color</code> provides browsers with a CSS color to customize the display of the page or of the surrounding user interface.</p>
<p>This is currently only supported by Chromium based browsers on Android. From version 15 until 26, Safari also supported this meta tag, but starting with version 26, it either uses the body's background color or a color value from a position fixed element at the very top of the page.</p>
<ul>
<li><a href="https://grooovinger.com/notes/2026-02-27-safari-26-header-background">Define the Theme Color for Safari 26</a></li>
<li><a href="https://blog.tomayac.com/2019/09/21/prefers-color-scheme-in-svg-favicons-for-dark-mode-icons/">prefers-color-scheme in SVG favicons for dark mode icons</a></li>
</ul>
<h3 id="author-attribution-optional">Author attribution <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta property="fediverse:creator" content="@user@mastodon.social" &gt;</code></pre>
<p>This meta tag adds an additional line to links to your website posted on Mastodon, giving you credit.</p>
<img alt="Preview card in Slack showing the preview of a link with title, description, image, and the published date August 1st" height="192" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/html-boilerplate/11413becca-1786960561/mastodon_attribution.avif" width="600">
<p>For this to work, you have to add your domains to the allow list on Mastodon under Preferences -&gt; Public profile -&gt; Verification -&gt; Author attribution.</p>
<h3 id="general-non-render-blocking-js-optional">General non-render-blocking JS <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;script src="/js/non-render-blocking-scripts.js"&gt;&lt;/script&gt;</code></pre>
<p>Non-render-blocking JavaScript for the site.</p>
<hr>
<p>This isn’t the absolute minimum, but it’s what I need in most sites I build. To round things up, I’ve added a bunch of tags you might need from time to time.</p>
<h2 id="other-noteworthy-elements">Other noteworthy elements</h2>
<h3 id="dedicated-title-for-social-media-optional">Dedicated title for Social Media <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta property="og:title" content="Unique title"&gt;</code></pre>
<p>The title used by URL scrapers on social media platforms like Bluesky or Mastodon. You only need this if you want the title in the preview to be different from the page title. If you don't define it, apps and websites default to the page title. If you define it, also define a dedicated Open Graph description, because some apps, such as Discord, won't pick up the standard meta description anymore.</p>
<h3 id="dedicated-description-for-social-media-optional">Dedicated description for Social Media <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta property="og:description" content="Unique description"&gt;</code></pre>
<p>The description used by URL scrapers on social media platforms like Bluesky or Mastodon. You only need this if you want the description in the preview to be different from thes standard meta description.</p>
<h3 id="alt-for-preview-images-optional">Alt for preview images <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta property="og:image:alt" content="Image description"&gt;</code></pre>
<p>A description of the preview image. Whether the website or app uses it is really up to them. In my brief and incomplete tests, I found the situation very complicated.</p>
<ul>
<li>Bluesky ignores it.</li>
<li>Mastodon respects it, but it's part of the card's accessible name. The link text of a preview card contains the description of the image, the title, and the description. Mastodon uses an empty alt if you omit the meta tag.</li>
<li>Slack ignores it entirely.</li>
<li>Discord respects it, but falls back to "Image" if you omit the meta tag.</li>
</ul>
<p>I tried using an empty string, but it is also ignored by all the websites I tested. If you add alt text to the preview image, keep it short.</p>
<h3 id="type-optional">Type <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta property="og:type" content="article"&gt;</code></pre>
<p>The <a href="https://ogp.me/#types">type of content</a> you’re sharing, e.g. <code>website</code>, <code>article</code>, or <code>video.movie</code>. If you omit it, it defaults to <code>website</code>, which is what you want for most pages. Whether you see a difference when you change the type depends on the website or application rendering the preview. In my very limited testing, the only significant difference I saw was that if you set the type to <code>article</code> and added published and modified dates, Slack will display the published date and Signal the modified date.</p>
<pre><code class="language-html">&lt;meta property="article:published_time" content="2026-08-01T09:00:00+02:00"&gt;
&lt;meta property="article:modified_time" content="2026-08-03T14:30:00+02:00"&gt;</code></pre>
<figure>
<img alt="Preview card in Slack showing the preview of a link with title, description, image, and the published date August 1st" height="281" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/html-boilerplate/e6a0a3c144-1786960561/og_slack.avif" width="306">
<figcaption>Slack showing the publish date</figcaption>
</figure>
<figure>
<img alt="Preview card in Signal showing the preview of a link with title, description, image, and the modified date August 3rd" height="281" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/html-boilerplate/3c093ec286-1786960561/og_signal.avif" width="306">
<figcaption>Signal showing the modified date</figcaption>
</figure>
<h3 id="language-for-social-media-previews-optional">Language for social media previews <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta property="og:locale" content="en_GB"&gt;</code></pre>
<p>An optional Open Graph property. It defines the page's natural language. Bluesky ignores this, but Mastodon does not. If you don't define the locale, Mastodon will just fall back to the page's lang attribute, which I assume most applications will do too. I guess it only makes sense to define the locale if the language of the title and preview description is different from the language of the post.</p>
<h3 id="preloading-optional">Preloading <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin&gt;</code></pre>
<p>Use <code>preload</code> if you want to ensure that specific resources are available earlier in the page lifecycle.</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content">Preloading content with rel="preload"</a></li>
<li><a href="https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf">Preload, Prefetch And Priorities in Chrome</a></li>
<li><a href="https://css-tricks.com/prefetching-preloading-prebrowsing/">Prefetching, preloading, prebrowsing</a></li>
</ul>
<h3 id="rss-optional">RSS <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;link type="application/atom+xml" rel="alternate" href="/feed.xml" title="My Blog - Manuel Matuzovic"&gt;</code></pre>
<p>RSS feed for your site.</p>
<ul>
<li><a href="https://www.htmhell.dev/adventcalendar/2025/14/">The Wonderful World of Web Feeds</a></li>
</ul>
<h3 id="the-document-s-author-optional">The document's author <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta name="author" content="Manuel Matuzović"&gt;</code></pre>
<p>Websites or apps may want to know who the author of the document is.</p>
<h3 id="the-notch-optional">The notch <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta name="viewport" content="width=device-width, viewport-fit=cover" /&gt;</code></pre>
<p>By default, the content on a device like the iPhone is within a safe area kept away from the notch and rounded corners.</p>
<figure>
<img alt="A page in landscape mode. A heading with a black background. White bars on the left and right (the safe space)" height="461" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/html-boilerplate/8e5ae6d4ea-1786960561/notch_default.avif" width="1000">
<figcaption>Default on Safari (note: the body has no explicit background color)</figcaption>
</figure>
<p>If this is an issue, you can opt out of that behavior and take up the entire screen edge-to-edge.</p>
<figure>
<img alt="A page in landscape mode. A heading with a black backgroundcover the entire screen" height="461" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/html-boilerplate/f7a4765549-1786960561/notch_cover.avif" width="1000">
<figcaption>Same page with viewport-fit=cover</figcaption>
</figure>
<h3 id="viewport-resize-behavior-optional">Viewport resize behavior <span class="pill pill--optional">optional</span></h3>
<pre><code class="language-html">&lt;meta name="viewport" content="width=device-width, interactive-widget=resizes-content"&gt;</code></pre>
<p>This option allows you to control how the viewport resizes on mobile when the keyboard is shown. With <code>resizes-content</code>, not just the visual viewport, but also the layout viewport, resizes. This fixes an issue where sticky or fixed elements at the bottom of the screen would disappear when the keyboard is shown. I haven't used it yet, so I don't know how it affects UX, performance, or accessibility. I guess the best advice is to use this option only where you need it and test it properly.</p>
<ul>
<li><a href="https://www.htmhell.dev/adventcalendar/2024/4/">Control the Viewport Resize Behavior on mobile with interactive-widget</a></li>
</ul>
<h3 id="color-scheme">Color scheme</h3>
<pre><code class="language-html">&lt;meta name="color-scheme" content="dark light" /&gt;</code></pre>
<p>You can use this meta tag to indicate to the user agent which color schemes the website supports.</p>
<hr>
<p>If you want to learn more about the <code>&lt;head&gt;</code> element and its children, check out Josh Buchea’s fantastic repository <a href="https://github.com/joshbuchea/HEAD">HEAD</a>.</p>
<p>Did I get anything wrong or did I miss anything? Please find me on <a href="https://matuzo.social">social media</a> or via <a href="mailto:manuel@matuzo.at">e-mail</a>.</p>
<h3 id="thanks">Thanks</h3>
<p>Thank you, Vale, Nathan, Šime, Thain, and Matt, for your feedback!</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CMy+HTML+boilerplate+in+2026%E2%80%9D">blog@matuzo.at</a>.</p>        ]]></description>
      <pubDate>Mon, 17 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://www.matuzo.at/blog/2026/html-boilerplate</link>
      <dc:creator>Manuel Matuzović - Blog</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5345491455</guid>
    </item>
    <item>
      <title><![CDATA[AI High]]></title>
      <description><![CDATA[<p>Recently, I watched <a href="https://www.youtube.com/watch?v=cVT7MbRBiLY">a video by Colin &amp; Samir</a> speaking about a situation <a href="https://www.hankgreen.com/">Hank Green</a> is currently facing. Hank was recently accused of using AI in a video script because he said the words “I appreciate the pushback.” Hank went on to address not only that particular video but also transparently explained his AI usage. Even with that transparency, there was backlash. Long-time followers of Hank were distraught because they believed Hank was getting lazy and not thinking for himself. But I picked up on something very small in the video, a small pause between the words “use” and “AI”.</p>
<p>I can’t know for sure if that pause was deliberate but something triggered in my brain that reminded me of how we call folks who partake in medicinal substances “users”. The connotation of a drug “user” is often negative but it gave me a better understanding of what’s going on with some analogies to how drugs exist in society today.</p>
<h2 id="on-maturity">On maturity</h2>
<p>I’d like to think my relationship with drugs is fairly normal. The only recreational substance I’ve ever done is alcohol and only ever socially. I have a solid self-discipline that doesn’t give in easily to peer pressure. I also don’t immediately reach for drugs just for minor inconveniences nor do I believe pumping myself with antibiotics is a good idea since your body then depends on that over time. However, it doesn’t mean I’ve never taken drugs. I’ve taken stuff for headaches and more significant ones when prescribed.</p>
<p>Importantly, this is only my experience. I know there’s a large spectrum out there, from folks who are straight-edge and haven’t done anything to folks who could be considered junkies. There’s folks that take drugs to regulate their own issues, and folks who want to try new experiences. There’s all kinds of people with all kinds of backgrounds, beliefs, and situations they are going through.</p>
<h2 id="on-addiction">On addiction</h2>
<p>When you look at AI in the same lens, I think the bottom line is addiction. If you are in a situation where you cannot function without AI, this is a dependence that you don’t want to have. We’re seeing a lot of AI slop because people have an addiction. It’s basically a super performance enhancing drug that anyone can use. As Uncle Ben says to Peter Parker: “With great power comes great responsibility.” There are many people who are irresponsible with AI and cannot self-regulate. Some of these folks are in leadership positions that affect the livelihood of the folks around them. This is the challenge that society is facing today. I think we need to address this in the same way we might address someone who is abusing substances.</p>
<p>It takes a big person to recognize when they have a problem, and otherwise it’ll take help from others for folks to realize their abuse. It’s not healthy if you’ve thrown out all of your processes and experts in favor of the dopamine hit that AI will give you. You have to use it with self-control. I’m seeing some of the effects in peers, where token limits are being constrained. These folks feel helpless without it because they relied too heavily on the dependency.</p>
<h2 id="on-shame">On shame</h2>
<p>By the same token, I don’t think it’s right to chastise someone for using AI. I feel this is akin to straight-edge folks who look down upon people recreationally drinking or vegetarians scolding meat-eaters. I don’t see AI as inherently bad. I think there’s good qualities to AI. An example I think about all the time is trying to understand how to get something done in a new tool. Before AI, I’d search on the Internet for something like creating an image mask in Affinity, only to find forum posts with people arguing about the best way to do it or YouTube videos that spend 10 minutes introducing themselves and the tool. Having something that cuts through all of the fat to unblock you is helpful. Especially if you are in the creative flow. Just think of all the times you had to side quest and get distracted from the main task because people put up unnecessary barriers to farm engagement or unknowingly tangent from the purpose. You’re still in learning mode, just with less friction.</p>
<p>For full transparency, each of the posts here on the blog has a random animal illustration associated with it. Each of these is generated with AI and has been for years. The animal images provide no value to the blog beyond matching the parody motif of the website’s design. If I wasn’t able to generate the drawings, it just means I might need to pivot. I’d have to weigh out the importance of continuing the style over the amount of time it would take to draw these animals myself. If I found that it isn’t worth it, which is likely the case, I’d just redesign the website. AI doesn’t define what I do, or drive the content on the site. I’m still writing every word by hand. That’s the value here. There’s areas where AI is helping. I see this the same as taking an Advil to get rid of a headache quickly instead of wasting time trying to sleep it off.</p>
<p>As another example, I’ve just started a <a href="https://news.ds.house">newsletter at Design Systems House</a>. While the short sentences found in the newsletter are AI generated, the links found inside are not. I could just send you a sterile list of links with zero AI involvement or I could dress them up with something that summarizes what you are getting; helping you determine if the link is worth your time to read without clicking it. Am I lazy for having AI summarize the links? Maybe. Or maybe I prefer redirecting my energy to the projects I’m sharing in that news.</p>
<p>I have empathy for <del>users</del> people. I care about the things I’m making and I think about how they affect people. I can’t please everyone, but I have confidence that my use of AI isn’t addiction. Maybe that’s exactly what an addict would say. But I do believe if AI were to disappear tomorrow, I could continue being creative because I’ve always had those skills. Not the Markdown files, the real expertise that I’ve gained through learning. I value being able to do this stuff no matter what.</p>
<p>Prompt responsibly.</p>]]></description>
      <pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://blog.damato.design/posts/ai-high</link>
      <dc:creator>blog.damato.design</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5342730085</guid>
    </item>
    <item>
      <title><![CDATA[The golden rule of Customizable Select]]></title>
      <description><![CDATA[<p>Customizable select is coming to Safari 27. With this technology, developers can fully control the appearance of <code>&lt;select&gt;</code> elements — custom arrows, option layouts, color swatches, icons, full visual styling — without the need for JavaScript libraries or an endless parade of <code>&lt;div&gt;</code> elements. And because it’s a built-in control, you don’t have to compromise on keyboard navigation or accessibility semantics.</p>
<p>But, to ensure this built-in control works well for everyone, it’s important to follow this single but essential rule: <strong>always provide text content or accessible text attributes for your <code>option</code> elements.</strong></p>
<p>Every time that rule is broken, every time an option is styled to show a visual without any text and without any accessible fallbacks, three different problems get introduced all at once. The menu is harder to use for everyone, impossible to use with accessibility tools, and it becomes a completely broken experience in browsers that don’t support it yet.</p>
<p>When you remember to follow the rule, you’ll improve the user experience, support accessibility, and provide progressive enhancement so it works for people regardless of what browser they choose.</p>
<p>We’ll show you why following this mission critical rule gets you:</p>
<h2>Better UX</h2>
<p>Take this category filter from a photographer’s gallery site. The version below uses icons alone — a building, a flower, a hummingbird — to represent each category:</p>
<figure><picture><source srcset="https://webkit.org/wp-content/uploads/customizable-select-dark-no-labels.png" type="image/png" media="(prefers-color-scheme: dark)"><img fetchpriority="high" decoding="async" src="https://webkit.org/wp-content/uploads/customizable-select-no-labels-LIGHT.png" alt="customizable select of categories for photo gallery with no labels" width="1920" height="1080" class="aligncenter size-full wp-image-17357 preserve-color"></picture></figure>
<p>It looks clean. But a user who doesn’t immediately recognize what the hummingbird icon represents has no fallback. The closed select shows only an icon in the button, with no other hint of what’s currently selected. Add a text label to each option and the experience becomes immediately scannable. The selected state is readable at a glance, and every option is unambiguous:</p>
<figure><picture><source srcset="https://webkit.org/wp-content/uploads/customizable-select-dark-with-labels.png" type="image/png" media="(prefers-color-scheme: dark)"><img decoding="async" src="https://webkit.org/wp-content/uploads/customizable-select-with-labels-LIGHT.png" alt="customizable select of categories for photo gallery with labels" width="1920" height="1080" class="aligncenter size-full wp-image-17357 preserve-color"></picture></figure>
<p>The icons are still there. The labels make it readily decipherable for everyone.</p>
<h2>Better accessibility</h2>
<p>When a screen reader encounters an option with no text, the user may not hear a descriptive label for each option. Braille rendering and other assistive technology output may also be confusing. Text, even when hidden visually with a <code>.visually-hidden</code> class, stays in the accessibility tree and gives screen readers, braille displays, and speech recognition software something real to work with. If you use an icon as an <code>&lt;img&gt;</code>, add an <code>alt</code> or <code>aria-label</code> — or mark it decorative using <code>alt=""</code> and let the visible or visually-hidden label carry the meaning.</p>
<pre><code class="html"><span class="tag">&lt;<span class="keyword">option</span>&gt;</span>
   <span class="tag">&lt;<span class="keyword">img</span> <span class="keyword attribute">src</span>=<span class="attribute value string">"bird.svg"</span> <span class="keyword attribute">alt</span>=<span class="attribute value string">""</span>&gt;</span>
   <span class="tag">&lt;<span class="keyword">span</span>&gt;</span>Wildlife<span class="tag">&lt;/<span class="keyword">span</span>&gt;</span>
<span class="tag">&lt;/<span class="keyword">option</span>&gt;</span>
</code></pre>
<p>The problem you solve isn’t just a compliance checkbox: it’s the difference between a visitor completing your form and someone abandoning it.</p>
<h2>Better progressive enhancement</h2>
<p>Customizable select is a new feature. Browsers that don’t yet support it fall back to the platform-native <code>&lt;select&gt;</code> — which is exactly the right behavior, as long as your options still make sense in that fallback state.</p>
<p>If you’ve removed text in favor of icons or swatches, a user on an older browser sees a dropdown full of empty options. The same is true when CSS fails to load at all: a slow connection, a corporate proxy stripping stylesheets, a user with custom styles enabled. Wrap your enhancements in <code>@supports (appearance: base-select)</code> and keep plain text as your baseline. Adding a swatch is an enhancement. Removing the color name to make room for it is a regression.</p>
<p>The rule for maximizing the power and utility of customizable select is simple: keep the text. You can hide it visually. You can make it tiny. You can position it off-screen. But it needs to be there. Icons, swatches, and illustrations are additions to an option — never substitutes for it. Follow that rule and the rest of customizable select is yours to play with.</p>
<h2>Feedback</h2>
<p>We love hearing from you. To share your thoughts, find our web evangelists online: Saron Yitbarek on <a href="https://bsky.app/profile/saron.bsky.social">BlueSky</a>, Jen Simmons on <a href="https://bsky.app/profile/jensimmons.bsky.social">Bluesky</a> / <a href="https://front-end.social/@jensimmons">Mastodon</a>, and Jon Davis on <a href="https://bsky.app/profile/jondavis.bsky.social">Bluesky</a> / <a href="https://mastodon.social/@jondavis">Mastodon</a>. You can follow WebKit <a href="https://www.linkedin.com/in/apple-webkit/">on LinkedIn</a>. If you run into any issues, we welcome your <a href="https://feedbackassistant.apple.com/">feedback</a> on Safari UI (learn more about <a href="https://developer.apple.com/bug-reporting/">filing Feedback</a>), or your <a href="https://bugs.webkit.org/">WebKit bug report</a> about web technologies or Web Inspector. If you run into a website that isn’t working as expected, please file a report at <a href="https://webcompat.com/">webcompat.com</a>. Filing issues really does make a difference.</p>
]]></description>
      <pubDate>Mon, 15 Jun 2026 21:20:31 +0000</pubDate>
      <link>https://webkit.org/blog/18117/the-golden-rule-of-customizable-select/</link>
      <dc:creator>WebKit</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5260881939</guid>
    </item>
    <item>
      <title><![CDATA[Don't Use A Class For That!]]></title>
      <description><![CDATA[Developers love classes. They pile on custom classes everywhere, but HTML already counts with many attributes that do a better job. So why not use them?
This article goes through some of the most common native attributes that can be used instead of a class.]]></description>
      <pubDate>Fri, 07 Aug 2026 05:44:01 +0000</pubDate>
      <link>https://alvaromontoro.com/blog/68107/dont-use-a-class-for-that</link>
      <dc:creator>Alvaro Montoro&#39;s website</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5336673826</guid>
    </item>
    <item>
      <title><![CDATA[Who Is Legally to Blame for Autonomous A.I. Hacks?]]></title>
      <description><![CDATA[<p><a href="https://techcrunch.com/2026/08/03/whos-legally-to-blame-for-anthropic-and-openais-autonomous-ai-hacks-its-complicated/">Lorenzo Franceschi-Bicchierai and Zack Whittaker</a>, <em>TechCrunch</em>:</p>

<blockquote>
  <p>Under current U.S. hacking laws, a human can face criminal charges for breaking into someone else’s computer without permission. But when an AI agent autonomously hacks into a company’s computers, determining who is liable is much murkier.</p>
  
  <p>The surprise admissions by OpenAI and Anthropic that their unreleased AI models autonomously hacked into several companies have upended our understanding of America’s computer hacking laws, prompting discussions over whether the companies could face legal reprisals.</p>
</blockquote>

<p>You can <a href="https://www.bleepingcomputer.com/news/security/meta-ai-model-hacked-a-company-during-misconfigured-cyber-test/">add Meta</a> to the list of companies that have done some light digital breaking-and-entering because they insufficiently secured an A.I. model. In any case, I am fascinated by the seemingly pervasive idea that obviously illegal things somehow become a grey area when a highly-valued A.I. company is responsible. Corporations now <a href="https://arstechnica.com/tech-policy/2025/03/meta-mocked-for-raising-bob-dylan-defense-of-torrenting-in-ai-copyright-fight/">allegedly authorize mass piracy at the highest levels</a> and <a href="https://pxlnv.com/linklog/openai-pirated-ebooks/">use illicit movie rips</a> as though it is just another day.</p>
<p><a href="https://pxlnv.com/linklog/legal-blame-autonomous-hacks/" rel="bookmark" title="Permanent link to 'Who Is Legally to Blame for Autonomous A.I. Hacks?'" class="glyph">⌥ Permalink</a></p>
]]></description>
      <pubDate>Fri, 07 Aug 2026 00:31:11 +0000</pubDate>
      <link>https://pxlnv.com/linklog/legal-blame-autonomous-hacks/</link>
      <dc:creator>Pixel Envy</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5336162566</guid>
    </item>
    <item>
      <title><![CDATA[Dark mode toggles: two states are enough]]></title>
      <description><![CDATA[<figure style="display: grid; grid-template-columns: 1fr 1fr; --media-width: 100%;">
<object data="https://lea.verou.me/blog/2026/dark-mode-toggles/demo.svg#light-system-2" type="image/svg+xml"></object>
<object data="https://lea.verou.me/blog/2026/dark-mode-toggles/demo.svg#light-dark-2" type="image/svg+xml"></object>
<object data="https://lea.verou.me/blog/2026/dark-mode-toggles/demo.svg#dark-system-2" type="image/svg+xml"></object>
<object data="https://lea.verou.me/blog/2026/dark-mode-toggles/demo.svg#dark-light-2" type="image/svg+xml"></object>
<figcaption style="grid-column: 1 / -1;">
<p>A <a href="https://lea.verou.me/blog/2026/dark-mode-toggles/#good-two-state-ux">good two-state toggle</a> can actually express all three data model states.</p>
</figcaption>
</figure>
<p>Until recently, if you looked at most websites with a theme toggle<sup class="footnote-ref"><a href="https://lea.verou.me/blog/2026/dark-mode-toggles/#fn1" id="fnref1">[1]</a></sup>, you’d find three options: <strong>Light</strong>, <strong>Dark</strong>, and <strong>System</strong>.</p>
<style>
figure {
  --media-min-width: 5em;
  hr { width: 100%; min-width: 100%; border: 0px solid transparent; }
}
</style>
<figure class="multiple">
<p><img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/tailwind.png" alt="Tailwind">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/redhat.png" alt="RetHat Design System">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/ant.png" alt="Ant">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/webawesome.png" alt="Web Awesome" style="flex: .7;">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/excalidraw.png" alt="Excalidraw">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/taiga.png" alt="Taiga" style="flex: 1.2;">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/astro.png" alt="Astro" style="min-width: 100%;">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/heroui.png" alt="Hero UI" style="min-width: 100%;"></p>
<figcaption>
<p>Examples of tri-state dark mode toggles.
In (LTR) reading direction: Ant Design, Red Hat Design System, Web Awesome, Excalidraw, Taiga, Astro, Hero UI.</p>
</figcaption>
</figure>
<p>Thankfully, these days the trend has shifted towards a simpler two-state toggle, but tri-state ones are still incredibly common.</p>
<figure id="two-mode-examples" class="multiple" style="--media-min-width: 5em">
<p><img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/vitepress.png" alt="Vitepress">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/material.png" alt="Material" style="flex: .07">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/spectrum.png" alt="Spectrum">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/radix.png" alt="Radix" style="min-width: 100%;">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/shadcn.png" alt="Shadcn" style="min-width: 100%;"></p>
<figcaption>
<p>Examples of two-state dark mode toggles.
In (LTR) reading direction: Vitepress, Material Design, Adobe Spectrum, Radix, ShadCN.</p>
</figcaption>
</figure>
<p>The rationale sounds plausible:
“System” is a different intent than “Light” or “Dark”!
One is a policy (<em>whatever my OS says, do that</em>)
The other is a value (<em>dark, forever, I don’t care what my OS says.</em>)
Surely, users should be able to express that intent!</p>
<p>Except, real users don’t generally seek out dark mode toggles to express intent for things to stay as they are, they seek them out when things need to change.</p>
<p>Think of the user goal when browsing a website (as opposed to a separate Settings page, where <a href="https://lea.verou.me/blog/2026/dark-mode-toggles/#settings">three states are fine</a>).
E.g. on a documentation site, they may be there to look something up.
On a landing page, they may be trying to evaluate whether the product is suitable for their needs.<br>
On a media site, they may be there to read the news.<br>
On a graphics app, they want to draw something.</p>
<p>One thing is for certain: tweaking the theme is <em>not</em> their primary goal <sup class="footnote-ref"><a href="https://lea.verou.me/blog/2026/dark-mode-toggles/#fn2" id="fnref2">[2]</a></sup>.
To get in the mindset of tweaking the theme, <em>something needs to be off</em>.
When things look right, users just move on with their <em>actual</em> goal instead of thinking about the theme.</p>
<p><strong>The tri-state control is solving a largely imaginary user goal</strong> that is extremely rare among real users, and does not justify the additional complication and UX friction of a three-state toggle.</p>
<p>Worse, it forces the user to decide between choices that produce no visible difference, breaking the principle of <a href="https://www.nngroup.com/articles/ten-usability-heuristics/">feedback</a>.</p>
<p>Yes, tri-state toggles are <em>common</em>.
<a href="https://en.wikipedia.org/wiki/Argumentum_ad_populum">That doesn’t make them <em>good</em></a>.
This essay explains why, and how to do better.</p>
<h2 id="tri-state-toggles-are-implementation-driven-ui" tabindex="-1"><a class="header-anchor" href="https://lea.verou.me/blog/2026/dark-mode-toggles/#tri-state-toggles-are-implementation-driven-ui">Tri-state toggles are implementation-driven UI</a></h2>
<p>One of the most common UX mistakes is designing UI around the underlying data model instead of user goals.
<strong>Good interfaces abstract away the underlying model and expose a model that aligns with user goals</strong> (unless of course these happen to coincide, which is rare).</p>
<figure style="align-items: end;">
<p><img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/faucet-1.jpg" alt="">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/faucet-2.jpg" alt=""></p>
<figcaption>
The user goal here is to set a specific flow and temperature.
The underlying model works with amounts of hot and cold water.
Guess which faucet is easier to use?
</figcaption>
</figure>
<p>This is exactly the case with tri-state dark mode toggles; exposing all three states is data model leaking into the UI.</p>
<p>Yes, there should <em>absolutely</em> be three states in the underlying implementation!
But at any given point, one of them is irrelevant to the end-user.</p>
<p><strong>Users cannot meaningfully express intent about problems they don’t currently have.</strong></p>
<p>A dark mode toggle is a <strong>temporary comfort adjustment</strong>.
When it comes to user goals, there are only two real states:</p>
<ol>
<li>The website looks ok. The user moves on with their actual goal and doesn’t look for the toggle at all.</li>
<li>The website is too bright or too dark to be comfortable. The user wants to fix it.</li>
</ol>
<p>You’re reading in bed, the page is a flashbang, you hit the toggle.
You’re on a laptop outside and the dark theme is unreadable in sunlight, you hit the toggle.
It’s situational, it’s immediate, and it’s usually about the environment you’re in rather than a considered long-term stance on color schemes.</p>
<p>A third state assumes a usage scenario where a user visits a website that looks perfectly fine, and still looks for a dark mode toggle to ensure it can continue to look fine in the future.
Users do all sorts of weird things, so I won’t assert that this never happens, but it is not a natural user interaction, fueled by a real user goal.
Even the strongest proponents of tri-state toggles I have spoken with either admit they have never done this, or bring up some extremely rare, weird one-off edge cases.</p>
<aside class="note">
<p>I tried to ask on social media (<a href="https://bsky.app/profile/lea.verou.me/post/3msat5jppgk2n">Bsky</a>, <a href="https://x.com/LeaVerou/status/2084592593678008495">Twitter/X</a>, <a href="https://front-end.social/@leaverou/117036981996311229">Mastodon</a>, <a href="https://github.com/LeaVerou/blog/discussions/141">GitHub</a>), but no matter how hard I tried to word it well, the question kept getting so misunderstood that the data is too noisy to be useful.</p>
<p>Besides people misunderstanding the question and talking about these times where they want to override the OS theme, there were also developers answering about debugging use cases (rather than their actual user behavior), or people talking about that one time they accidentally clicked that option.
There was even someone who concluded that because they want the site to inherit the OS setting almost always, they should vote “frequently”! 😵‍💫</p>
<p>That said, I don’t think it matters all that much beyond academic curiosity, as a <a href="https://lea.verou.me/blog/2026/dark-mode-toggles/#good-two-state-ux">good two state control</a> can actually express all three states — users just need to apply the override the first time it becomes relevant.</p>
</aside>
<h2 id="but-what%E2%80%99s-the-harm%3F" tabindex="-1"><a class="header-anchor" href="https://lea.verou.me/blog/2026/dark-mode-toggles/#but-what%E2%80%99s-the-harm%3F">But what’s the harm?</a></h2>
<p>One could argue that sure, the third state is not frequently needed, but surely it doesn’t hurt to have it there for the one user that will need it, right?</p>
<p>But a more complex UI has a cost.
It increases cognitive load for interacting with the control and  forces you towards certain UI design decisions.</p>
<p>A two-state toggle can be very compact: Just a single icon that switches to another when clicked.</p>
<p>Some websites do go that route with a tri-state toggle that cycles through three states. Docusaurus for example:</p>
<p><img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/docusaurus-header.png" alt="Docusaurus header"></p>
<p>But generally, the ergonomics of that are poorer than for the two state toggle, so it is no surprise it’s rare (Docusaurus was the only example I could find).</p>
<p>Some tri-state controls go for three icons side by side, which triples the screen real estate used.</p>
<figure>
<p><img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/redhat.png" alt="RetHat Design System">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/heroui.png" alt="Hero UI" style="flex: 2.2">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/excalidraw.png" alt="Excalidraw">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/tailwind.png" alt="Tailwind"></p>
<figcaption>
Red Hat, Hero UI, Excalidraw, and Tailwind display three icons side by side.
</figcaption>
</figure>
<p>Others, in an attempt to balance clarity and real estate, resort to a dropdown:</p>
<figure class="multiple">
<p><img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/ant.png" alt="Ant">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/webawesome.png" alt="Web Awesome">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/taiga.png" alt="Taiga">
<img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/astro.png" alt="Astro" style="min-width: 100%;"></p>
<figcaption>
Ant Design, Web Awesome, and Taiga UI go this route.
Points for Web Awesome being the only one (!) to actually display what the system default resolves to.
</figcaption>
</figure>
<p>That improves learnability, at the cost of efficiency, as it turns a single click interaction into a two-step process.</p>
<p>The actual <em>perceived friction</em> is actually worse than one extra click.
Perceived friction is not a pure function of user actions, but also of the mental effort required to make a decision, and larger UI shifts (e.g. opening a dropdown) are more cognitively expensive than smaller ones (e.g. clicking a toggle) as the user needs to perceive and interpret a larger area.</p>
<h2 id="good-two-state-ux" tabindex="-1"><a class="header-anchor" href="https://lea.verou.me/blog/2026/dark-mode-toggles/#good-two-state-ux">If not three states, then what?</a></h2>
<p>Guidance towards using tri-state controls is well meaning, but often based on paring good tri-state controls against poor two-state ones.
E.g. in <a href="https://www.bram.us/2022/05/25/dark-mode-toggles-should-be-a-browser-feature/#the-problem-with-dark-mode-toggles">this article by Bramus</a>:</p>
<blockquote>
<p>Above that many implementations I have seen don’t take the “System” value into account. By omitting this option, the sites will never be able to respond to the system preference again, as they always have an override applied.</p>
</blockquote>
<p>Indeed, a bad two state toggle is worse than a tri-state one.
It makes the system mode unreachable once tweaked, making the selection <strong>irreversible</strong> and violating the usability principle of <a href="https://www.nngroup.com/articles/ten-usability-heuristics/">user control and freedom</a>.
A good two-state should be able to express all three states.</p>
<p>The idea is that <strong>the underlying model is still three states, but only two are shown at any given time</strong>:</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Shown as</th>
<th>Stored value</th>
</tr>
</thead>
<tbody>
<tr>
<td>System default</td>
<td>Current resolved value (e.g. sun or moon)</td>
<td>None</td>
</tr>
<tr>
<td>Override</td>
<td>Opposite of current resolved value (e.g. moon or sun)</td>
<td><code>light</code> or <code>dark</code></td>
</tr>
</tbody>
</table>
<p>When you press it for the first time, it toggles to the opposite of what you’re currently seeing, and stores the literal value (<code>light</code> or <code>dark</code>).
The next time you press it, <strong>it toggles back to the system default</strong>, and removes the stored value.</p>
<p>That last bit is the one many two-state toggles get wrong.
Storing a value that happens to match the system preference silently converts a temporary adjustment into a permanent pin with no way out.</p>
<p>Another common mistake is being overzealous about removing the stored value when the system preference changes, even if the user has explicitly set an override.</p>
<p><strong>This evaluation must only happen at user interaction.</strong></p>
<p>This is important because many users have their OS set to automatically switch between light and dark mode based on time of day, and removing the stored value proactively would make it impossible for them to actually pin a theme.</p>
<p>If a stored override later happens to coincide with the system preference — because the OS changed, not because the user did anything — you <strong>keep it</strong>.</p>
<p>This looks like an oversight — they’re the same now, why not tidy up?
Because tidying up silently downgrades an explicit choice into a default, based on an event the user didn’t cause and can’t see.</p>
<h3 id="interactive-demonstration" tabindex="-1"><a class="header-anchor" href="https://lea.verou.me/blog/2026/dark-mode-toggles/#interactive-demonstration">Interactive demonstration</a></h3>
<p>Here’s a concrete scenario that you can navigate interactively (<a href="https://lea.verou.me/blog/2026/dark-mode-toggles/demo.html">view on separate page</a>):</p>
<script type="module" src="https://lea.verou.me/blog/2026/dark-mode-toggles/demo.js"></script>
<theme-scenario src="demo.svg">
<ol>
<li data-editable="os">Your OS is in <strong data-value="start">light</strong> mode and the site has stored nothing, so the page follows along. Flip the OS control to run this the other way round.</li>
<li data-action="page: !page">You toggle. The target is <span data-value="other">dark</span>, which is <em>not</em> what the OS says, so the site stores an override. <strong>The page goes <span data-value="other">dark</span>.</strong></li>
<li data-action="os: !os">Your OS switches to <span data-value="other">dark</span>. The override now matches it <strong>but is still kept</strong>. Nothing visibly happens, which is correct.</li>
<li data-action="os: !os">Your OS switches back to <span data-value="start">light</span>. <strong>The page stays <span data-value="other">dark</span></strong>, because the override is still active.</li>
<li data-action="page: !page">You toggle. The target is <span data-value="start">light</span>, which <em>is</em> what the OS says, so the override is <strong>removed</strong>. The page follows the OS again.</li>
<li data-editable=""><strong>Your turn.</strong> Both controls are live and nothing from here on is scripted. Drive them in any order and watch what does — and does not — end up in <code>localStorage</code>.</li>
</ol>
</theme-scenario>
<h3 id="but-what-if-users-get-confused%3F" tabindex="-1"><a class="header-anchor" href="https://lea.verou.me/blog/2026/dark-mode-toggles/#but-what-if-users-get-confused%3F">But what if users get <em>confused</em>?</a></h3>
<p>An argument I heard when discussing this was “but if the user selects light when their OS is light, then the OS switches to dark, won’t they get confused that the website did not preserve their choice?”</p>
<p>People hypothesizing that <em>other</em> people, who are not them, will get “confused” is a bit of a pet peeve of mine in usability discussions, but let’s entertain it for a moment.</p>
<p>Here’s that exact scenario:</p>
<theme-scenario src="demo.svg" key="theme-pin">
<ol>
<li data-editable="os">Your OS is in <strong data-value="start">light</strong> mode and nothing is stored.</li>
<li data-action="page: !page">You toggle to <span data-value="other">dark</span>, which is stored as an override.</li>
<li data-action="page: !page">You toggle again, meaning to <em>pin</em> <span data-value="start">light</span>. It matches the OS, so the override is <strong>removed</strong> — you actually got the system default.</li>
<li data-action="os: !os">Your OS switches to <span data-value="other">dark</span> and <strong>the page follows</strong>. Not what you meant!</li>
<li data-action="page: !page" data-editable="os">But the fix is a single click: <span data-value="start">light</span> no longer matches the OS, so this time it <em>is</em> an override, and thus pinned, so this can only happen at most <strong>once</strong>.</li>
</ol>
</theme-scenario>
<p>Remember, this control is <strong>entirely tangential</strong> to the actual user goal for visiting the website.
Even if their intent were to pin <em>light</em> instead of reverting to <em>System (light)</em>, this is something they would only notice once these diverge, i.e. the OS switches to dark.
At that point, fixing it is a single click away.
It’s such an easy fix, that there is no point in dwelling on it further.</p>
<p>It’s not that this never comes up, but making the tradeoff in favor of a tri-state control isn’t justifiable, IMO.
A tri-state control introduces <strong>permanent UI complexity</strong> to prevent a <strong>one-time, easily fixable problem</strong>.</p>
<p>Additionally, <a href="https://en.wikipedia.org/wiki/Color_appearance_model">color appearance</a> is not just a pure function of color components, but also affected by surroundings and other factors.
Even if a website implements only two modes, light mode may look slightly different in a light OS vs a dark OS, so selecting it as an override makes it an <strong>informed decision</strong>.</p>
<p>The title and icon <em>could</em> make the state clearer (e.g. the tooltip saying “Switch back to light (system default)” instead of “Switch to light” or the icon having a small screen icon instead of just a sun or moon).
But those would need user testing to validate that they are an actual improvement.
My concern is that once you distinguish <em>System (light)</em> from <em>light</em>, it (ironically) could <em>become</em> the thing that primes users to seek a third state that they previously had not considered.</p>
<p>Even if there is an ingenious UI that exposes three states at the same time without adding any cognitive load or friction (I have some ideas about what that might look like), I’m unconvinced this is a problem worth solving, and feels a lot like the UX version of <a href="https://en.wikipedia.org/wiki/Program_optimization#When_to_optimize">premature optimization</a>.</p>
<h2 id="when-is-a-tri-state-control-appropriate%3F" tabindex="-1"><a class="header-anchor" href="https://lea.verou.me/blog/2026/dark-mode-toggles/#when-is-a-tri-state-control-appropriate%3F">When is a tri-state control appropriate?</a></h2>
<p>Although I spent the whole article arguing against tri-state toggles, there <em>are</em> actually valid use cases for them.</p>
<p>These are the two cases I’m aware of, but feel free to recommend more in the comments!</p>
<h3 id="settings" tabindex="-1"><a class="header-anchor" href="https://lea.verou.me/blog/2026/dark-mode-toggles/#settings">1. Color scheme setting that lives a separate settings panel</a></h3>
<p>This article is primarily geared towards a <strong>permanently visible toggle in the header or footer</strong>.</p>
<p>A setting that lives alongside other settings in a settings panel is a fundamentally different usage scenario:</p>
<ul>
<li>The user is already in the mode of making decisions about their future</li>
<li>The expectation is not that every setting must produce immediate feedback</li>
<li>There is a lot more screen real estate to explain three states.</li>
</ul>
<p>It is no accident that while 2-state toggles are becoming the norm for persistent controls, tri-state is (rightly) king for settings panels.</p>
<figure>
<p><img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/bsky.png" alt="Bluesky settings panel"></p>
<figcaption>
<p>Bluesky’s Appearance settings panel.
The tri-state is fine here.
Showing the “Dark mode” option below even when it produces no effect, on the other hand…</p>
</figcaption>
</figure>
<figure>
<p><img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/google-calendar.png" alt="Google Calendar settings panel"></p>
<figcaption>
<p>Google Calendar. Love the icons, it would be nice to actually indicate what System currently resolves to.</p>
</figcaption>
</figure>
<figure>
<p><img src="https://lea.verou.me/blog/2026/dark-mode-toggles/images/x.png" alt="Twitter/X settings panel"></p>
<figcaption>
<p>I’m not one to praise post-X Twitter, but having two two-state toggles instead of one tri-state is a very interesting design choice.
The UX is not quite there, but if done well, I think it could be the best of both worlds when you have the screen real estate.</p>
</figcaption>
</figure>
<h3 id="four-themes" tabindex="-1"><a class="header-anchor" href="https://lea.verou.me/blog/2026/dark-mode-toggles/#four-themes">2. When color schemes are implemented differently depending on the system setting</a></h3>
<p>This entire essay assumes the common case where a website only has two color schemes: light and dark, and there is no difference between light mode in a dark OS vs light mode in a light OS.
<a href="https://blog.kizu.dev/querying-the-color-scheme/#not-the-user-preference">Vadim Makeev had an interesting idea</a>:
color schemes should take the underlying OS setting into account.
Light mode should be less bright in a dark OS and dark mode should be less dark in a light OS, to reduce the contrast between the website and the rest of the system.</p>
<p>I have not seen many UIs doing this, and CSS does not make it easier (<code>light-dark()</code> is very much designed around duality), but if you are actually doing this, <strong>you have earned your three states my friend</strong>, display them as prominently as you like, none of this applies to you!</p>
<p>Edit: I reached out to Vadim to ask if he had seen any UIs following his guidance. Here’s what he had to say:</p>
<blockquote>
<p>Unfortunately, I haven’t seen any websites using this idea. I would say we’re pretty limited with tools currently to do so. The moment we want to override prefer-color-scheme, the whole light-dark() convenience is falling apart.</p>
</blockquote>
<p>Yet another problem that CSS functions will solve (nothing preventing us from creating a <a href="https://www.bram.us/2025/09/30/css-custom-light-dark/">2-4 arg version of this</a>).</p>
<h2 id="the-general-version" tabindex="-1"><a class="header-anchor" href="https://lea.verou.me/blog/2026/dark-mode-toggles/#the-general-version">The general version</a></h2>
<p>The dark mode toggle is a nice case study, but the underlying lesson is bigger:</p>
<p><strong>Users do not seek out solutions to problems they don’t currently have.</strong></p>
<p>The tri-state toggle is the GUI version of <a href="https://lea.verou.me/blog/2025/user-effort/#signal-to-noise">low signal-to-noise APIs</a> that ask you to pass dozens of parameters that could have sensible defaults, forcing you to decide on problems you have not encountered and are not relevant.</p>
<p>Do not flood users with options that are irrelevant to their current situation.
Options that <em>might</em> become relevant in the future, should be surfaced in that future, not pre-emptively.</p>
<p>Not every state of your state machine warrants visible UI.</p>
<p>Ultimately, everything boils down to the very same principle:<br>
<a href="https://lea.verou.me/blog/2025/user-effort/"><strong>Respect user effort.</strong></a></p>
<p><em>Thanks to Chris Lilley and Jake Archibald for reviewing an earlier version of this draft</em></p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Unless otherwise noted, this refers to a permanently visible toggle in the header or (rarely) footer, <strong>not</strong> a theme setting in a separate settings panel. <a href="https://lea.verou.me/blog/2026/dark-mode-toggles/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>This is about users. Yes, the developers of the site may have a goal of testing the theme, but we optimize UIs for being <em>used</em>, not getting debugged. <a href="https://lea.verou.me/blog/2026/dark-mode-toggles/#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></description>
      <pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://lea.verou.me/blog/2026/dark-mode-toggles/</link>
      <dc:creator>Lea Verou’s blog</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5335541974</guid>
    </item>
    <item>
      <title><![CDATA[when-do web component [blog]]]></title>
      <description><![CDATA[<p>I saw recently saw a blog post (which annoyingly I can't find any more, but I'll link it if I do), showing off a web component that if I recall correctly, would show (or hide) it's contents depending on a browser feature (I'd guess using the <code>@supports</code> query).</p>
<p>That reminded me that I wrote a web component that I use quite a bit (particularly with <a href="https://2026.ffconf.org">FFConf</a>), which should really be called <code>do-when</code>, but it's not, it's (stupidly) called <code>when-do</code> (the dash, I guess, being a comma in my head).</p>
<p>The component says: <code>when</code> a time occurs or passes, <code>do</code> a <code>what</code>: show, hide or scroll-to.</p>
<p>You can get the code <a href="https://github.com/remy/web-components/tree/main/when-do">from github</a> - which also includes an <a href="https://github.com/remy/web-components/blob/main/when-do/example/index.html">interactive demo</a> that lets you try the settings.</p>
<h2>What it is</h2>
<p>A small web component to toggle on or off depending on wall clock (date) time without affecting the visual layout (with it's own element).</p>
<h2>Use case</h2>
<p>I've been using this web component on the FFConf event pages for a number of years to reveal the ticket button if a visitor is sitting on the page (though I appreciate that's unlikely!).</p>
<pre><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>when-do</span> <span class="token attr-name">what</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hide<span class="token punctuation">"</span></span> <span class="token attr-name">datetime</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{ticket-live}<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">&gt;</span></span>Tickets live at {nice-ticket-live}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>when-do</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>when-do</span> <span class="token attr-name">what</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>show<span class="token punctuation">"</span></span> <span class="token attr-name">datetime</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{ticket-live}<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">&gt;</span></span>Buy <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>…<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>now<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>when-do</span><span class="token punctuation">&gt;</span></span>
</code></pre>
<p>I've also used the <code>what="scroll"</code> for our schedule page, to automatically scroll to the current speaker.</p>
<p>Though I do appreciate my "battle testing" has been rather limited, so if you use this and spot bugs, please let me know!</p>
<h2>API</h2>
<ul>
<li><code>what="show|hide|scroll"</code> - the component also throws an exception if a non valid string is used</li>
<li><code>datetime="isodate"</code> - recommended that you include the timezone, ie. "2026-09-13T10:00Z" is 10am on 13th September on UTC</li>
<li><code>apply="classname"</code> optional class name to apply to the <code>when-do</code> element when it activates, useful if you wanted to transition your contents into view, or highlight the newly scrolled element</li>
</ul>
<h2>How it works/styling</h2>
<p>The web component makes use of <code>display: contents</code> to prevent itself from modifying the visual layout.</p>
<p>Due to this display method, it means you can't style the element directly, in that styling has no effect.</p>
<p>If you want to style the contents when the element is active, such as when it has been scrolled into view, it's recommended you wrap the child elements it a parent element, such as a <code>div</code>.</p>
<p>You can also use the <code>apply</code> property to indicate the current <code>when-do</code> has activated.</p>
<hr>
<p>AI wasn't used in any part of this web component (which is also why the demo page is so ugly).</p>
<p><em>Originally published on <a href="https://remysharp.com/2026/07/24/when-do-web-component">Remy Sharp's b:log</a></em></p>]]></description>
      <pubDate>Fri, 24 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://remysharp.com/2026/07/24/when-do-web-component</link>
      <dc:creator>remy sharp&#39;s b:log</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5325411956</guid>
    </item>
    <item>
      <title><![CDATA[New Working Draft: CSS Linked Parameters Level&nbsp;1.]]></title>
      <description><![CDATA[<span class="updated" title="2026-07-14"><span>14</span> Jul 2026</span> New Working Draft: <a href="https://www.w3.org/TR/2026/WD-css-link-params-1-20260714/"><cite>CSS Linked Parameters Level&nbsp;1</cite></a>.]]></description>
      <pubDate>Tue, 14 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://www.w3.org/Style/CSS/Overview.en.html#n2026-07-14</link>
      <dc:creator>Cascading Style Sheets</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5308987947</guid>
    </item>
    <item>
      <title><![CDATA[Remove Empty Shortcodes: Now It Removes the Shortcodes That Were the Whole Point]]></title>
      <description><![CDATA[<p class="wp-block-paragraph">Back in February, I wrote about <a href="https://tommcfarlin.com/remove-empty-shortcodes-1/">Remove Empty Shortcodes 1.0.0</a> in which the plugin includes the admin scanner, a real test suite, and automated deployment to WordPress.org.</p>

<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="944" height="1024" src="https://tommcfarlin.com/wp-content/uploads/2026/06/CleanShot-2026-06-29-at-09.59.43-944x1024.png" alt="" class="wp-image-42929" srcset="https://tommcfarlin.com/wp-content/uploads/2026/06/CleanShot-2026-06-29-at-09.59.43-944x1024.png 944w, https://tommcfarlin.com/wp-content/uploads/2026/06/CleanShot-2026-06-29-at-09.59.43-277x300.png 277w, https://tommcfarlin.com/wp-content/uploads/2026/06/CleanShot-2026-06-29-at-09.59.43-768x833.png 768w, https://tommcfarlin.com/wp-content/uploads/2026/06/CleanShot-2026-06-29-at-09.59.43-676x733.png 676w, https://tommcfarlin.com/wp-content/uploads/2026/06/CleanShot-2026-06-29-at-09.59.43.png 1183w" sizes="auto, (max-width: 944px) 100vw, 944px"></figure>

<p class="wp-block-paragraph">Then a <a href="https://wordpress.org/support/topic/does-this-plugin-work-with-wordpress-7-0/">bug report was submitted on the wp.org forums</a> leading to <code>1.1.0</code>.</p>

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

<h2 class="wp-block-heading">The Bug I Shipped on Purpose</h2>

<p class="wp-block-paragraph">In 1.0.0, I cleaned up a batch of false positives such as bracketed text like <code>[at]</code> and <code>[dot]</code> that isn’t really a shortcode by leaning on WordPress’s own <code><a href="https://developer.wordpress.org/reference/functions/get_shortcode_regex/">get_shortcode_regex()</a></code>. </p>

<p class="wp-block-paragraph">It was the right call for false positives but wrong call for other reasons. It was also, it turns out, the wrong call for the plugin’s entire reason to exist.</p>

<p class="wp-block-paragraph"><code>get_shortcode_regex()</code> only matches shortcodes that are <em>currently registered</em>. But the shortcodes this plugin is meant to clean up are, almost by definition, the ones that <em>aren’t</em> registered anymore such as the leftovers from a plugin or page builder you’ve since turned off. </p>

<p class="wp-block-paragraph">By keying off the registered list, 1.0.0 quietly stopped seeing the exact thing it was built to remove.</p>

<p class="wp-block-paragraph">The nudge came from someone migrating a client off a site built with WP Bakery. They’d imported all the posts, deactivated the builder, and were staring at content packed wall-to-wall with <code>, , </code>, and similar tags. </p>

<p class="wp-block-paragraph">The user installed Remove Empty Shortcodes hoping it would at least stop those from rendering on the front end while they cleaned house. It did nothing. The scanner came back empty, too. Obviously, a bug.</p>

<h2 class="wp-block-heading">What’s New in 1.0.0</h2>

<h3 class="wp-block-heading">It Removes Inactive Shortcodes. Again. (Seriously.)</h3>

<p class="wp-block-paragraph">The front end now detects <em>unregistered</em> shortcodes and removes them, while keeping all of the false-positive protection from 1.0.0. So <code></code> from a deactivated builder gets handled, but <code>[at]</code> in “email me <code>[at]</code> example <code>[dot]</code> com” is left alone, exactly as it should be.</p>

<p class="wp-block-paragraph">If you’d rather remove the whole block, content and all, there’s a <code>resc_removal_mode</code> filter for that. </p>

<h3 class="wp-block-heading">It Keeps Your Content</h3>

<p class="wp-block-paragraph">If you’d rather remove the whole block, content and all, there’s a <code>resc_removal_mode</code> filter for that. The default is the gentle one.</p>

<p class="wp-block-paragraph">This is the part I cared about most. When the plugin finds an inactive shortcode that <em>wraps</em> content such as<code>Some real copy</code>, it strips the wrapper tags and <strong>keeps the copy</strong>. You lose the dead <code></code>, you keep “Some real copy.” Your writing survives; only the scaffolding goes.</p>

<h3 class="wp-block-heading">The Scanner Looks Where the Content Actually Is</h3>

<p class="wp-block-paragraph">The 1.0.0 scanner only examined registered, public post types. So if your import dropped everything into a custom post type whose plugin is no longer active, the scanner never looked at it which is exactly how you get “nothing found” on a site that’s clearly full of orphaned shortcodes. </p>

<p class="wp-block-paragraph">The scanner now looks at the post types actually present in your database, minus the obvious system ones.</p>

<h3 class="wp-block-heading">“Ignore” Finally Means Ignore</h3>

<p class="wp-block-paragraph">The scanner has always let you add shortcodes to an ignore list. In 1.0.0, the front-end remover didn’t consult it. In actuality, the two halves of the plugin weren’t talking to each other. Now they are. If you tell the scanner to leave a shortcode alone, the front end leaves it alone too.</p>

<h3 class="wp-block-heading">WordPress 7.0</h3>

<p class="wp-block-paragraph">1.1.0 is tested up to WordPress 7.0, and I fixed a small admin-button icon that 7.0’s new button styling had knocked out of alignment.</p>

<h2 class="wp-block-heading">Still Nothing Touches Your Database</h2>

<p class="wp-block-paragraph"><a href="https://tommcfarlin.com/remove-empty-shortcodes/" data-type="post" data-id="40450">This was the founding promise of the plugin back in 2019</a>, when it started life as a way to retire Restrict Content Pro without gutting my posts: <strong>remove shortcodes from what’s displayed, never from what’s stored.</strong></p>

<p class="wp-block-paragraph">That’s still true, and now it’s pinned down by tests. The plugin filters content on its way to the screen. Your <code>post_content</code> in the database is left byte-for-byte intact. Reinstall the page builder and every shortcode renders again as if nothing happened because, in the database, nothing did.</p>

<h2 class="wp-block-heading">Under the Hood</h2>

<p class="wp-block-paragraph">A couple of things for the developers in the room:</p>

<ul class="wp-block-list">
<li><strong>Integration tests against real WordPress.</strong> The 1.0.0 suite mocked WordPress with Brain Monkey, which is fast but can’t catch how the scanner behaves against an actual database. 1.1.0 adds a full integration suite that boots WordPress, seeds real posts, and exercises the scan-and-remove path end to end including a test that proves stored content is never modified.</li>

<li><strong>CI across PHP 7.4–8.3.</strong> The unit suite now runs on every push and pull request across four PHP versions. Which is how I caught that the code had quietly drifted into PHP 8.0-only syntax while the header still claimed a 7.4 minimum. 1.1.0 honors 7.4 again (though I’d eventually love to retire support for it sooner rather than later).</li>
</ul>

<h2 class="wp-block-heading">Get It</h2>

<p class="wp-block-paragraph"><a href="https://wordpress.org/plugins/remove-empty-shortcodes/">Remove Empty Shortcodes 1.1.0</a> is live on WordPress.org. Install it, activate it, and the front-end cleanup just works. There’s still no configuration required. If you want to review what’s lurking in your content first, the scanner is waiting under <strong>Tools → Empty Shortcodes</strong>.</p>

<p class="wp-block-paragraph">The code, the changelog, and the issues are all on <a href="https://github.com/tommcfarlin/remove-empty-shortcodes/">GitHub</a> if you’d like to follow along.</p>
<p><a href="https://tommcfarlin.com/remove-empty-shortcodes-110/" rel="nofollow">Source</a></p>]]></description>
      <pubDate>Mon, 13 Jul 2026 14:02:27 +0000</pubDate>
      <link>https://tommcfarlin.com/remove-empty-shortcodes-110/</link>
      <dc:creator>Tom McFarlin</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5304915167</guid>
    </item>
    <item>
      <title><![CDATA[Accessible (I Think) Split-Cell Table Headers]]></title>
      <description><![CDATA[<p>My colleague Chris Griffith, with whom I collaborated to put <a href="https://atomicarchive.com/resources/documents/effects/glasstone-dolan/"><cite>The
Effects of Nuclear Weapons, Third Edition</cite> (1977) online</a>, is
also a <a href="https://www.american-spacecraft.org/">spaceflight
enthusiast</a> (and an urban trails hiker: check out <a href="https://urbantrails-sandiego.com/">his new book</a>!).&nbsp; He recently
asked me how I would mark up a table with a split diagonal header cell;
specifically, this one from the Apollo 16 documentation:</p>

<figure class="standalone">
<a href="https://meyerweb.com/eric/thoughts/wp-content/uploads/apollo_16-page_050-crop.png"><img fetchpriority="high" decoding="async" src="https://meyerweb.com/eric/thoughts/wp-content/uploads/apollo_16-page_050-crop.png" alt="" width="1500" height="949" class="alignnone size-full wp-image-5758" srcset="https://meyerweb.com/eric/thoughts/wp-content/uploads/apollo_16-page_050-crop.png 1500w, https://meyerweb.com/eric/thoughts/wp-content/uploads/apollo_16-page_050-crop-300x190.png 300w, https://meyerweb.com/eric/thoughts/wp-content/uploads/apollo_16-page_050-crop-1024x648.png 1024w, https://meyerweb.com/eric/thoughts/wp-content/uploads/apollo_16-page_050-crop-768x486.png 768w" sizes="(max-width: 1500px) 100vw, 1500px"></a>
<figcaption>The top left corner of the page in question.</figcaption>
</figure>

<p>My immediate thought was to throw two spans in the header cell and
position or grid them within that cell, but the accessibility of that
seemed… questionable.&nbsp; It’s also <a href="https://en.wikipedia.org/wiki/Template:Diagonal_split_header">what
Wikipedia already does</a>, and we here at meyerweb are nothing if not
obsessed with finding new ways to do niche stuff.&nbsp; So I tried something
different.&nbsp; But is its accessibility any better?</p>

<p>If you want to see it as a live example, <a href="https://codepen.io/meyerweb/pen/OPbyoWw?editors=1100">it’s over at
Codepen</a>.&nbsp; Most of the text in the table is what macOS Preview OCRed
out of the original image, which I kept intact because I think it’s
funny.&nbsp; Anyway, here is the original markup I came up with for the 
table head, which <strong>you should not use</strong>:</p>

<pre class="html"><code>&lt;thead&gt;
	&lt;tr&gt;
		&lt;th scope="row"&gt;SCIENTIFIC DISCIPLINE&lt;/th&gt;
		&lt;th scope="col"&gt;GEOLOGY&lt;/th&gt;
		&lt;th scope="col"&gt;GEOPHYSICS&lt;/th&gt;
		&lt;th scope="col"&gt;GEOCHEMISTRY&lt;/th&gt;
	&lt;/tr&gt;
	&lt;tr&gt;
		&lt;th scope="col"&gt;EXPERIMENT&lt;/th&gt;
	&lt;/tr&gt;
 &lt;/thead&gt;</code></pre>

<p>So one row for the headers across the top of the table, including the
top-left label that goes with them, and then another row with the header
that relates to the row headers for the rows below.&nbsp; That is to say, the
row-scoped table header in each of the rows in the table’s bodies (it
has more than one), like this:</p>

<pre class="html"><code>&lt;tbody&gt;
	&lt;tr&gt;
		&lt;th scope="row"&gt;CONTINGENCY SAMPLE COLLECTION&lt;/th&gt;
		[…]
	&lt;/tr&gt;</code></pre>

<p>The thing is, when I ran the idea past accessibility experts like <a href="https://alice.boxhall.au/" rel="colleague met">Alice Boxhall</a> and <a href="https://adrianroselli.com/" rel="colleague met">Adrian Roselli</a>, they identified
a problem: Not having a full row of cells, as is the case for the second
header row, fails <a href="https://www.w3.org/WAI/WCAG21/Understanding/sensory-characteristics.html">WCAG
1.3.3</a>.&nbsp; The suggested fix was to rowspan most of the cells in the
first row, like this:</p>

<pre class="html"><code> &lt;thead&gt;
	&lt;tr&gt;
		&lt;th scope="row"&gt;SCIENTIFIC DISCIPLINE&lt;/th&gt;
		&lt;th scope="col" rowspan="2"&gt;GEOLOGY&lt;/th&gt;
		&lt;th scope="col" rowspan="2"&gt;GEOPHYSICS&lt;/th&gt;
		&lt;th scope="col" rowspan="2"&gt;GEOCHEMISTRY&lt;/th&gt;
	&lt;/tr&gt;
	&lt;tr&gt;
		&lt;th scope="col"&gt;EXPERIMENT&lt;/th&gt;
	&lt;/tr&gt;
 &lt;/thead&gt;</code></pre>

<p>With that, table navigation wasn’t perfect, but it seemed decent, so
we could move forward.</p>

<p>In terms of presentation, to get the upper-left header cell to do the
split-diagonal thing, I relatively position the
<code>&lt;thead&gt;</code> and then absolutely position the second row
in the table head to sit over top of the first, pinned to the bottom
left corner.</p>

<pre class="css"><code>thead {
	position: relative;
}
thead tr:nth-child(2) th {
	&nbsp; position: absolute;
	&nbsp; bottom: 0;
	&nbsp; left: 0;
}
</code></pre>

<p>I fiddled around for a bit with trying to use a grid instead, but it
didn’t really add anything that positioning didn’t already provide and
threw some other wrenches into the works, like having to convert the
entire table into a grid so the columns would stay aligned, so I decided
to just stick with the positioning.</p>

<p>Then I throw a linear gradient background into the first row’s first
cell to draw the diagonal, and everything’s thus more or less as
intended, visually speaking.&nbsp; (That diagonal could also be an SVG, in
fact probably should be in production, but I was seeing how an all-CSS
solution might work so a gradient is where things stand.)</p>

<p>There are some layout caveats with this approach, but they’re pretty
much the same as other solutions I saw: primarily, the two bits of text
that the diagonal visually separates can stick out of their respective
halves of the split cell, or even overlap each other.&nbsp; Also, you
might need to explicitly set a minimum height of the first header row,
in order to not exacerbate the overlap risk just described.</p>

<p>And then there’s a really big caveat: Safari, as of this writing,
doesn’t handle the layout at all well, because it doesn’t apply
relative positioning to <code>&lt;thead&gt;</code> (or
<code>&lt;tfoot&gt;</code> or <code>&lt;tbody&gt;</code>, but at least
it does <code>&lt;tr&gt;</code>s).&nbsp; I went to file a bug and found <a href="https://bugs.webkit.org/show_bug.cgi?id=203449">there’s already
one open</a>, so maybe this will be fixed in the near future.&nbsp; I figured
out a way to get at least close to the intended result while still
allowing line-wrapping in the column header cells, but it mangled the
layout in Firefox and Chrome.&nbsp; In the end, to work around the problem, I
delved into <a href="https://browserstrangeness.github.io/css_hacks.html">browser
strangeness</a> (at the suggestion of <a href="https://mariusgundersen.net/">Marius Gundersen</a>) and settled on
the following:</p>

<pre class="css"><code>/* this is gross and I hate it but it works to fix 
&nbsp;  Safari’s layout of the table’s top headers */
@supports (font: -apple-system-body) {
	thead tr:nth-child(1) th {
		white-space: nowrap;
	}
	thead tr:nth-child(2) th {
		position: static;
		display: block;
		margin-block: -1.5lh 0;
		padding-block: 0;
		text-align: start;
		transform: translateY(0.25lh);
	}
}</code></pre>

<p>Thanks, I hate it!&nbsp; But it works, and I try to be pragmatic.</p>

<p>Anyway, the point being, what I’ve done here <em>feels</em> more accessible 
to me, and basic testing by both me and Adrian didn’t reveal any major
problems, but I still worry about the positioning dorking things up for
the users of screen readers I don’t have access to.&nbsp; So I throw it to the
audience, particularly the accessibility-technology-using part of the
audience: does this solution fall down for you, or is it good enough?
Please let me know!</p>
	<hr><p>Have something to say to all that?  You can <a href="https://meyerweb.com/eric/thoughts/2026/05/28/accessible-i-think-split-cell-table-headers/#commentform">add a comment to the post</a>, or <a href="mailto:eric@meyerweb.com?subject=In%20reply%20to%20%22Accessible%20(I%20Think)%20Split-Cell%20Table%20Headers%22">email Eric directly</a>.</p>]]></description>
      <pubDate>Thu, 28 May 2026 13:05:08 +0000</pubDate>
      <link>https://meyerweb.com/eric/thoughts/2026/05/28/accessible-i-think-split-cell-table-headers/</link>
      <dc:creator>Thoughts From Eric</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5240799146</guid>
    </item>
    <item>
      <title><![CDATA[Designing For Distressed Users: Why Mental Health Apps Shouldn’t Follow Every UI Fashion]]></title>
      <description><![CDATA[<p>Mental health applications keep facing a continuing, measurable crisis: many people stop using them quickly. The data is stark: <a href="https://www.jmir.org/2019/9/e14567/">almost 95% of users who open the app on day 1 abandon the app by day 30</a>, with a median 30-day retention of only 3.3%. Even the recognised mental health giants lose around <a href="https://link.springer.com/article/10.1007/s12671-018-1050-9">50% of their users</a> within the first ten days. This severe engagement loss and retention collapse are why effective interface design must be a clinical and operational priority. Good design is not merely aesthetic; it is a fundamental <strong>tool for user retention</strong>. </p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/01-app-30day-retention-mental-health.png"></p>
<p>While many factors drive this abandonment, research suggests that <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC8844980/">mental health apps have tended to prioritise visual appeal</a> at the expense of what actually <strong>sustains</strong> users. In a space defined by vulnerability and cognitive strain, chasing visual fashion risks adding effort when users have the least to spare — quietly trading away the <strong>utility</strong> and <strong>trust</strong> the app depends on. Users don’t open mental health apps out of curiosity, but from need — often while stressed, anxious, overwhelmed, or exhausted. In these states, an unconventional icon, a confusing gesture, or a flashy animation instead of a delightful surprise becomes <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC6093283/#:~:text=Many%20individuals%20with%20serious%20mental,make%20decisions%20on%20their%20behalf.">an extra cognitive overload</a>. Moreover, <a href="https://www.mdpi.com/2078-2489/14/6/308">it becomes a reason to disengage</a>.</p>
<p>In those moments, visual experimentation from a mild distraction can turn into a friction that undermines the very help the app is meant to deliver. A solution must be a visual interface that is simple in usage and understanding from the first moment.</p>
<p>Crucially, improving engagement depends less on which UI trends you follow than on a single test applied to each one of them: does a trend lower the cost of using the app when the user can least afford it?</p>
The High Cost Of Trend-driven Design In Mental Health
<p>Before we look into the specific problems, we must recognise a core tension: many UI trends are optimised for goals that mental health apps don’t share.</p>
<blockquote>Trend design is often about capturing attention and signaling innovation. Mental health design, in contrast, must be about <strong>offering refuge</strong>, <strong>reducing strain</strong>, and <strong>building trust</strong>.</blockquote>

<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/02-trend-ui-mental-health-ui.png"></p>
<p>Pursuing the former directly overrides the latter. It’s not a surface-level error of colour or font; it’s a foundational <strong>conflict of purpose</strong>. This tension surfaces across five fronts, each a place where adopting a trend on novelty alone can cost more engagement than it seeks to create.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/03-fonts-trend-driven-ui-therapeutic-usability.png"></p>
<p>The proposed principles are not based on a single A/B test or one isolated study. They are built from published research on mental health app engagement, cognitive load, accessibility, and emotional response in mHealth, set against competitive product audits and app-store evidence, and pressure-tested against my own quantitative and qualitative product work. That last source I treat as an illustration, understanding the limits of personal experience. In this context, validation is less about proving that one interface pattern universally works and more about asking whether a design reduces effort, preserves agency, avoids emotional mismatch, and remains usable when the user is already under strain.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/04-distressed-user.png"></p>
<p><strong>A note on the examples</strong>: <em>This isn’t a ranking of apps. Every app has its own positioning, target audience, constraints, and business pressures that may not be visible from outside, and a pattern that strains a user in distress may be exactly right for that product’s actual goal. I’m reading individual, visible design decisions for one question only: how they might affect someone arriving in a low-capacity state.</em></p>
1. Cognitive Friction: When Design Becomes A Barrier To Healing
<p>The primary goal of any mental health tool is to reduce, not increase, cognitive load. Yet, many trendy interfaces achieve the opposite. <a href="https://www.onething.design/post/neo-brutalism-ui-design-trend">Neo-brutalist layouts</a> with stark contrasts demand visual parsing. <a href="https://raw.studio/blog/why-menus-are-disappearing-in-modern-ux/">Hidden navigation menus</a> that rely on non-standard swipes turn simple tasks into puzzles. <a href="https://makeitfable.com/article/can-eaa-boost-ux-and-profit/">Abstract, unlabeled icons</a> force users to guess rather than recognise. Each of these patterns adds friction — seconds of hesitation, a moment of confusion — and for a user whose mental energy is already low, those costs start to accumulate.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/05-abstract-controls.png"></p>
<p>This friction is most damaging during acute need. <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC9474730/">Research suggests</a> that when a user is in a state of high anxiety or depression, even typing or making simple choices can feel overwhelming. When an interface demands high cognitive effort at the moment support is needed, it doesn’t just make that session harder — it gives an overwhelmed user a <strong>reason to close the app</strong>, and a reason not to reopen it. Each point of confusion can become a place where a user may quit for good.</p>
<p><a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC9474730/">Other findings</a> show that apps with simple interfaces reduce the time and effort required to engage, directly improving retention. Conversely, a complex, trend-driven UI increases that time, creating an obstacle course that undermines the very healthy habit formation the app is meant to support. </p>
<p>This does not mean that every mental health product must be visually plain or minimal. The issue is whether the interface meets the <strong>user’s current capacity</strong>. </p>
<p>A panic-support tool, for example, works best when it offers a small number of obvious actions, rather than asking the user to browse. However, if a calming action meant for a moment of panic instead surfaces an upgrade screen, the product fails the user at precisely the point where failing matters most. Monetization is not the issue by itself; the issue is whether it appears at a point where the user expects immediate support.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/06-panic-entry.png"></p>
<p><a href="https://play.google.com/store/apps/details?id=com.dualphotonlabs.nonori&amp;hl=en">Nonori</a> shows the same principle in a more reflective context. The app does not present the user with a large content library or a complex dashboard at the start. Instead, it leads them through a simple, linear sequence of small actions. The value of this pattern is that it reduces the effort needed to begin. When a user is tired, anxious, or mentally overloaded, knowing exactly what to do next can lower the barrier to returning.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/07-linear-flow.png"></p>
<p>At the other end of the spectrum, comprehensive tracking apps show a different trade-off. <a href="https://bearable.app/"><em>Bearable</em></a>, for example, is genuinely powerful: it consolidates almost everything a person tracks — mood, symptoms, sleep, medication, habits, reports, correlations — into one place. For users managing chronic conditions or preparing for medical appointments, this can be genuinely useful. But the same comprehensiveness can become a burden for an exhausted user. Dense dashboards and multi-step check-ins require executive capacity — the very resource that anxiety, depression, burnout, or brain fog often reduce.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/08-comprehensive-tracking.png"></p>
<p>A similar tension appears in anxiety apps with strong support content but busy entry points. A product may contain useful features yet still make the first screen feel noisy with too many cards, locked items, playful characters, or upgrade prompts. This is not evidence that the product is bad. It shows how the same interface can feel very different depending on the user’s state: clear enough during exploration, but too demanding during distress.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/09-visually-rich-home-screens.png"></p>
<p>This insight shaped a guiding principle for our work: <strong>every interaction point must meet users at their current level of capacity</strong>, removing mechanical and cognitive barriers rather than adding to them. This principle guided our integration of low-friction, state-aware interactions in apps like <a href="https://play.google.com/store/apps/details?id=world.dobra.bearroom"><em>Bear Room</em></a>, a stress and anxiety reduction app, and <a href="https://apps.apple.com/us/app/teeni-supported-parenting/id6618150447"><em>Teeni</em></a>, an emotional-wellbeing app for parents of teens. </p>
<p>In <a href="https://play.google.com/store/apps/details?id=world.dobra.bearroom"><em>Bear Room</em></a>, we already had a fast mood-based flow built around four emotion cards. At the same time, our product research supported a second need: users also wanted more personalised support. We avoided making this a long selection flow or a typing-only route because both can still create friction for people under stress or anxiety. Instead, we made voice a primary, prominent path, always alongside a text alternative. A central microphone button allows users to share what’s on their mind. The app then uses AI to analyse the input and provide a tailored set of coping practices.</p>
<p>Rather than picking a single entry model, we kept two paths because they served different states. That matched what we saw in later analytics and user conversations: quick emotional selection worked better when users wanted speed, while open voice or text input worked better when they wanted personalisation and to feel more heard. This was more a pattern we noticed, without having the exact measured results. </p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/010-voice-low-friction-input.png"></p>
<p>Similarly, in <a href="https://apps.apple.com/us/app/teeni-supported-parenting/id6618150447"><em>Teeni</em></a>, we directly addressed the cognitive friction of parenting stress by introducing <strong>a “Quick Relief” button</strong>. This creates an empathy-friendly flow for parents experiencing anger or frustration. The button initiates a dedicated “Hot Flow,” allowing them to first vent and relieve their immediate negative emotions through voice input. Only after this emotional release does the app gently guide them into the more reflective “Cold Flow” for the rest of the app’s resources. This sequential, state-sensitive design acknowledges that a user in peak distress cannot navigate a complex app; they need a direct, simple, and validating first step.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/011-teeni-quick-relief-flow.png"></p>
<p>These solutions directly tackle cognitive friction by meeting users at their level of capacity, resisting trends that add visual or interaction complexity. Voice input and single-action buttons remove the mechanical and cognitive burden of navigation and typing. The result is an interface that feels reliably non-judgmental and genuinely helpful when users are least equipped to navigate complexity.</p>
2. Emotional Mismatch: The Trust Erosion Of Misaligned Design Tone
<p>A user’s emotional state is the context in which a mental health app operates. This is why its visual language must be <strong>empathetic</strong> and <strong>considerate</strong>. <a href="https://journals.sagepub.com/doi/10.1177/14604582241295948">Research investigating how colour and aesthetics influence mood in mHealth apps</a> suggests a critical insight: users in distress show a strong preference for <strong>subtlety</strong>. They long for dark palettes, sleek and sophisticated looks, and clean, uncluttered aesthetics, explicitly noting that cheerful, bright colours, while seemingly appropriate, can create a jarring, even physically uncomfortable conflict with their current mood. </p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/012-emotional-alignment.png"></p>
<p>This does not mean that every mental health or wellbeing app should look dark, quiet, or clinically restrained. The category is broad: it includes self-care, anxiety support, habit change, addiction recovery, trauma tools, therapy-adjacent products, and apps for more severe mental health contexts. A playful visual style may be appropriate for one product and a poor fit for another. What matters is not whether the interface is bright or muted, but whether its <strong>emotional tone</strong> fits the product’s purpose and the likely state in which users arrive.</p>
<p>Emotional mismatch can also appear in mechanics, not only in aesthetics. In <a href="https://gocalmer.com/"><em>Calmer</em></a>, an anxiety and panic relief app, the interface itself appeared relatively clean and relaxed. Yet some of its engagement and monetisation mechanics sit in a different register from that relaxed: a discount wheel, or confetti celebrating a logged low mood. For a user who just recorded a hard moment, that shift — from quiet support to upsell or celebration — can land as a mismatch, whatever its intent.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/013-calmer-emotional-mismatch.png"></p>
<p>For <a href="https://play.google.com/store/apps/details?id=world.dobra.bearroom"><em>Bear Room</em></a>, we prototyped a “cosy room” design informed by direct feedback from our users, which echoed the study’s conclusions. Several users in our research described the apps they had tried for similar needs as “too bright, too happy, and too overwhelming”. Users longed for a digital safe space. This was part of what pointed us toward a quieter palette in the final design: muted, earthy tones — neutral hues like soft greens and taupes — set against darker, calming backgrounds. For this product — a refuge for users arriving overwhelmed — that meant a middle ground: a space that feels safe without being gloomy. The interface avoided any bright alerts or sudden animations, making calmness the core feature.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/014-bear-room-muted-palette.png"></p>
<p>This case underscores a critical principle: an overly cheerful, bold, or trend-forward interface can feel dismissive to someone in distress, creating a conflict that erodes trust. As <a href="https://play.google.com/store/apps/details?id=world.dobra.bearroom"><em>Bear Room</em></a> shows, trust is built when the interface respectfully aligns with the user’s emotional reality, <strong>offering solace through subtlety</strong> (which creates an overall feeling of a “welcoming and safe atmosphere”), not a solution through saturation.</p>
3. The Inconsistency Penalty: Why Novelty Undermines Routine
<p><a href="https://www.verywellmind.com/the-importance-of-keeping-a-routine-during-stressful-times-4802638#:~:text=Rachel%20Goldman%2C%20PhD,lack%20of%20concentration%2C%20and%20focus.&amp;text=A%20lack%20of%20structure%20and,and%20routine%20throughout%20your%20day.">Mental health often relies on routine and predictability</a>. Yet many contemporary UI trends thrive on novelty and disruption, intentionally reimagining fundamental navigation. When an app introduces a novel interaction pattern, such as a unique swipe or a non-standard button behavior, it asks the user to learn something before they can act, forcing them into cognitive effort they can’t afford.</p>
<p>This does not mean that mental health apps must be plain, rigid, or generic. A product can have its own character, playfulness, and sense of identity. The question is whether that identity remains <strong>understandable</strong> and <strong>predictable</strong> when the user returns in a low-capacity state. A gamified self-care app like <a href="https://finchcare.com/"><em>Finch</em></a> may work well when the user arrives ready to explore, play, and build a routine. But open it after a hard day just to mark one task done, and the can’t-skip celebration screens that delight an engaged user become one more layer to get through before they reach what they came for.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/015-cant-skip-celebration-screens.png"></p>
<p>A similar tension appears in large meditation and wellbeing platforms. Apps such as <a href="https://www.headspace.com/"><em>Headspace</em></a> and <a href="https://www.calm.com/"><em>Calm</em></a> offer extensive libraries of different content. This breadth can be valuable during exploration. But in moments of stress, the product question becomes sharper: can the user return and immediately find the exact support they need, or do they have to search, filter, and relearn the structure?</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/016-large-content-libraries.png"></p>
<p>Someone experiencing anxiety or executive dysfunction needs to use the tool in <a href="https://www.researchgate.net/publication/333740636_Usability_Issues_in_Mental_Health_Applications">a straightforward navigation manner</a>, not an interface they have to learn each time anew.</p>
<p><a href="https://play.google.com/store/apps/details?id=is.vertical.ptsdcoach&amp;hl=en"><em>PTSD Coach</em></a>, a public-health-oriented trauma-support app designed to help users learn about and manage symptoms after trauma, offers a useful positive example. Its interface is not trying to be fashion-forward. Its strength lies in a <strong>stable information architecture</strong>: users can learn, track symptoms, manage symptoms, and get support through clearly separated areas. For a user returning during distress, this <strong>predictability</strong> matters more than novelty.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/017-stable-information-architecture.png"></p>
<p><a href="https://www.thecalmzone.net/calmzone-app"><em>CALMzone</em></a> offers another useful example. Some of its breathing animations differ from standard visual patterns, but they remain tied to the exercise itself: the animation shows what to do, when to inhale, and when to hold. The guided audio screen also explicitly invites the user to put the phone down and listen. This is a rare and valuable form of interaction design: the product’s success is not more screen time, but reduced effort and regulation.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/018-novel-interaction-design.png"></p>
<p>These insights guided our approach in applications like <a href="https://play.google.com/store/apps/details?id=world.dobra.bearroom"><em>Bear Room</em></a>, where <strong>navigation reliability</strong> was treated as a therapeutic feature. We intentionally crafted an experience of an empathetic guided flow.</p>
<p>Recognising that users would likely approach in states of overwhelm, we structured the interface as a clear, unwavering path, with a visible “Start” sign. Key emotional support tools here are represented as the room’s objects — symbols of daily life, unmistakably recognised by all. They are visually highlighted by design so that the user won’t get lost in the elements, can easily access the needed tool, and can remember their way around the digital space upon the next return to the app. </p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/019-navigation-reliability-guided-flow.png"></p>
<p>Trend-driven interfaces sacrifice this navigational certainty for novelty. Each unconventional choice in mHealth apps, when core functions are buried behind experimental interactions or placed in unexpected locations, leads to a cumulative effect of <strong>fatigue</strong> instead of innovation. Users are not in a place to explore. They <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC10924263/">abandon the entire practice of seeking digital support</a> when every interaction feels like solving a new puzzle. This does not restrict experimentation; it simply means that animations, micro-interactions, AI, or playful mechanics must serve the user’s state rather than interrupt it.</p>
4. The Silent Exclusion: How Trends Compromise Accessibility
<p>Many popular UI trends can be exclusionary when applied without adaptation. The minimalist trend of low-contrast text fails users with visual impairments. Gesture-only navigation marginalises those with motor difficulties. Visually dense, animated interfaces can overwhelm users with cognitive or attentional conditions. In mental health, the population needing support <a href="https://www.frontiersin.org/journals/digital-health/articles/10.3389/fdgth.2021.742196/full">disproportionately includes individuals with these accessibility needs</a>.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/020-accessibility-risks.png"></p>
<p>Choosing a trending aesthetic over an accessible one is therefore an active decision to limit the app’s reach and efficacy. It ensures that those who might benefit most cannot use the tool effectively. Accessibility isn’t a layer you add at the end; it’s a <strong>constraint you design within</strong>, with most of it codified in <a href="https://www.w3.org/TR/WCAG22/">Web Content Accessibility Guidelines 2.2</a>. Body text needs 4.5:1 contrast against its background, large text and interface elements 3:1 — exactly what low-contrast minimalism fails. Interactive targets need a floor of 24×24 px (more for unsteady hands). Every gesture needs a visible button fallback, or you risk excluding anyone who can’t perform the swipe.</p>
5. The Coercion Paradox: When “Engagement” Becomes “Pressure”
<p>A final, and often overlooked, consequence of trend-following is the adoption of engagement mechanics designed for entertainment, educational, or productivity apps. Features like streaks, aggressive notifications, and gamified reward systems are engineered to maximise screen time and create dependency. Although they have long been considered effective tools for increasing retention, in a mental health context, this approach can easily be misguided. What presents as “motivation” can quickly transform into <a href="https://journals.sagepub.com/doi/full/10.1177/19367244231196768">a source of performance pressure and guilt</a>. For a user managing depression, a broken streak or a missed daily goal can exacerbate the very feelings the app aims to alleviate.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/021-streaks.png"></p>
<p>These mechanics are not inherently unethical. In routine-building products, they can help some users. The risk appears when they are transferred into mental health contexts without adapting for shame, low energy, relapse, and non-linear recovery. A streak, for example, is not just a retention mechanic when the user is emotionally vulnerable. It can become a visible record of whether they have “kept up” with their well-being. </p>
<p>In the apps I reviewed, this tension appeared through familiar persuasion patterns: streaks, streak freezes, commitment copy, urgency-based notifications, <em>“don’t miss this offer”</em> prompts, and success-framed buttons such as <em>“Yes, I want to succeed.”</em> In self-care or wellbeing products, these details can make the app feel less like a supportive tool and more like another system the user has to satisfy.</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/022-persuasion-patterns.png"></p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/023-commitment-contracts.png"></p>
<p>Designers still need return triggers. A mental health app has little value if users install it once and forget it exists. But return mechanics must be adapted to the emotional context. In <a href="https://play.google.com/store/apps/details?id=world.dobra.bearroom"><em>Bear Room</em></a>, for example, this philosophy is embodied in short, forgiving three-day streaks: the streak does not reset when a user misses a day, and every third day brings a small benefit. <strong>The goal is not to punish absence, but to gently support return.</strong></p>
<p>The same principle applies to lighter interactions. Bear Room includes a simple, optional bubble-popping game. Its purpose, however, is not to hook the user but to offer a brief, calming interlude. It is deliberately finite, providing a small mood lift and gently signposting other resources within the app. The value is in the momentary relief, not the extended session.</p>
<p>This commitment to supportive, non-coercive design extends to foundational app architecture:</p>
<ul>
<li><strong>Respectful and User-Tailored Interaction Models</strong><br>The Pillow, a visual interface in the app, acts as a neutral, accepting space. Users can select a feeling or record a voice note, and the app responds with an AI-curated set of practices (28). It offers support without judgment, commentary, or pressure to “achieve” a certain state. Our app prioritises mood-aware algorithms to dynamically order activities. Breathing exercises or grounding techniques are surfaced based on the user’s reported emotional state, creating personal resonance without the need for an overwhelming content library.</li>
<li><strong>Feedback as a Reciprocal Exchange</strong><br>We approach feedback not as a data grab via constant emails but as a respectful dialogue. An unobtrusive object allows users to contribute at a natural pause point, and their input is acknowledged with a small reward. This frames their participation as a valued gift, not a demanded obligation.</li>
</ul>
<p>In mental health technology, sustainable retention is earned not by capturing attention, but by becoming a consistently respectful and helpful presence in a user’s life.</p>
<p>The mHealth apps design invites finding a challenging balance between boosting their use without being too demanding. </p>
The Scale Of The Stakes
<p>This is not a niche concern affecting a fringe audience. The World Health Organization (WHO) estimates that about one <a href="https://www.who.int/news/item/02-09-2025-over-a-billion-people-living-with-mental-health-conditions-services-require-urgent-scale-up">billion people</a> globally live with a mental disorder, with depression alone affecting nearly 5% of adults. Critically, the overall number is rising — in the last decade, depression and anxiety cases have increased <a href="https://www.who.int/news/item/17-06-2022-who-highlights-urgent-need-to-transform-mental-health-and-mental-health-care">by 25%</a>. This vast and vulnerable population cannot afford for its tools to fail due to poor design. While UI trends aren’t inherently problematic, their application within wellbeing products demands a <strong>radically contextual approach</strong>.</p>
<p>Even a five-minute decompression tool between meetings has an emotional context, and a style chosen for its look — glassmorphism, a certain flavour of ultra-minimalism — can miss it, however sophisticated the audience. The point isn’t that these styles are wrong; it’s that <strong>the look has to answer to the moment</strong>. Soft biomorphic shapes or fluid transitions can genuinely help when they directly serve the goal of calm — and the same elements become noise when they’re there to impress.</p>
<blockquote>What carries the highest risk is lifting a visually striking “Dribbble shot” and applying it without deep adaptation: it solves for the designer’s portfolio, not the user’s need.</blockquote>

A Practical Framework For Evaluation
<p>The five fronts outlined above are not just a diagnostic lens; they are the foundation of an <strong>evaluation framework</strong> for anyone designing in the mental health space. Before incorporating any trendy visual or interaction pattern, it is worth running it through each of them in sequence:</p>
<p><img src="https://files.smashing.media/articles/designing-distressed-users-mental-health-apps-ui/024-practical-framework-evaluation.png"></p>
<ul>
<li><strong>Cognitive load</strong><br>Does this reduce the effort required for someone who is overwhelmed, or does it add another layer of complexity to an already strained experience?</li>
<li><strong>Emotional alignment</strong><br>Does this support a wide spectrum of emotional states, including distress and exhaustion, or does it clash with the context in which users are most likely to arrive?</li>
<li><strong>Navigational reliability</strong><br>Does this build trust through predictability, allowing users to return and find their way without relearning, or does it prioritise novelty at the cost of consistency?</li>
<li><strong>Accessibility</strong><br>Does this uphold or enhance accessibility for diverse sensory and cognitive abilities, or does it quietly exclude the users who may need support the most?</li>
<li><strong>Engagement integrity</strong><br>Does this invite use in a way that is supportive and non-coercive, or does it borrow mechanics from entertainment products that may create pressure rather than relief?</li>
</ul>
<p>A design that passes all five holds together as something more than usable: it becomes a tool that users can trust enough to return to in moments of genuine need.</p>
<p>Trends can be inspiring. They can win awards and generate buzz. But in mental health, sometimes the best design is the one that helps users feel understood — a <strong>quiet helper</strong> they trust enough to return to in moments of stress and vulnerability. It doesn’t steal the spotlight, but focuses on the user’s emotions. In the end, the ultimate goal is not for the interface to be seen — but for the support to be felt.</p>
]]></description>
      <pubDate>Thu, 09 Jul 2026 15:00:00 +0000</pubDate>
      <link>https://smashingmagazine.com/2026/07/designing-distressed-users-mental-health-apps-ui/</link>
      <dc:creator>Articles on Smashing Magazine — For Web Designers And Developers</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5301447040</guid>
    </item>
    <item>
      <title><![CDATA[Boundary-Aware Styling in CSS]]></title>
      <description><![CDATA[
<p class="wp-block-paragraph">There are times we might prefer specific styles for elements located near the edges of their containers or the screen. </p>



<p class="wp-block-paragraph">Different colors, sizes, or rotations based on how close they are to the edge and which side of the container they are on. Instead of relying on class names, item counts, or sibling positions, we can achieve this with <em>a single CSS function</em>.</p>



<p class="wp-block-paragraph">The <code>view()</code> CSS function is known for its use in scroll-driven animations, affecting elements based on their visibility within a scroll container. However, it can be <strong>used without involving any actual scrolling or motion effect</strong>.</p>



<p class="wp-block-paragraph">This article shows you how to do that to create more adaptive interfaces based on their closeness to their container’s boundaries.</p>



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



<h2 class="wp-block-heading">How <code>view()</code> Works</h2>



<p class="wp-block-paragraph">The&nbsp;<code>view()</code>&nbsp;function links an animation’s progress to its visibility within its scroll container. When we apply&nbsp;<code>animation-timeline: view()</code>, the browser tracks the position of the element relative to its nearest scrollable container. The animation progress goes like this:</p>



<ul class="wp-block-list">
<li><strong>0% Progress (Start)</strong>: Triggered the exact moment the top/left/leading edge of the element enters the visible boundary of the scrollport.</li>



<li><strong>100% Progress (End)</strong>: Reached when the bottom/right/trailing edge of the element completely exits the opposite side of the scrollport.</li>
</ul>



<h2 class="wp-block-heading">Boundary-Aware Style on Static Elements</h2>



<p class="wp-block-paragraph">Here’s a group of elements that are arranged in a three-column grid.</p>


<pre class="wp-block-code"><span><code class="hljs language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card"</span>&gt;</span>Contacts<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card"</span>&gt;</span>Shipping information<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card"</span>&gt;</span>Find a product<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- more --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></code></span></pre>

<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-selector-class">.card</span> {
  <span class="hljs-attribute">background</span>: <span class="hljs-built_in">rgb</span>(<span class="hljs-number">16</span>, <span class="hljs-number">111</span>, <span class="hljs-number">235</span>);
}</code></span></pre>


<p class="wp-block-paragraph">I want the cards to have different colors based on their horizontal distribution in their container. So, first, we&nbsp;<strong>turn the cards’ parent,</strong>&nbsp;<code>&lt;main&gt;</code><strong>, into a scroll container</strong>.</p>



<div class="wp-block-group learn-more"><div class="wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow">
<p class="wp-block-paragraph"><strong>How does an element become a scroll container?</strong>&nbsp;An element automatically turns into a scroll container when you apply one of these:</p>



<ul class="wp-block-list">
<li><code>overflow: scroll;</code></li>



<li><code>overflow: auto;</code></li>



<li><code>overflow: hidden;</code> (No scrollbars, but the content is scrollable programmatically)</li>
</ul>
</div></div>



<p class="wp-block-paragraph">Let’s update&nbsp;<code>&lt;main&gt;</code>&nbsp;to be a scroll container:</p>


<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-selector-tag">main</span> {
  <span class="hljs-attribute">overflow</span>: hidden;
}</code></span></pre>


<p class="wp-block-paragraph">If <code>&lt;main&gt;</code> doesn’t become a scroll container, the <code>view()</code> function we’ll later apply might take the <code>&lt;body&gt;</code> element as the nearest scroll container.</p>



<p class="wp-block-paragraph">To change the cards’ colors, I’ll use the <code>hue-rotate()</code> filter. This goes into a <code>@keyframes</code> declaration.</p>


<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-keyword">@keyframes</span> cr {
  <span class="hljs-comment">/* Set the color range */</span>
  <span class="hljs-selector-tag">from</span> { <span class="hljs-attribute">filter</span>: <span class="hljs-built_in">hue-rotate</span>(<span class="hljs-number">180deg</span>); }
}</code></span></pre>


<p class="wp-block-paragraph">Use the keyframes animation on the cards with the <code>view(x)</code> timeline. <code>x</code> is the horizontal axis.</p>


<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-selector-class">.card</span> {
  <span class="hljs-attribute">background</span>: <span class="hljs-built_in">rgb</span>(<span class="hljs-number">16</span>, <span class="hljs-number">111</span>, <span class="hljs-number">235</span>);
  <span class="hljs-attribute">animation</span>: cr;
  <span class="hljs-attribute">animation-timeline</span>: <span class="hljs-built_in">view</span>(x);
}</code></span></pre>


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



<p class="wp-block-paragraph">Another example:</p>


<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-keyword">@property</span> --a {
  <span class="hljs-selector-tag">syntax</span>: "&lt;<span class="hljs-selector-tag">angle</span>&gt;";
  <span class="hljs-selector-tag">initial-value</span>: 0<span class="hljs-selector-tag">deg</span>;
  <span class="hljs-selector-tag">inherits</span>: <span class="hljs-selector-tag">false</span>;
}
<span class="hljs-keyword">@keyframes</span> cr {
  <span class="hljs-selector-tag">from</span> { <span class="hljs-attribute">--a</span>: <span class="hljs-number">360deg</span>; }
}
<span class="hljs-selector-tag">main</span> {
  <span class="hljs-attribute">overflow</span>: hidden;
}
<span class="hljs-selector-class">.card</span> {
  <span class="hljs-attribute">background</span>: <span class="hljs-built_in">conic-gradient</span>( from var(--a), red, pink, crimson, fuchsia, orange);
  <span class="hljs-attribute">animation</span>: cr;
  <span class="hljs-attribute">animation-timeline</span>: <span class="hljs-built_in">view</span>(x);
}</code></span></pre>


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



<p class="wp-block-paragraph">Here’s what some simple style changes look like:</p>



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



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



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



<h2 class="wp-block-heading">Boundary-aware style on draggable elements</h2>


<pre class="wp-block-code"><span><code class="hljs language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"draggable-box"</span>&gt;</span>
  Draggable box
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></code></span></pre>


<p class="wp-block-paragraph">The <code>#draggable-box</code>&nbsp;element is a draggable (by JavaScript). Its position on screen, the default scroll container, changes as we drag it across the viewport. So does its color.</p>


<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-keyword">@keyframes</span> shiftColor {
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">background-color</span>: <span class="hljs-built_in">rgb</span>(<span class="hljs-number">203</span>, <span class="hljs-number">49</span>, <span class="hljs-number">242</span>);
  }
}
<span class="hljs-selector-id">#draggable-box</span> {
  <span class="hljs-attribute">background-color</span>: <span class="hljs-built_in">rgb</span>(<span class="hljs-number">41</span>, <span class="hljs-number">113</span>, <span class="hljs-number">248</span>);
  <span class="hljs-attribute">animation</span>: shiftColor;
  <span class="hljs-attribute">animation-timeline</span>: <span class="hljs-built_in">view</span>(x);
}</code></span></pre>


<p class="wp-block-paragraph">See the box’s color change as you drag it from the left to the right side of the screen.</p>



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



<p class="wp-block-paragraph">You can apply the same principle in HTML sliders.</p>


<pre class="wp-block-code"><span><code class="hljs language-css">&lt;<span class="hljs-selector-tag">input</span> <span class="hljs-selector-tag">id</span>="<span class="hljs-selector-tag">slider-range</span>" <span class="hljs-selector-tag">type</span>="<span class="hljs-selector-tag">range</span>" <span class="hljs-selector-tag">min</span>="0" <span class="hljs-selector-tag">max</span>="100" <span class="hljs-selector-tag">value</span>="50"&gt;</code></span></pre>

<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-keyword">@keyframes</span> shiftColor {
  <span class="hljs-selector-tag">from</span> { <span class="hljs-attribute">background-color</span>: red; }
  50% { <span class="hljs-attribute">background-color</span>: yellow; }
  <span class="hljs-selector-tag">to</span> { <span class="hljs-attribute">background-color</span>: green; }
}
<span class="hljs-selector-id">#slider-range</span> {
  <span class="hljs-comment">/* the slider track */</span>
  &amp;::-webkit-slider-runnable-track{ <span class="hljs-comment">/* Webkit */</span>
    <span class="hljs-attribute">overflow</span>: hidden;
  }
  &amp;<span class="hljs-selector-pseudo">::-moz-range-track</span> {             <span class="hljs-comment">/* Firefox */</span>
    <span class="hljs-attribute">overflow</span>: hidden;
  }
 <span class="hljs-comment">/* the slider thumb */</span>
  &amp;<span class="hljs-selector-pseudo">::-webkit-slider-thumb</span> {         <span class="hljs-comment">/* Webkit */</span>
    <span class="hljs-attribute">animation</span>: shiftColor;
    <span class="hljs-attribute">animation-timeline</span>: <span class="hljs-built_in">view</span>(x);
  }
  &amp;<span class="hljs-selector-pseudo">::-moz-range-thumb</span> {             <span class="hljs-comment">/* Firefox */</span>
    <span class="hljs-attribute">animation</span>: shiftColor;
    <span class="hljs-attribute">animation-timeline</span>: <span class="hljs-built_in">view</span>(x);
  }
}</code></span></pre>


<p class="wp-block-paragraph">The slider’s track is set as the scrollable container with <code>overflow: hidden</code>, and <code>view(x)</code> is applied to the thumb. There are three color stops in the <code>@keyframes</code> animation that <code>view()</code> drives. As we slide the thumb along the track from one edge to the other, its color changes between the edges and the middle.</p>



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



<p class="wp-block-paragraph">Here’s another example where an anchor-positioned element to the thumb changes.</p>


<pre class="wp-block-code"><span><code class="hljs language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"slider-container"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"slider-range"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"range"</span> <span class="hljs-attr">min</span>=<span class="hljs-string">"0"</span> <span class="hljs-attr">max</span>=<span class="hljs-string">"100"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"50"</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- the anchor-positioned custom thumb --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"slider-thumb"</span>&gt;</span><span class="hljs-symbol">&amp;#x2B50;</span><span class="hljs-symbol">&amp;#xFE0F;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></code></span></pre>

<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-keyword">@property</span> --a {
  <span class="hljs-selector-tag">syntax</span>: "&lt;<span class="hljs-selector-tag">angle</span>&gt;";
  <span class="hljs-selector-tag">initial-value</span>: 0<span class="hljs-selector-tag">deg</span>;
  <span class="hljs-selector-tag">inherits</span>: <span class="hljs-selector-tag">false</span>;
}
<span class="hljs-keyword">@keyframes</span> shiftColor {
  <span class="hljs-selector-tag">from</span> { <span class="hljs-attribute">--a</span>: <span class="hljs-number">370deg</span> }
  50%  { <span class="hljs-attribute">--a</span>: <span class="hljs-number">180deg</span> }
  <span class="hljs-selector-tag">to</span>   { <span class="hljs-attribute">--a</span>: -<span class="hljs-number">10deg</span> }
}
<span class="hljs-selector-id">#slider-container</span> {
  <span class="hljs-attribute">overflow</span>: hidden;
}
<span class="hljs-selector-id">#slider-thumb</span> {
  <span class="hljs-attribute">background-image</span>: <span class="hljs-built_in">conic-gradient</span>(crimson var(--a), dodgerblue <span class="hljs-built_in">var</span>(--a));
  <span class="hljs-attribute">animation</span>: shiftColor;
  <span class="hljs-attribute">animation-timeline</span>: <span class="hljs-built_in">view</span>(x);
}</code></span></pre>


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



<p class="wp-block-paragraph">You can experiment with the axes, animation range, animation steps, and more to achieve different results. Just make sure the right ancestor is set as the scroll container using the&nbsp;<code>overflow</code>&nbsp;property.</p>



<p class="wp-block-paragraph"></p>
]]></description>
      <pubDate>Tue, 07 Jul 2026 15:09:39 +0000</pubDate>
      <link>https://master.dev/blog/boundary-aware-styling-in-css/</link>
      <dc:creator>Master.dev Blog RSS Feed</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5295785324</guid>
    </item>
    <item>
      <title><![CDATA[Links For You (7/5/26)]]></title>
      <description><![CDATA[
				
                        <p>In my <a href="https://www.raymondcamden.com/2026/06/21/links-for-you-62126">last links post</a>, I hinted that I may have some good news on the job front, and if you follow me on <a href="https://linkedin.com/in/raymondcamden">LinkedIn</a> you already know that I've signed on to my next gig. Tomorrow morning I'll share on here (and on LI) details about the new gig, but I am <em>beyond</em> excited about this new job. With that out of the way, how about some happy links for what's going to be a really dang good week?</p>
<h2 id="javascript-in-the-shadows" tabindex="-1"><a class="header-anchor" href="https://www.raymondcamden.com/2026/07/05/links-for-you-7526#javascript-in-the-shadows">JavaScript in the Shadows</a></h2>
<p>As one of the new proposals for JavaScript, the <a href="https://github.com/tc39/proposal-shadowrealm">ShadowRealm API</a> is pretty interesting. It allows for the creation of a realm, or in my mind, an environment, with JavaScript that is 100% disconnected from the main environment of your browser window. Mat Marquis gives a great introduction to this in <a href="https://css-tricks.com/soon-we-can-finally-banish-javascript-to-the-shadowrealm/">Soon We Can Finally Banish JavaScript to the ShadowRealm</a> over on CSS Tricks. (If for some reason you've never heard of <a href="https://css-tricks.com">css-tricks</a>, you should absolutely bookmark it and make it part of your required reading.) Mat's introduction is a pretty fun read as well.</p>
<h2 id="brownies-for-storage" tabindex="-1"><a class="header-anchor" href="https://www.raymondcamden.com/2026/07/05/links-for-you-7526#brownies-for-storage">Brownies for Storage</a></h2>
<p>I'm a sucker for anything browser-storage related, and <a href="https://github.com/franciscop/brownies">Brownies</a> is a great example of that. Brownies is a library that wraps cookies, Web Storage, and IndexedDB, all in one. Here's an example from their docs:</p>
<pre><code class="language-js">import { cookies, local, db } from 'brownies';

cookies.token = 42;     // Set it
let t = cookies.token;  // Get it
delete cookies.token;   // Eat it

local.token = 42;       // Set it
let t = local.token;    // Get it
delete local.token;     // Del it

// db is ASYNC so read is different
db.token = 42;          // Set it
let t = await db.token; // Get it
delete db.token;        // Del it
</code></pre>
<p>This looks quite delicious. It's probably not fair to say it is a wrapper for IndexedDB as from what I can see, it's just doing a key/value store and not letting you specify custom object stores and such. In fact, the docs for <code>db</code> seem to be missing, and the repository doesn't have issues enabled so I'm going to have to find another way to leave that feedback, but check it out nonetheless. (I sent the author an email about this.)</p>
<h2 id="don't-use-javascript!" tabindex="-1"><a class="header-anchor" href="https://www.raymondcamden.com/2026/07/05/links-for-you-7526#don't-use-javascript!">Don't use JavaScript!</a></h2>
<p>My first two links were JavaScript related, so why not advise <em>against</em> JavaScript in my third link? While not an "anti" JavaScript resource, <a href="https://aarontgrogg.github.io/NoLoJS/">NoLoJS</a> is a resource showing multiple different UX/UI components that can be built with no, or at least little, JavaScript. As I <a href="https://www.raymondcamden.com/2026/07/01/building-custom-form-selection-blocks-no-js-all-css">discovered last week</a>, there's a lot you can do with CSS and removing JavaScript dependencies is just one benefit.</p>
<h2 id="just-for-fun" tabindex="-1"><a class="header-anchor" href="https://www.raymondcamden.com/2026/07/05/links-for-you-7526#just-for-fun">Just For Fun</a></h2>
<p>Yesterday was July 4th which means hot dogs and fireworks and that's all... right? Unsurprisingly, there's quite a few examples of fireworks being rendered in HTML. Here's one fun example that's pure CSS:</p>
<p class="codepen" data-height="700" data-pen-title="Pure CSS Fireworks" data-default-tab="result" data-slug-hash="WNMmQX" data-user="yshlin" style="height: 700px; 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/yshlin/pen/WNMmQX">
  Pure CSS Fireworks</a> by Eddie Lin (<a href="https://codepen.io/yshlin">@yshlin</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>
                        
                
				]]></description>
      <pubDate>Sun, 05 Jul 2026 17:02:14 +0000</pubDate>
      <link>https://www.raymondcamden.com/2026/07/05/links-for-you-7526</link>
      <dc:creator>Raymond Camden</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5292967975</guid>
    </item>
    <item>
      <title><![CDATA[Fixing full-bleed CSS]]></title>
      <description><![CDATA[<div class="Alert"><p>I’m a front-end developer not a medical practitioner. If you’re bleeding IRL visit the hospital and stop googling medical issues!</p></div><p>The <a href="https://www.joshwcomeau.com/css/full-bleed/" rel="noopener noreferrer" target="_blank">full-bleed layout</a> — as described there by Josh Comeau — can be done with CSS grid (<a href="https://dbushell.com/2026/04/02/css-subgrid-is-super-good/">and subgrid</a>). Sometimes you can’t grid the entire page.</p><p>That’s where <a href="https://piccalil.li/blog/creating-a-full-bleed-css-utility/" rel="noopener noreferrer" target="_blank">Andy Bell’s utility class</a> is useful.</p><pre data-lang="css" tabindex="0" translate="no" id="pre-c770d7de"><code><span class="line"><span class="syntax-4ddd3d58">.full-bleed</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">width</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-765b360f">100</span><span class="syntax-1656d967">vw</span><span class="syntax-26f6a8db">;</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">margin-left</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-3eeadcf9">calc</span><span class="syntax-26f6a8db">(</span><span class="syntax-765b360f">50</span><span class="syntax-1656d967">%</span><span class="space"> </span><span class="syntax-1656d967">-</span><span class="space"> </span><span class="syntax-765b360f">50</span><span class="syntax-1656d967">vw</span><span class="syntax-26f6a8db">);</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span></code></pre><p>But it ain’t perfect. The issue is that viewport units don’t solve the <a href="https://www.smashingmagazine.com/2023/12/new-css-viewport-units-not-solve-classic-scrollbar-problem/" rel="noopener noreferrer" target="_blank">classic scrollbar problem</a>. If you’re on macOS or a fancy OS that has fancy scrollbars, test on Windows! <code>100vw</code> can be wider than the viewport. Because why would browsers do anything sensible?</p><p>It’s hard to see in <a href="https://codepen.io/piccalilli/pen/vYOJjNw" rel="noopener noreferrer" target="_blank">Andy’s CodePen</a> but a few pixels can be cropped either side. Add something like a border or shadow and it’s easier to see.</p><pre data-lang="css" tabindex="0" translate="no" id="pre-926afc7e"><code><span class="line"><span class="syntax-4ddd3d58">.full-bleed</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">box-shadow</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-765b360f">inset</span><span class="space"> </span><span class="syntax-765b360f">0</span><span class="space"> </span><span class="syntax-765b360f">0</span><span class="space"> </span><span class="syntax-765b360f">0</span><span class="space"> </span><span class="syntax-765b360f">10</span><span class="syntax-1656d967">px</span><span class="space"> </span><span class="syntax-765b360f">yellow</span><span class="syntax-26f6a8db">;</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span></code></pre><figure class="Image"><img src="https://dbushell.com/images/blog/2026/full-bleed-crop.avif" alt="Yellow border is slightly clipped either side." width="375" height="150" decoding="async" fetchpriority="low" loading="lazy" id="--img-f718a368"><figcaption>Ignore the pink left border that’s my blog style.</figcaption></figure><p>This is not always a problem but it can lead to subtle alignment issues. By the way, macOS has a scrollbar setting <em>“Show scroll bars &gt; Always”</em> that’ll let you test the issue.</p><figure class="Image"><img src="https://dbushell.com/images/blog/2026/macos-scrollbar-preferences.avif" alt="macOS scrollbar settings on the appearance tab." width="540" height="118" decoding="async" fetchpriority="low" loading="lazy" id="--img-fec9c3ac"></figure><p>Andy solves this partially by hiding horizontal overflow on the <code>&lt;body&gt;</code> element.</p><pre data-lang="css" tabindex="0" translate="no" id="pre-194af6f5"><code><span class="line"><span class="syntax-1656d967">body</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">overflow-x</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-765b360f">hidden</span><span class="syntax-26f6a8db">;</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span></code></pre><p>An alternative fix is to always reserve space for the classic scrollbar.</p><pre data-lang="css" tabindex="0" translate="no" id="pre-5ae2ebc6"><code><span class="line"><span class="syntax-1656d967">html</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">scrollbar-gutter</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-26f6a8db">stable;</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span></code></pre><p>That can look weird if there is no vertical scroll necessary.</p><h2 id="modern-solution">Modern solution</h2><p>The “modern” approach is to use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment" rel="noopener noreferrer" target="_blank">CSS containment</a>. Turn the <code>&lt;body&gt;</code> element (or any 100% width child) into a container. Then replace the viewport units with container units.</p><pre data-lang="css" tabindex="0" translate="no" id="pre-f8beed2c"><code><span class="line"><span class="syntax-1656d967">body</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">container</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-26f6a8db">body</span><span class="space"> </span><span class="syntax-26f6a8db">/</span><span class="space"> </span><span class="syntax-26f6a8db">inline-size;</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">overflow-x</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-765b360f">clip</span><span class="syntax-26f6a8db">;</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span>
<span class="line"></span>
<span class="line"><span class="syntax-4ddd3d58">.full-bleed</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">inline-size</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-765b360f">100</span><span class="syntax-1656d967">cqi</span><span class="syntax-26f6a8db">;</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">margin-inline-start</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-3eeadcf9">calc</span><span class="syntax-26f6a8db">(</span><span class="syntax-765b360f">50</span><span class="syntax-1656d967">%</span><span class="space"> </span><span class="syntax-1656d967">-</span><span class="space"> </span><span class="syntax-765b360f">50</span><span class="syntax-1656d967">cqi</span><span class="syntax-26f6a8db">);</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span></code></pre><p>Now hiding overflow is not strictly necessary. I prefer <code>clip</code> — see <a href="https://ishadeed.com/article/overflow-clip/" rel="noopener noreferrer" target="_blank">Overflow Clip guide</a> by Ahmad Shadeed. I clip out of caution because <a href="https://dbushell.com/2026/07/02/the-modern-app/">I make dumb things.</a> I also use <a href="https://dbushell.com/2021/02/02/changing-css-for-good-logical-properties-and-values/">logical properties and values</a> to support right-to-left (RTL) text direction. Ahmad has an excellent <a href="https://rtlstyling.com/" rel="noopener noreferrer" target="_blank">RTL Styling 101</a> too.</p><p>But wait!</p><p>Using <code>cqi</code> units assumes <code>body</code> is the parent container of the <code>.full-bleed</code> element. What if we have nested containers? Check this out. <a href="https://codepen.io/dbushell/pen/rajYvPj" rel="noopener noreferrer" target="_blank">I’ve forked Andy’s CodePen</a> to add another <code>.inside</code> container that is <strong>not</strong> the full viewport width.</p><figure class="Image"><img src="https://dbushell.com/images/blog/2026/full-bleed-inside.avif" alt="Inspecting CSS container layout using Chromium dev tools to show that the inside container is smaller." width="1084" height="654" decoding="async" fetchpriority="low" loading="lazy" id="--img-6b856c86"></figure><p>This alone would usually break the new <code>.full-bleed</code> class and ruin the fun.</p><p>But we can fix that!</p><pre data-lang="css" tabindex="0" translate="no" id="pre-011df034"><code><span class="line"><span class="syntax-1656d967">@property</span><span class="space"> </span><span class="syntax-26f6a8db">--body-size</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-1656d967">syntax:</span><span class="space"> </span><span class="syntax-1656d967">"&lt;length&gt;</span><span class="space"> </span><span class="syntax-1656d967">|</span><span class="space"> </span><span class="syntax-1656d967">&lt;percentage&gt;";</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-1656d967">inherits:</span><span class="space"> </span><span class="syntax-1656d967">true;</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-1656d967">initial-value:</span><span class="space"> </span><span class="syntax-1656d967">100%;</span></span>
<span class="line"><span class="syntax-1656d967">}</span></span>
<span class="line"></span>
<span class="line"><span class="syntax-1656d967">body</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">container</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-26f6a8db">body</span><span class="space"> </span><span class="syntax-26f6a8db">/</span><span class="space"> </span><span class="syntax-26f6a8db">inline-size;</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span>
<span class="line"></span>
<span class="line"><span class="syntax-4ddd3d58">.inside</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-26f6a8db">--body-size</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-765b360f">100</span><span class="syntax-1656d967">cqi</span><span class="syntax-26f6a8db">;</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">container</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-765b360f">inside</span><span class="space"> </span><span class="syntax-26f6a8db">/</span><span class="space"> </span><span class="syntax-26f6a8db">inline-size;</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span>
<span class="line"></span>
<span class="line"><span class="syntax-4ddd3d58">.full-bleed</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">inline-size</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-3eeadcf9">var</span><span class="syntax-26f6a8db">(--body-size);</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">margin-inline-start</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-3eeadcf9">calc</span><span class="syntax-26f6a8db">(</span><span class="syntax-765b360f">50</span><span class="syntax-1656d967">%</span><span class="space"> </span><span class="syntax-1656d967">-</span><span class="space"> </span><span class="syntax-26f6a8db">(</span><span class="syntax-765b360f">0.5</span><span class="space"> </span><span class="syntax-1656d967">*</span><span class="space"> </span><span class="syntax-3eeadcf9">var</span><span class="syntax-26f6a8db">(--body-size)));</span><span class="space"> </span><span class="syntax-7b4433fd">/*</span><span class="space"> </span><span class="syntax-7b4433fd">†</span><span class="space"> </span><span class="syntax-7b4433fd">*/</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span></code></pre><p>What is that <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@property" rel="noopener noreferrer" target="_blank"><code>@property</code> magic?</a></p><p>To be honest I struggle to wrap my smooth brain around this!</p><p>Let me try to explain it to myself. Without the at-rule the value of <code>--body-size</code> is calculated at the time of use, i.e. within <code>.full-bleed</code> and therefore relative to the <code>inside</code> container. By explicitly defining a <code>@property</code> the value is now calculated when it’s set within <code>.inside</code>. There <code>100cqi</code> refers to the parent <code>body</code> container and <code>.full-bleed</code> inherits that value.</p><p>But what if you have more containers?</p><p role="img" aria-label="ASCII art of an incensed person flipping a table.">(╯°□°)╯︵ ┻━┻</p><p><em>Gosh!</em> Stop being so difficult!</p><p>I would have a direct child of <code>&lt;body&gt;</code> like <code>&lt;main&gt;</code> set the <code>--body-size</code> value.</p><pre data-lang="html" tabindex="0" translate="no" id="pre-b14b429b"><code><span class="line"><span class="syntax-26f6a8db">&lt;</span><span class="syntax-1656d967">body</span><span class="syntax-26f6a8db">&gt;</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-26f6a8db">&lt;</span><span class="syntax-1656d967">main</span><span class="syntax-26f6a8db">&gt;</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="space"> </span><span class="space"> </span><span class="syntax-7b4433fd">&lt;!--</span><span class="space"> </span><span class="syntax-7b4433fd">blah</span><span class="space"> </span><span class="syntax-7b4433fd">--&gt;</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-26f6a8db">&lt;/</span><span class="syntax-1656d967">main</span><span class="syntax-26f6a8db">&gt;</span></span>
<span class="line"><span class="syntax-26f6a8db">&lt;/</span><span class="syntax-1656d967">body</span><span class="syntax-26f6a8db">&gt;</span></span></code></pre><p><small><sup>†</sup> I’m multiplying by 0.5 because division is for chumps.</small></p><h2 id="next-gen-solution">Next-gen solution</h2><p>What CSS needs is a way to reference a container when using container units.</p><p><a href="https://github.com/w3c/csswg-drafts/issues/7858" rel="noopener noreferrer" target="_blank">Ideas have been proposed</a> for example:</p><pre data-lang="css" tabindex="0" translate="no" id="pre-de80f5a5"><code><span class="line"><span class="syntax-4ddd3d58">.full-bleed</span><span class="space"> </span><span class="syntax-26f6a8db">{</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">inline-size</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-765b360f">100</span><span class="syntax-1656d967">cqi</span><span class="syntax-26f6a8db">(body);</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">inline-size</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-3eeadcf9">calc</span><span class="syntax-26f6a8db">(</span><span class="syntax-765b360f">100</span><span class="space"> </span><span class="syntax-1656d967">*</span><span class="space"> </span><span class="syntax-26f6a8db">cqi(body));</span></span>
<span class="line"><span class="space"> </span><span class="space"> </span><span class="syntax-3eeadcf9">inline-size</span><span class="syntax-1656d967">:</span><span class="space"> </span><span class="syntax-3eeadcf9">calc</span><span class="syntax-26f6a8db">(</span><span class="syntax-765b360f">100</span><span class="space"> </span><span class="syntax-1656d967">*</span><span class="space"> </span><span class="syntax-26f6a8db">container(cqi,</span><span class="space"> </span><span class="syntax-26f6a8db">body));</span></span>
<span class="line"><span class="syntax-26f6a8db">}</span></span></code></pre><p>Cancel <a href="https://wpt.fyi/interop-2026" rel="noopener noreferrer" target="_blank">Interop 2026</a> and make this happen!</p>
<hr>
<p>
Thanks for reading! Follow me on <a href="https://dbushell.com/mastodon/">Mastodon</a> and <a href="https://dbushell.com/bluesky/">Bluesky</a>.
Subscribe to my <a href="https://dbushell.com/rss.xml">Blog</a> and <a href="https://dbushell.com/notes/rss.xml">Notes</a> or <a href="https://dbushell.com/merge/rss.xml">Combined</a> feeds.
</p>
]]></description>
      <pubDate>Fri, 03 Jul 2026 09:15:04 +0000</pubDate>
      <link>https://dbushell.com/2026/07/03/fixing-full-bleed-css/</link>
      <dc:creator>dbushell.com (blog)</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5291032673</guid>
    </item>
    <item>
      <title><![CDATA[Goodbye, forever, probably.]]></title>
      <description><![CDATA[ 
    <p class="post__p">In December 2025 I wrote about <a href="https://whitep4nth3r.com/blog/goodbye-twitch/" target="_blank">my retirement from live streaming</a>, which was once a valuable and very enjoyable part of my career in developer relations (DevRel). Since closing that chapter I have <a href="https://whitep4nth3r.com/blog/website-redesign-2026/" target="_blank">redesigned my website</a>, <a href="https://whitep4nth3r.com/blog/website-for-drummers-free-pdf-downloads-transcriptions/" target="_blank">launched a new side project,</a> <a href="https://whitep4nth3r.com/blog/new-electronica-music-manchester-uk/" target="_blank">released some new music</a>, and made a lot of space to reflect on the last five and a half years I’ve spent in DevRel and ultimately, my place in the technology industry.</p><h2 class="post__h2">On birds, nature, and being carefree</h2><p class="post__p">I recently spruced up my garden with a bird-feeding station and it has been an absolute joy to sit and watch nature at every opportunity. As I observe the wood pigeons, collared doves, gold crests, house sparrows, robins and a family of squirrels coexist peacefully and peck the seeds and munch on the nuts (even if the squirrels do often steal the bird food) I become painfully aware of how I’ve lived so much of my life on The Internet in the last six years. I also reflect on how much has changed in the technology industry throughout that time, how much negativity and weird stuff I am forced to consume every day as a result, and how much I yearn to be as carefree and alive as the nature that I love so much to sit and witness.</p><p class="post__p">Before working in DevRel I was a tech lead for a small creative agency in Manchester, UK. At the time I was frustrated with how much of my time was taken up with meetings, presentations, sprint and stakeholder management, and how little time I had to dedicate to building and working on my craft as a front-end developer. As a self-taught engineer who worked incredibly hard to pivot from teaching music to a successful tech career, I moved to DevRel in 2021, which promised to give me the time and space to continue to hone my craft. At the time, this small niche of the tech industry offered me the opportunity to explore, learn and build through teaching. I couldn't believe what a perfect combination of my skills this type of job was at the time. It felt too good to be true, honestly.</p><h2 class="post__h2">The good parts of DevRel</h2><p class="post__p">The good parts of DevRel are really good. Throughout the years I’ve had the privilege of helping many people succeed through my teaching, mentoring and support, and at the same time, I’ve been able to build some fun projects and learn about The Web along the way. I even picked up some half-decent video editing skills. Through the opportunity to travel and speak at conferences, I’ve made some truly wonderful friends scattered around the world, who all come from different backgrounds and cultures, brought together through their shared love and passion for technology (or at least, what it used to be). As I reflect on the last five and a half years, I’ve found myself at the heart of a thriving network of technologists who all have each other’s back. And I will be eternally grateful for that.</p><p class="post__p">Unfortunately, for me, the difficult parts have outweighed the good.</p><h2 class="post__h2">The difficult parts of DevRel</h2><p class="post__p">The approaches to effective DevRel can vary wildly depending on the individuals that make up a team and the company itself, but ultimately, the primary goal of a DevRel team is to inspire and educate a community to encourage product adoption, and to help users achieve meaningful outcomes with that product in as short a time as possible. Unfortunately, the nuances within this clear remit are often at odds with what business leaders require in terms of being able to measure <b class="post__p--italic">success</b>.</p><h3 class="post__h3">Arbitrary measures of success</h3><p class="post__p">Developer education in the community is a long game. Product adoption by the right audience takes time, patience, and an experimental approach. But very often, this does not satisfy those in leadership positions, as they want to see <b class="post__p--italic">measurable success</b> in the short-term. I get it, I really do. However, it is notoriously difficult to measure the short-term impact and success of DevRel unless you use somewhat arbitrary metrics. These could be sign-ups (that might not end up being activated), blog post views or video stats in order just to have something to measure and report on. </p><p class="post__p">The issue with this is that you can measure <b class="post__p--italic">anything</b>, and you'll naturally optimise for making that line go up, <b class="post__p--bold">regardless of whether it's the right approach for the product or the customers or the team.</b> For example, you could write a click-bait blog post and reach the front page of Hacker News. You could make a silly YouTube short that gets 15k views. Congrats, you demonstrated success! But it's most likely the "wrong" success, as those little Internet escapades most probably won't convert to impactful product adoption and paying customers. What's more, all of this distracts and diverts the efforts of DevRel teams from real impactful work that shows meaningful results in the long term.</p><p class="post__p">Like many others in the industry, I have tried to find ways to show how DevRel and education is more than content creation, arbitrary metrics and making the line go up. But my detailed article <a href="https://whitep4nth3r.com/blog/defining-paths-to-business-value-in-developer-relations/" target="_blank">Defining Paths to Business Value in Developer Relations</a>, in retrospect, feels like a bit of a cry for help in trying to justify the profession to people who have already decided that regardless, they need those meaningless results in the short term in order to appease the shareholders.</p><h3 class="post__h3">Fighting to be valued and justify my existence</h3><p class="post__p">Given DevRel is a long game, with the only way to measure short-term successes through entirely arbitrary metrics, businesses who are struggling to grow in this reckless economy often find financial investment into DevRel very difficult to justify. Even as this niche of the tech industry was seemingly <b class="post__p--italic">thriving</b> in the digital post-pandemic pre-AI era of 2021-2023, my day-to-day work was always accompanied by an ominous, low-level, yet all-encompassing feeling of anxiety centred on the premise that if I cannot demonstrate short-term financial success for the company, then I could lose my job at a moment's notice and be without a way to support my family. In fact, I was made redundant in 2023 from a DevRel role at a company that was moving away from a community-centred approach to an enterprise-based strategy at the time. On reflection, what most notably contributed to the feelings of anxiety was that my job and responsibilities were being heavily micromanaged by those that did not understand or respect DevRel as a discipline. This came as a direct result of the lack of immediate measurable metrics that convert to financial gains, which often tends to send leadership into a flailing panic of pivots which leave a wake of destruction in their path. </p><p class="post__p">As a result, I have — all too often — <b class="post__p--bold">overworked myself</b> in a desperate attempt to justify my existence, demonstrate my value and impact, and keep my job. This has, more than once, caused me to suffer from health problems including joint and nerve pain, despite being "completely healthy" according to numerous tests. At its worst at the start of 2025, I couldn’t even use my hands due to the pain for work or anything else, and I had to <a href="https://whitep4nth3r.com/blog/how-i-learned-to-code-with-my-voice/" target="_blank">learn how to code with my voice</a>. Unfortunately, I’ve been suffering from wrist pain and unexplained ailments again since December 2025. It's becoming quite embarrassing how frequently I visit the doctors only to realise that I am, once again, suffering from work-related stress and anxiety that manifests in my physical body.</p><p class="post__p">It’s worth mentioning that in my latest role, my education work was definitely respected and understood, but by the end of my tenure, it had become difficult to <b class="post__p--italic">justify</b>. Given the shift in the technology industry that has changed how people learn and seek information (more on this later), it was difficult to find the right captive audience for my content through the more traditional methods that worked well in the past. I know my work was excellent, <b class="post__p--bold">but I also feel like I failed.</b> And for someone who takes pride in their work and wants to do well, this was a heavy burden for me.</p><h3 class="post__h3">On being the face of a company</h3><p class="post__p">When you work in DevRel, you are a public company representative. As such, your online presence (because you are expected to be online, and present) is tied inextricably to the perception of a company. If you struggle with any of the above issues mentioned, you must seek solace in the deep, dark shadows of group chats and backchannels. It’s comforting, in a way, that many of my peers also feel this way, but alas it is destroying us slowly, one by one. And whilst you share your anxiety and worries with your peers in the dark, you must continue to remain outwardly positive and sparky and cheerful on The Internet as a company representative whilst you’re crying and screaming and throwing up on the inside because you spend all day, every day, trying to prove your worth to keep your job. </p><p class="post__p">It’s exhausting to have to always play a character. As a result, <b class="post__p--bold">I’ve spent pretty much all of my developer relations career in a constant state of burnout</b>.</p><p class="post__p">Over the years, I also started to feel trapped inside my own “online technical educator” <b class="post__p--italic">whitep4nth3r</b> persona, and for every time I have summoned the courage to write or talk about a controversial topic that affects us all, there have been many more things that I have been unable to say. In fact, one of the reasons I started <a href="https://whitep4nth3r.com/blog/how-to-create-a-newsletter/" target="_blank">my weird newsletter</a> was to give me a way to share the things on my mind that were difficult to say as <b class="post__p--italic">whitep4nth3r</b>, albeit rather cryptically.</p><h2 class="post__h2">AI is killing developer education</h2><p class="post__p">These feelings of instability, anxiety and unease are even more pervasive now in 2026, as the way people learn and explore technology has changed significantly since the mass-adoption of generative AI tools. <a href="https://tante.cc/2026/05/20/on-google-declaring-war-on-the-web/" target="_blank">Google has declared war on the web</a>. People aren’t <b class="post__p--italic">seeking information</b> in the ways we once knew; The Internet and its communities have fragmented. It is now more and more difficult to use search engines to <b class="post__p--italic">actually search</b> for real and accurate information as a result of imposing AI overviews and the swathes of <a href="https://maggieappleton.com/ai-dark-forest" target="_blank">new AI slop articles that are poor regurgitations of stolen content that have been eating themselves for quite some time</a>.</p><p class="post__p">Additionally, developers aren’t gathering together in online spaces to learn and teach and share exciting projects anymore, rather they are mindlessly scrolling for a much-needed hit of short-form content dopamine (because the horrors of the world are more persistent than ever). And, as I mentioned in <a href="https://whitep4nth3r.com/blog/i-am-in-an-abusive-relationship-with-the-technology-industry/" target="_blank">a recent blog post</a>, many developers have replaced authentic human collaboration with conversations with the dreaded Chat Bots. A friend who is a very talented developer educator has come to the same conclusions:</p><blockquote class="post__blockquote"><p class="post__p">The entire vibe has shifted. The majority of [community] folks are still on Twitter but there’s so much AI grifting and misery and hate in that place. LinkedIn is a parody of itself at this point. Bluesky feels reasonably cosy but way too much of a bubble. The community isn’t there. The forums are dead, the new Discord is quiet.</p></blockquote><p class="post__p">Freelance educators are also being impacted by this shift. Josh W. Comeau, author of incredible industry-defining courses like <a href="https://whimsy.joshwcomeau.com/" target="_blank">Whimsical Animations</a>, recently <a href="https://bsky.app/profile/joshwcomeau.com/post/3mkxyqfwwm22t" target="_blank">shared on Bluesky that he has doubts about how sustainable his business model is</a>:</p><blockquote class="post__blockquote"><p class="post__p">I just launched my third course, Whimsical Animations, and so far, it’s on track to sell roughly ⅓ as many copies as a typical course launch. It’s a similar story with my two existing courses. Sales are down significantly from last year.</p><p class="post__p">There are likely a lot of reasons for this, but I think the biggest is AI. There’s sort of a double whammy with AI:</p><p class="post__p">1. Many people are wondering whether developer jobs will even exist in a few months, so they’re reluctant to spend time/money learning new dev skills.</p><p class="post__p">2. Even if they do want to learn new dev skills, LLMs can provide personalized tutoring, so there’s less incentive to buy a paid course.</p></blockquote><p class="post__p">The future feels bleak for educators. Rich curriculums we craft intentionally and carefully for real human audiences are now routinely stolen and regurgitated by predictive algorithms. The Chat Bots, with their sycophantic tone of voice, are deliberately engineered to erase our own personalities, hard work and craft entirely from the equation. If DevRel is to survive, I think it will need to look entirely different from how it functioned during the last ten years.</p><p class="post__p">The only conclusion I can draw from this is that my experience, skills, empathy and teaching expertise no longer hold the value they once did in this volatile industry (if they ever really did?). <b class="post__p--bold">And this only exacerbates the burnout and anxiety that so often consumes me</b>.</p><h2 class="post__h2">What’s next for me</h2><p class="post__p"><b class="post__p--italic">I am leaving developer relations and I am going offline.</b></p><p class="post__p">I wondered for a while whether I should leave tech entirely. But I still really, really enjoy making websites. I’ve found an exciting new role as a Staff Engineer, and I can’t wait to get stuck in. I’ve taken great joy in tearing down my video production studio to create a zen little coding den in which to explore my new role, and I can’t wait to never have to wear makeup ever again to do my job. (I mean, I didn’t really <b class="post__p--italic">have to</b> wear makeup, but I also kind of did. If you know, you know.) To protect my mental health and decouple my online presence from a company brand (at last!), I will not be announcing this new role publicly in the ways this weird industry has become accustomed to. Although, if you’re a close friend, you know where I’m going and that I’m happy and I trust you. I’ve deactivated my LinkedIn profile, and I’m going dark on Bluesky.</p><p class="post__p">This is not a decision I have taken lightly, and it’s probably been in the works in my subconscious for a couple of years now. I’ve taken a lower salary, and outwardly it may look like I’ve gone “backwards” in my career, but I know this is right for me, right now. After all, progress is not always linear, and I see this as the first step towards the life I have been craving whilst sitting and watching nature in my beautiful garden. One day I’d like to follow in <a href="https://openpath.quest/2026/i-am-retiring-from-tech-to-live-offline/" target="_blank">Chad Whitacre’s footsteps</a> (please read his letter), and leave this place for good. But that’s a little while off yet; I have some more things to do here first.</p><p class="post__p">As for what the future holds for <b class="post__p--italic">whitep4nth3r</b>, I’m actually not sure. I know I’m not that important in this vast and strange ever-evolving world; I know this won’t affect many people in the grand scheme of things, but I’m sure some of you will be wondering if I’ll still be around. I’d love to continue sharing what I’ve learned about The Web and technology on my blog, but given the current state of the search engine industry, would anyone even see it? (I have an <a href="https://whitep4nth3r.com/feed.xml" target="_blank">RSS feed</a>, for what it's worth.) I’ll probably continue to send <a href="https://buttondown.com/weirdwidewebhole" target="_blank">my weekly newsletter</a>, but the format may change given I am Logging Off for good and won’t be scouring The Internet for weird links anymore.</p><p class="post__p">After all these years I can finally say out loud: I do not want to be the face of a company. I do not want to speak at conferences or on podcasts or do workshops in order to try and <b class="post__p--italic">sell you a product</b>. I do not want my value determined by arbitrary gamified metrics like how many video views or blog post views or company sign-ups I generate in a landscape that is completely out of my control. I do not want to be perceived. For being perceived is breaking me, and I need to put myself back together.</p><p class="post__p">I just want to exist. I want to be closer to nature and my loved ones. I want to be present in the now. I want to be free. I want to make music. I want to live.</p><p class="post__p">And I just want to build good websites.</p>
  ]]></description>
      <pubDate>Thu, 02 Jul 2026 23:00:00 +0000</pubDate>
      <link>https://whitep4nth3r.com/blog/goodbye-forever-probably/</link>
      <dc:creator>Salma Alam-Naylor&#39;s RSS Feed</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5291005022</guid>
    </item>
    <item>
      <title><![CDATA[Reading List 360]]></title>
      <description><![CDATA[<p>This reading list is courtesy of <a href="https://vivaldi.com">Vivaldi browser</a>, who pay me decent money to <a href="https://vivaldi.com/for-a-better-web/">fight for a better web</a> and don’t moan at me for reading all this stuff. We’ve just released <a href="https://vivaldi.com/download/">Vivaldi 8 for desktop</a>, with our biggest UI overhaul to date, and <a href="https://vivaldi.com/blog/vivaldi-on-mobile-8-0/">Vivaldi 8 for Android and iThings</a>. All are free from Big Tech’s snooping and its AI-pooping. </p>
<ul>
<li><a href="https://www.matuzo.at/blog/2026/grid-lanes-accessibility">Your Grid Lanes will likely fail WCAG 2.4.3</a></li>
<li><a href="https://jakearchibald.com/2026/goldilocks-select-height/">The Goldilocks customizable select height</a> – ” I realised there’s a sizing ‘pattern’ that’s almost always the-one-you-want, but it took me a long time to figure out how to do it in CSS” writes Web Standards hunk Jank Architect</li>
<li><a href="https://www.404media.co/henrico-virginia-datacenter-energy-cost-email/">County With 37 Data Centers Asks Schools to ‘Conserve Electricity’</a> – “Henrico County is a major hub for data centers in Virginia. Its officials said it expects a 25% rise in electricity costs next year, and advised workers to close the blinds and turn off their computers to make up for it.”</li>
<li><a href="https://css-tricks.com/using-css-corner-shape-for-folded-corners/">Using CSS corner-shape For Folded Corners</a></li>
<li><a href="https://github.com/m1k1o/neko">Neko</a> – a browser to run other browsers</li>
<li><a href="https://www.404media.co/apple-hide-my-email-vulnerability-reveals-peoples-real-email-addresses/">Apple ‘Hide My Email’ Vulnerability Reveals Peoples’ Real Email Addresses</a> – Remember, although it’s still unfixed a year after it was reported to Apple, your privacy and security remain Apple’s absolute priority. Honest.</li>
<li><a href="https://noyb.eu/en/us-supreme-court-just-blew-eu-us-data-transfers">US Supreme Court just blew up EU-US Data Transfers</a>– “US Supreme Court decided the US Federal Trade Commission (“FTC”) may not be independent anymore. Since 2000, the EU has relied on the “independent” FTC as the enforcer of EU-US deals on personal data. According to EU treaty law, such oversight must be independent. In the current EU-US deal, the European Commission relies on the independent FTC 259 (!) times”.</li>
<li><a href="https://patrickbrosset.com/articles/2026-06-22-whats-missing-from-svg/">What’s missing from SVG</a> – glamorous Patrick Brosset tells all</li>
<li><a href="https://www.theguardian.com/commentisfree/2026/jun/30/ireland-big-tech-lapdog-eu-presidency-digital-sovereignty">Ireland is big tech’s lapdog – and that compromises its EU presidency</a></li>
<li><a href="https://polypane.app/blog/experimenting-with-random-in-css/">Experimenting with random() in CSS</a> by ‪Kilian Valkhof‬</li>
<li><a href="https://dtinit.org/blog/2026/06/09/global-roundup-june-2026">Data Transfer Initiative’s regular regulatory roundup</a></li>
<li><a href="https://signal.org/blog/pdfs/2026-06-08-uk-surveillance-is-not-safety.pdf">Surveillance Is Not Safety: A statement on the UK’s latest threat to privacy <sup>[PDF]</sup></a> by Signal</li>
<li><a href="https://css-tricks.com/the-siren-song-of-arianotify/">The Siren Song of ariaNotify()</a> – a means of programmatically triggering narration in a screen reader</li>
<li><a href="https://www.oidaisdes.org/blog/aria-notify-browser-support/">How good is Browser Support for the ARIA Notify API?</a><a></a></li>
<li><a href="https://www.gov.uk/government/calls-for-evidence/issues-relating-to-nfc-access-on-ios">NFC access on iOS – CMA call for evidence</a> – “We are continuing to work on the full design of any potential intervention to ensure fair and reasonable NFC access on iOS”</li>
<li><a href="https://www.techpolicy.press/google-and-apples-antidma-lobbying-strategy-goes-allin-on-security-and-privacy/">Google and Apple’s Anti-DMA Lobbying Strategy Goes All-in on Security and Privacy</a> – As the EU starts to press Big Tech to open their operating systems to competitors’ services, you’ll hear a lot more handwringing from them about privacy and security. Most is FUD. Look at their record for privacy and security, especially of their own single-platform (“native”) apps and app stores.</li>
<li><a href="https://fireborn.mataroa.blog/blog/the-slow-death-of-the-power-user/">The Slow Death of the Power User</a> – “This is the result of two decades of deliberate, calculated effort by the largest technology companies on earth to turn users into consumers, instruments into appliances, and technical literacy into a niche hobby for weirdos.”</li>
<li><a href="https://restofworld.org/2023/ai-image-stereotypes/">How AI reduces the world to stereotypes</a> – Rest of World analyzed 3,000 AI images (“an Indian person,” “a house in Mexico,” “a plate of Nigerian food”, etc) to see how image generators visualize different countries and cultures.</li>
<li><a href="https://css-tip.com/wiggly-range-slider/">Wiggly/Wavy Input Range Slider</a> – almost certainly useless; great fun</li>
</ul>
<p><span hidden="" class="__iawmlf-post-loop-links" data-iawmlf-links="[{&quot;id&quot;:22,&quot;href&quot;:&quot;https:\/\/vivaldi.com&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20251011222029\/https:\/\/vivaldi.com\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2025-12-03 17:48:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-07 13:40:29&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-10 13:59:31&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-13 16:28:35&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-17 04:50:58&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-20 06:03:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-23 08:37:55&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-26 09:54:03&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-29 10:21:46&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-01 10:25:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-04 10:37:34&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-07 10:40:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-10 10:53:18&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-13 12:00:06&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-16 12:03:02&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-19 12:03:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-22 12:35:31&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-25 13:37:48&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-28 14:07:35&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-31 15:05:33&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-03 16:14:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-06 16:28:26&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-09 17:16:24&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-12 17:24:56&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-16 08:27:29&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-19 10:06:33&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-22 10:14:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-26 02:02:11&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-01 02:05:15&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-04 02:44:52&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-07 03:00:52&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-10 04:24:51&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-13 21:40:11&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-16 23:02:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-20 04:54:47&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-23 08:12:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-26 08:47:25&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-29 08:58:19&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-01 10:26:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-04 10:58:31&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-07 11:04:27&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-10 12:49:02&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-13 13:10:17&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-16 15:04:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-19 15:08:34&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-22 16:16:09&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-25 16:23:36&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-29 07:47:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-02 08:06:57&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-05 14:30:56&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-08 14:36:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-11 14:42:38&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-14 22:09:09&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-18 02:18:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-21 03:21:36&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-24 08:39:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-27 11:50:07&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-30 14:08:03&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-02 17:45:14&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-05 19:01:22&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-08 19:34:02&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-11 20:27:24&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-15 02:47:32&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-18 04:15:32&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-21 12:24:50&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-24 14:02:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-27 17:39:18&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-01 07:40:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-04 08:05:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-07 08:29:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 08:55:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-13 08:56:09&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-16 23:13:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-20 04:48:48&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-23 04:57:13&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-26 06:00:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-29 07:26:03&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 06:18:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-05 08:30:33&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-08 11:07:04&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-11 15:30:40&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-14 16:28:15&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-17 19:02:30&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-20 21:29:29&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 06:39:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 06:03:15&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:23,&quot;href&quot;:&quot;https:\/\/vivaldi.com\/for-a-better-web&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20251203171645\/https:\/\/vivaldi.com\/for-a-better-web\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2025-12-03 17:48:48&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-07 13:40:30&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-10 15:57:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-13 16:28:35&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-17 04:51:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-20 06:03:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-23 08:37:55&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-26 09:54:04&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2025-12-29 10:21:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-01 10:25:17&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-04 10:37:34&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-07 10:40:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-10 10:53:18&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-13 12:00:06&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-16 12:03:02&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-19 12:03:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-22 12:35:32&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-25 13:37:49&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-28 14:07:37&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-01-31 15:05:39&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-03 16:14:19&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-06 16:28:27&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-09 17:16:24&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-12 17:24:56&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-16 09:11:55&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-19 15:50:58&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-23 02:10:57&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-02-26 02:39:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-01 05:53:17&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-05 06:38:18&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-08 13:31:04&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-12 00:44:33&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-15 02:31:39&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-18 02:35:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-21 07:37:15&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-24 14:28:57&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-28 00:27:05&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-03-31 01:29:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-03 01:32:22&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-04-06 03:33:30&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-09 07:53:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-12 11:13:11&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-15 11:42:40&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-18 12:42:38&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-21 13:36:52&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-24 19:52:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-27 20:26:14&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-01 01:40:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-04 09:33:28&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-07 11:16:17&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-10 12:48:15&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-13 13:29:49&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-16 13:40:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-19 14:18:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-22 16:28:32&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-25 17:53:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-28 20:50:48&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-31 22:00:29&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-03 22:25:17&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-08 06:01:18&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-11 06:27:48&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-14 06:59:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-17 07:21:30&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-20 12:36:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-23 12:48:50&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-26 13:48:14&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-30 02:57:05&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-03 09:04:39&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-06 09:14:02&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-09 09:43:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-12 11:32:23&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-15 15:59:22&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-18 16:17:43&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-22 18:40:25&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-25 21:21:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-29 07:26:03&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 06:17:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-05 08:30:33&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-08 11:07:04&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-11 15:30:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-14 16:28:19&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-17 19:02:30&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-20 21:29:30&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 06:39:09&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 07:15:06&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8877,&quot;href&quot;:&quot;https:\/\/vivaldi.com\/download&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260331221409\/https:\/\/vivaldi.com\/download\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-04-10 15:52:07&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-13 16:13:51&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-17 07:22:36&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-20 19:09:18&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-23 19:35:01&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-26 22:18:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-04-29 22:20:07&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-03 02:12:37&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-06 03:26:22&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-09 03:26:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-12 04:20:06&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-15 06:24:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-18 10:41:46&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-21 11:22:57&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-24 12:49:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-27 21:26:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-05-30 21:48:25&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-02 22:36:47&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-06 04:37:52&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-09 08:47:29&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-12 10:27:55&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-15 12:19:00&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-06-18 15:45:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-21 16:30:34&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-24 20:27:03&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-28 08:40:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-01 08:58:54&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-04 09:38:23&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-07 11:00:22&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 11:04:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-13 14:42:50&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-16 23:13:01&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-19 23:31:37&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-22 23:33:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-25 23:44:49&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-29 07:26:03&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 06:17:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-05 08:30:33&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-08 11:07:04&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-11 15:30:40&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-14 16:28:19&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-17 19:02:30&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-21 07:37:49&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 14:03:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 07:15:06&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8917,&quot;href&quot;:&quot;https:\/\/vivaldi.com\/blog\/vivaldi-on-mobile-8-0&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260529141252\/https:\/\/vivaldi.com\/blog\/vivaldi-on-mobile-8-0\/&quot;,&quot;redirect_href&quot;:&quot;https:\/\/vivaldi.com\/blog\/vivaldi-on-mobile-8-0\/&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-05-30 03:27:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-02 03:33:50&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-05 04:23:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-08 07:50:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-11 08:33:34&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-14 11:38:29&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-17 20:33:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-21 02:30:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-24 08:19:13&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-06-28 00:11:20&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-01 07:40:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-04 08:05:46&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-07 08:29:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 08:55:20&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-13 08:56:11&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-16 23:12:58&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-19 23:31:37&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-22 23:33:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-25 23:44:49&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-29 07:26:04&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 06:18:01&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-05 08:30:33&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-08 11:07:05&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-11 15:30:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-14 16:28:19&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-17 19:02:30&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-21 07:37:53&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 14:03:17&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 07:15:07&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8946,&quot;href&quot;:&quot;https:\/\/www.matuzo.at\/blog\/2026\/grid-lanes-accessibility&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260703130003\/https:\/\/www.matuzo.at\/blog\/2026\/grid-lanes-accessibility&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 14:07:27&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-06 19:44:09&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 02:21:06&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-13 07:52:18&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-16 23:12:57&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-20 04:48:48&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-23 04:57:12&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-26 06:00:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-29 07:26:02&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 06:18:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-05 08:30:37&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-08 11:07:04&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-11 15:30:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-14 16:28:19&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-17 19:02:30&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-21 07:37:52&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 14:03:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 07:15:09&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-31 07:44:01&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-31 07:44:01&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8947,&quot;href&quot;:&quot;https:\/\/jakearchibald.com\/2026\/goldilocks-select-height&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260702122916\/https:\/\/jakearchibald.com\/2026\/goldilocks-select-height\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 12:58:07&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-06 13:20:38&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-09 13:34:56&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-07-12 13:46:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-15 15:59:20&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-18 16:17:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-22 18:40:29&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-25 21:21:00&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-29 07:26:03&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 06:17:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-05 08:30:37&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-08 11:07:04&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-11 15:30:41&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-15 04:33:06&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-18 04:54:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-21 07:37:51&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 14:03:16&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-28 07:15:06&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-31 07:44:01&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-31 07:44:01&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8940,&quot;href&quot;:&quot;https:\/\/www.404media.co\/henrico-virginia-datacenter-energy-cost-email&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260630155706\/https:\/\/www.404media.co\/henrico-virginia-datacenter-energy-cost-email\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-06-30 16:41:29&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-03 17:12:27&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-06 19:44:09&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-10 02:21:05&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-13 07:52:17&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-16 23:13:01&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-20 04:48:47&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-23 04:57:12&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-26 06:00:09&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-29 07:26:02&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-02 06:17:59&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-05 08:30:32&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-08 11:07:04&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-11 15:30:39&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-15 04:33:08&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-18 04:54:45&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-21 07:37:51&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-24 14:03:17&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-28 07:15:06&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:206}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-31 07:44:02&quot;,&quot;http_code&quot;:206},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8948,&quot;href&quot;:&quot;https:\/\/css-tricks.com\/using-css-corner-shape-for-folded-corners&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260519114508\/https:\/\/css-tricks.com\/using-css-corner-shape-for-folded-corners\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 12:58:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-06 13:20:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-09 13:34:54&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-12 13:46:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-15 15:59:20&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-18 16:17:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-22 18:40:24&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-25 21:20:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-29 07:26:02&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 06:18:02&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-05 08:30:32&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-08 11:07:04&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-11 15:30:39&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-15 04:33:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-18 04:54:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-21 07:37:52&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 14:03:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 07:15:07&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-31 07:44:03&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-31 07:44:03&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8949,&quot;href&quot;:&quot;https:\/\/github.com\/m1k1o\/neko&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260703130005\/https:\/\/github.com\/m1k1o\/neko&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 14:07:26&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-06 19:44:11&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-10 02:21:05&quot;,&quot;http_code&quot;:404},{&quot;date&quot;:&quot;2026-07-13 07:52:17&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-16 23:12:59&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-19 23:31:36&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-23 02:15:21&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-26 03:07:01&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-29 07:26:02&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-02 06:17:59&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-05 08:30:33&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-08 11:07:04&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-11 15:30:39&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-15 04:33:10&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-18 04:54:45&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-21 07:37:52&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-24 14:03:15&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 11:52:26&quot;,&quot;http_code&quot;:206}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 11:52:26&quot;,&quot;http_code&quot;:206},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8945,&quot;href&quot;:&quot;https:\/\/www.404media.co\/apple-hide-my-email-vulnerability-reveals-peoples-real-email-addresses&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260701233754\/https:\/\/www.404media.co\/apple-hide-my-email-vulnerability-reveals-peoples-real-email-addresses\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-02 11:28:23&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-05 20:25:47&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-09 06:21:20&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-12 07:57:48&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-15 15:59:20&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-18 16:17:45&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-22 04:36:41&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-25 05:13:28&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-28 06:52:08&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-02 06:17:59&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-05 08:30:32&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-08 11:07:04&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-11 15:30:39&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-15 04:33:06&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-18 04:54:45&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-21 07:37:51&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-24 14:03:16&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-28 11:52:25&quot;,&quot;http_code&quot;:206}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 11:52:25&quot;,&quot;http_code&quot;:206},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8941,&quot;href&quot;:&quot;https:\/\/noyb.eu\/en\/us-supreme-court-just-blew-eu-us-data-transfers&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260630110431\/https:\/\/noyb.eu\/en\/us-supreme-court-just-blew-eu-us-data-transfers&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-06-30 16:41:38&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-03 17:12:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-06 19:44:14&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 02:21:09&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-07-13 07:52:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-16 23:13:50&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-07-19 23:31:36&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-23 03:47:13&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-26 04:15:04&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-07-29 07:26:06&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 06:17:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-05 08:30:37&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-08 11:07:12&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-11 15:30:43&quot;,&quot;http_code&quot;:404},{&quot;date&quot;:&quot;2026-08-15 04:33:14&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-18 04:54:49&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-21 07:37:57&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-24 14:03:20&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8950,&quot;href&quot;:&quot;https:\/\/patrickbrosset.com\/articles\/2026-06-22-whats-missing-from-svg&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260703130007\/https:\/\/patrickbrosset.com\/articles\/2026-06-22-whats-missing-from-svg\/&quot;,&quot;redirect_href&quot;:&quot;https:\/\/patrickbrosset.com:443\/articles\/2026-06-22-whats-missing-from-svg\/&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 14:07:36&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-06 19:44:12&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 02:30:17&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-13 08:20:18&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-16 23:12:58&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-20 04:48:48&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-23 05:15:25&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-26 09:30:25&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-29 11:17:02&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 09:45:05&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-05 14:23:28&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-08 14:56:28&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-11 16:58:28&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-15 04:33:13&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-18 16:18:20&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-21 20:04:32&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-25 06:52:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8942,&quot;href&quot;:&quot;https:\/\/www.theguardian.com\/commentisfree\/2026\/jun\/30\/ireland-big-tech-lapdog-eu-presidency-digital-sovereignty&quot;,&quot;archived_href&quot;:&quot;&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[],&quot;broken&quot;:false,&quot;last_checked&quot;:null,&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8958,&quot;href&quot;:&quot;https:\/\/polypane.app\/blog\/experimenting-with-random-in-css&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260630031328\/https:\/\/polypane.app\/blog\/experimenting-with-random-in-css\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 14:35:14&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-06 19:44:13&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-10 02:30:18&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-13 08:35:04&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-16 23:12:59&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-20 04:48:48&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-23 05:15:25&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-26 21:26:10&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-30 04:45:35&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-02 13:31:24&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-05 14:23:28&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-08 14:56:29&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-11 16:58:28&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-15 04:33:09&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-18 04:54:46&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-21 07:37:54&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-24 14:03:18&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-28 13:10:54&quot;,&quot;http_code&quot;:206}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 13:10:54&quot;,&quot;http_code&quot;:206},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8951,&quot;href&quot;:&quot;https:\/\/dtinit.org\/blog\/2026\/06\/09\/global-roundup-june-2026&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260703131630\/https:\/\/dtinit.org\/blog\/2026\/06\/09\/global-roundup-june-2026&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 14:07:35&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-06 19:44:13&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-10 02:30:23&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-07-13 08:35:04&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-16 23:13:15&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-19 23:31:36&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-23 02:15:21&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-26 03:07:01&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-29 11:17:07&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-02 09:45:01&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-05 14:23:28&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-08 14:56:28&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-11 16:58:28&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-15 04:33:12&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-18 04:54:45&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-21 07:37:53&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-24 14:03:18&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:206}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:206},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8943,&quot;href&quot;:&quot;https:\/\/signal.org\/blog\/pdfs\/2026-06-08-uk-surveillance-is-not-safety.pdf&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260626142921\/https:\/\/signal.org\/blog\/pdfs\/2026-06-08-uk-surveillance-is-not-safety.pdf&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-06-30 16:41:48&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-07-03 17:12:59&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-06 19:44:13&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-10 02:30:26&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-13 08:35:04&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-16 23:13:21&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-19 23:31:38&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-23 05:15:26&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-26 21:26:10&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-30 04:45:38&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-02 22:50:24&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-06 11:58:46&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-09 22:50:47&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-13 07:06:44&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-16 11:02:08&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-19 11:50:20&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-24 14:03:21&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:206}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:206},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8952,&quot;href&quot;:&quot;https:\/\/css-tricks.com\/the-siren-song-of-arianotify&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260703131614\/https:\/\/css-tricks.com\/the-siren-song-of-arianotify\/&quot;,&quot;redirect_href&quot;:&quot;https:\/\/css-tricks.com\/the-siren-song-of-arianotify\/&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 14:07:35&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-06 19:44:15&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 02:30:24&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-13 08:35:05&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-17 00:08:11&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-20 05:40:40&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-23 08:08:53&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-26 21:26:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-30 04:45:44&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-02 22:50:24&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-06 11:58:46&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-09 22:50:40&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-13 07:06:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-16 11:02:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-19 11:50:20&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 06:39:12&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8953,&quot;href&quot;:&quot;https:\/\/www.oidaisdes.org\/blog\/aria-notify-browser-support&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260703131632\/https:\/\/www.oidaisdes.org\/blog\/aria-notify-browser-support\/&quot;,&quot;redirect_href&quot;:&quot;https:\/\/www.oidaisdes.org\/blog\/aria-notify-browser-support\/&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 14:07:37&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-06 19:44:13&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-10 02:30:26&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-07-13 08:35:41&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-16 23:13:24&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-19 23:31:36&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-23 05:15:24&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-26 21:26:10&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-30 04:45:36&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-02 22:50:27&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-06 11:58:46&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-10 01:53:28&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-13 07:06:45&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-16 11:02:08&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-19 11:50:21&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-24 06:39:11&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:206}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:206},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8954,&quot;href&quot;:&quot;https:\/\/www.gov.uk\/government\/calls-for-evidence\/issues-relating-to-nfc-access-on-ios&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260703131644\/https:\/\/www.gov.uk\/government\/calls-for-evidence\/issues-relating-to-nfc-access-on-ios&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 14:07:36&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-06 19:44:13&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 02:30:24&quot;,&quot;http_code&quot;:404},{&quot;date&quot;:&quot;2026-07-13 08:35:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-16 23:13:24&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-19 23:31:36&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-23 05:15:25&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-26 21:26:13&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-30 04:45:36&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-02 22:50:24&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-06 11:58:47&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-09 22:50:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-13 07:06:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-16 11:02:09&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-19 11:50:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 06:39:12&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 13:10:53&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8944,&quot;href&quot;:&quot;https:\/\/www.techpolicy.press\/google-and-apples-antidma-lobbying-strategy-goes-allin-on-security-and-privacy&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260625202255\/https:\/\/www.techpolicy.press\/google-and-apples-antidma-lobbying-strategy-goes-allin-on-security-and-privacy\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-02 09:49:20&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-05 23:50:57&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-09 07:37:40&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-12 07:58:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-15 23:16:05&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-18 23:58:52&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-22 19:47:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-26 21:26:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-30 04:45:39&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-03 01:45:55&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-06 11:58:46&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-09 22:50:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-13 07:06:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-16 11:02:09&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-19 11:50:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 06:39:11&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 13:10:54&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 13:10:54&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8955,&quot;href&quot;:&quot;https:\/\/fireborn.mataroa.blog\/blog\/the-slow-death-of-the-power-user&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260620004936\/https:\/\/fireborn.mataroa.blog\/blog\/the-slow-death-of-the-power-user\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 12:59:21&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-06 19:44:13&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 02:30:32&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-13 08:35:42&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-16 23:13:16&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-20 05:44:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-23 08:08:59&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-26 21:26:12&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-30 04:45:57&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-03 01:45:55&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-06 11:58:47&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-09 22:50:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-13 07:06:45&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-16 11:02:09&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-19 11:50:22&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 06:39:12&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 18:14:29&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 18:14:29&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8956,&quot;href&quot;:&quot;https:\/\/restofworld.org\/2023\/ai-image-stereotypes&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260624194434\/https:\/\/restofworld.org\/2023\/ai-image-stereotypes\/&quot;,&quot;redirect_href&quot;:&quot;&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 12:59:24&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-06 19:44:13&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-10 02:30:35&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-13 08:35:41&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-16 23:13:15&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-19 23:31:39&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-23 05:15:26&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-26 21:26:10&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-07-30 04:45:55&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-03 01:45:54&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-06 11:58:47&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-09 22:50:40&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-13 07:06:44&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-16 11:02:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-19 16:17:08&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-24 06:39:11&quot;,&quot;http_code&quot;:200},{&quot;date&quot;:&quot;2026-08-28 18:14:28&quot;,&quot;http_code&quot;:200}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 18:14:28&quot;,&quot;http_code&quot;:200},&quot;process&quot;:&quot;done&quot;},{&quot;id&quot;:8957,&quot;href&quot;:&quot;https:\/\/css-tip.com\/wiggly-range-slider&quot;,&quot;archived_href&quot;:&quot;http:\/\/web-wp.archive.org\/web\/20260703131639\/https:\/\/css-tip.com\/wiggly-range-slider\/&quot;,&quot;redirect_href&quot;:&quot;https:\/\/css-tip.com\/wiggly-range-slider\/&quot;,&quot;checks&quot;:[{&quot;date&quot;:&quot;2026-07-03 14:07:38&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-06 19:57:57&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-10 02:30:36&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-13 08:35:57&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-16 23:13:19&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-19 23:31:40&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-23 05:15:28&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-26 21:26:12&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-07-30 04:45:58&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-03 01:45:56&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-06 11:58:49&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-09 23:31:58&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-13 07:06:45&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-16 11:02:13&quot;,&quot;http_code&quot;:503},{&quot;date&quot;:&quot;2026-08-19 16:17:10&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-24 06:39:13&quot;,&quot;http_code&quot;:206},{&quot;date&quot;:&quot;2026-08-28 18:14:29&quot;,&quot;http_code&quot;:206}],&quot;broken&quot;:false,&quot;last_checked&quot;:{&quot;date&quot;:&quot;2026-08-28 18:14:29&quot;,&quot;http_code&quot;:206},&quot;process&quot;:&quot;done&quot;}]"></span></p>
]]></description>
      <pubDate>Fri, 03 Jul 2026 12:57:05 +0000</pubDate>
      <link>https://brucelawson.co.uk/2026/reading-list-360/</link>
      <dc:creator>Bruce Lawson&#39;s  personal site</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5291245362</guid>
    </item>
    <item>
      <title><![CDATA[Your Grid Lanes will likely fail WCAG 2.4.3]]></title>
      <description><![CDATA[
                                <p>I saw a great <a href="https://patrickbrosset.com/slides/CSSDay-2026/">introduction to CSS Grid Lanes</a>, aka Masonry Layouts, by Patrick Brosset at CSS Day 2026. I liked the versatility of its use cases, but I was also concerned that it's inaccessible by default.</p><hni-info>
<p>
  <strong>Note:</strong>
  This post contains interactive demos. If you want to see them instead of screenshots, enable the <code>CSS Grid Lanes Layout</code> flag in Edge or Chrome or use Safari 26.4+ or Polypane 29+.
</p>
</hni-info>
<h2 id="intro-to-grid-lanes">Intro to Grid lanes</h2>
<p><a href="https://drafts.csswg.org/css-grid-3/#masonry-layout">CSS Grid Lanes</a> is a new layout technique in CSS. Although the name suggests it, you don’t create grids with it; instead, you create columns or rows that automatically get populated with items. The layout kinda looks like a grid, but the big difference from regular grids is that you can only control one axis. By default, the browser decides where to place each item: it’s placed in whichever column brings it closest to the start of the container.</p>
<p class="code-label">HTML</p>
<pre><code class="language-html">&lt;div class="grid-lanes"&gt;
  &lt;button class="s"&gt;1&lt;/button&gt;
  &lt;button class="m"&gt;2&lt;/button&gt;
  &lt;button class="l"&gt;3&lt;/button&gt;
  &lt;button class="l"&gt;4&lt;/button&gt;
  &lt;button class="m"&gt;5&lt;/button&gt;
  &lt;button class="s"&gt;6&lt;/button&gt;
&lt;/div&gt;</code></pre>
<p class="code-label">CSS</p>
<pre><code class="language-css">.grid-lanes {
  display: grid-lanes;
  grid-template-columns: repeat(3, 1fr);
}

button { block-size: var(--h)}

.s { --h: 2lh; }
.m { --h: 4lh; }
.l { --h: 6lh; }</code></pre>
<style>
.grid-lanes {
  display: grid-lanes;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--oli-g-spacing-s);
  background: var(--oli-g-colors-background);
}
.grid-lanes-focus:has(:focus-visible) {
  display: grid;
}
.grid-lanes-flow {
  flow-tolerance: 0;
}
button { block-size: var(--h)}
.s { --h: 2lh; }
.m { --h: 4lh; }
.l { --h: 6lh; }
.supports-reading-flow,
.supports-grid-lanes { display: none; }
@supports (display: grid-lanes) {
  .supports-grid-lanes { display: block;}
  .supports-not-grid-lanes { display: none; }
}
@supports not (display: grid-lanes) {
.supports-not-grid-lanes { display: block; }
}
@supports (reading-flow: grid-rows) {
  .supports-reading-flow { display: block; }
  .supports-not-reading-flow { display: none; }
}
figure p { margin: 0 !important; }
.focus-btn:focus {outline: 3px solid var(--oli-g-colors-cta)}
</style>
<figure>
<img alt="6 items placed in three columns. order from left to right: item 1, 2, 3, 6, 4, 5" class="supports-not-grid-lanes" height="268" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/4ea1b9219b-1782284499/demo1.avif" width="1000">
<div class="supports-grid-lanes">
<div class="grid-lanes">
  <button class="s">1</button>
  <button class="m">2</button>
  <button class="l">3</button>
  <button class="l">4</button>
  <button class="m">5</button>
  <button class="s">6</button>
</div>
</div>
<figcaption>Demo: Items stacked from left to right, from top to bottom </figcaption>
</figure>
<p>In the demo above, you can see what makes Grid Lanes special: you can arrange differently sized items in a grid-like fashion with equal gaps.<br>
You may have noticed that the columns in this demo are equal in height. That's because I arranged them that way, but one of the strengths of Grid Lanes is that you don't have to care about that. The browser looks at the items you give it and tries to create balanced columns (or rows). It does so by placing items as close as possible to the grid's start edge.</p>
<p>In the following demo, items 1-3 are each placed in a separate column because this brings them as close as possible to the start of the container. Item 4 is in the second column because that's the shortest column at the time of placement. The next two columns are equal in height. If there's a tie, the browser continues from where it left off (column 2 in our example) and places the item in the next available fitting column (column 3). Finally, the last item is placed in the shortest column, column 1.</p>
<p class="code-label">HTML</p>
<pre><code class="language-html">&lt;div class="grid-lanes"&gt;
  &lt;button class="l"&gt;1&lt;/button&gt;
  &lt;button class="m"&gt;2&lt;/button&gt;
  &lt;button class="l"&gt;3&lt;/button&gt;
  &lt;button class="m"&gt;4&lt;/button&gt;
  &lt;button class="m"&gt;5&lt;/button&gt;
  &lt;button class="s"&gt;6&lt;/button&gt;
&lt;/div&gt;</code></pre>
<figure>
<img alt="&quot;6 items placed in three columns. order from left to right: item 1, 2, 3, 6, 4, 5&quot;" class="supports-not-grid-lanes" height="331" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/736514ee1a-1782284494/demo2.avif" width="1000">
<div class="supports-grid-lanes">
<div class="grid-lanes">
  <button class="l">1</button>
  <button class="m">2</button>
  <button class="l">3</button>
  <button class="m">4</button>
  <button class="m">5</button>
  <button class="s">6</button>
</div>
</div>
<figcaption>Demo: The lengths of the columns are as equal as possible with the downside that items are out of order</figcaption>
</figure>
<p>That's pretty cool, but this strength is also Grid Lane's biggest weakness. As you can see, the visual order no longer matches the tab order.</p>
<figure>
<img alt="Screenshot of the previous demo with numbers and lines indicating the tabbing order" height="341" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/edae3a4405-1782284498/order1.avif" width="1000">
<figcaption>Tabbing order is correct for the first 3 items, but then it goes from column 2 to 3 to 1.</figcaption>
</figure>
<p>That can be a problem, especially when the container spans a larger area on screen. <a href="https://www.w3.org/WAI/WCAG21/Techniques/css/C27">WCAG Technique C27</a> lists several reasons why:</p>
<ul>
<li>A user with low vision who uses a screen magnifier in combination with a screen reader may be confused when the reading order appears to skip around on the screen.</li>
<li>A keyboard user may have trouble predicting where focus will go next when the source order does not match the visual order.</li>
<li>If a blind user, who reads the page with a screen reader that follows the source order, is working with a sighted user who reads the page in visual order, they may be confused when they encounter information in different orders.</li>
</ul>
<p>That’s definitely a problem, but not necessarily a deal breaker. The <code>flow-tolerance</code> property may help improve the situation.</p>
<h2 id="placement-tolerance">Placement tolerance</h2>
<p>The <a href="https://drafts.csswg.org/css-grid-3/#placement-tolerance">flow-tolerance</a> property allows us to adjust the placement precision. As mentioned, the algorithm places items as close as possible to the start edge. If one column is smaller than the other, the next item will be placed in the smaller column. If a column is only slightly larger, that behavior may result in an order mismatch and in an ugly layout. You'll see what I mean in the next demo.</p>
<p>We have 4 items. One is much larger than the others, and one is much smaller. The two others are almost equal in height; there's only a 1px difference.</p>
<p class="code-label">HTML</p>
<pre><code class="language-html">&lt;div class="grid-lanes"&gt;
  &lt;button style="--h: 50px"&gt;1&lt;/button&gt;
  &lt;button style="--h: 60px"&gt;2&lt;/button&gt;
  &lt;button style="--h: 49px""&gt;3&lt;/button&gt;
  &lt;button style="--h: 20px"&gt;4&lt;/button&gt;
&lt;/div&gt;</code></pre>
<p class="code-label">CSS</p>
<pre><code class="language-css">.grid-lanes {
  display: grid-lanes;
  grid-template-columns: repeat(3, 1fr);
  flow-tolerance: 0;
}

button { block-size: var(--h)}</code></pre>
<figure>
<img alt="&quot;4 items placed in 3 columns. item 4 is in the third column.&quot;" class="supports-not-grid-lanes" height="331" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/983d77fdf4-1782284498/demo3.avif" width="1000">
<div class="supports-grid-lanes">
  <div class="grid-lanes grid-lanes-flow">
    <button style="--h: 50px">1</button>
    <button style="--h: 60px">2</button>
    <button style="--h: 49px" "="">3</button>
    <button style="--h: 30px">4</button>
  </div>
</div>
<figcaption>The logical order would be correct and it would look nicer if item 4 was in the first column.</figcaption>
</figure>
<p>Item 4 ended up in the third column because item 3 is 1px smaller than item 1. That's only happening because I set <code>flow-tolerance: 0</code>. The default value is <code>1em</code>. If I increase the tolerance or use the initial value, you can see how item 4 ends up in the first column. That's because the property specifies what the threshold is for considering tracks to be “the same height”. If you set the value to 10px, a smaller column has to be at least 10px smaller to be considered smaller.</p>
<figure>
<img alt="4 items placed in 3 columns. item 4 is in the first column.&quot;" class="supports-not-grid-lanes" height="331" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/d826873dd6-1782284494/demo4.avif" width="1000">
<div class="supports-grid-lanes">
  <div class="grid-lanes grid-lanes-flow" style="flow-tolerance: 1px">
    <button style="--h: 50px">1</button>
    <button style="--h: 60px">2</button>
    <button style="--h: 49px" "="">3</button>
    <button style="--h: 30px">4</button>
  </div>
</div>
<figcaption>With <code>flow-tolerance: 1px</code> we get a different layout.</figcaption>
</figure>
<p>When the logical and visual orders are out of sync, you can increase the Flow Tolerance to mitigate the mismatch. That sounds like a solution to the problem, and it is for some (most?) layouts. The thing is, that the tolerance value sometimes needs to be very high to make a difference.</p>
<h2 id="demos">Demos</h2>
<p>The WebKit team created <a href="https://gridlanes.webkit.org/pinboard/">The Field Guide to Grid Lanes</a>, a demo and playground for CSS Grid Lanes. What they build is cool and useful, but all their demos would fail WCAG 2.4.3 by default if they contained focusable elements, which in a realistic example they most likely would.</p>
<p>Their first demo is a classic image gallery with images of varying heights. The numbers you see on each image indicate its position in the DOM within the container. The order of the first 5 items is fine, and we also know why, but starting with item 6, things get ugly. There is a critical mismatch between the DOM and the visual order.</p>
<figure>
<img alt="&quot;20+ images in 5 columns. order from left to right: item 1,2,3,4,5,10,8,6,7,11,15,12,9,14,16,19,17,13,21…&quot;" height="785" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/84a3bc2e55-1782284495/fieldguide1.avif" width="1000">
<figcaption>Strong mismatch between the tab and the visual order</figcaption>
</figure>
<p>The Field Guide comes with controls for each demo. You can increase the Flow Tolerance with a range slider that goes up to 10rem. With a Flow Tolerance of 10rem, it's slightly better but still not acceptable.</p>
<figure>
<img alt="&quot;20+ images in 5 columns. order from left to right: item 1,2,3,4,5,10,6,7,8,9,14,11,15,12,13,21,17,18,16,…&quot;" height="785" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/59b243e631-1782284499/fieldguide2.avif" width="1000">
<figcaption>Slightly better, but there is still a mismatch between the tab and the visual order</figcaption>
</figure>
<p>I had to increase the Flow Tolerance to 40rem (640px) to remove the mismatch entirely. Unfortunately, that surfaced another problem. The lengths of the columns are now imbalanced, and because the heights of the images are so different, focus still jumps around a lot, but now vertically. For example, you can see how the focus has to travel down from item 11 to 12, then up to 13, and down to 14. That's not great.</p>
<figure>
<img alt="&quot;Same demo, but order is correct now. Still, tab order doesn't go from top to bottom, left to right in straight lines but more in a zig zag pattern.&quot;" height="1073" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/3311bee0c2-1782284499/fieldguide3.avif" width="1000">
<figcaption>No mismatch, but column lenghts are now imbalanced.</figcaption>
</figure>
<p>To be fair, although all their demos fail WCAG by default, increasing Flow Tolerance sometimes fixes the problem. In the following screenshot (click to enlarge), you can see an order mismatch across all screen sizes.</p>
<figure>
<p><a href="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/86431a69e5-1782284498/fieldguide4_large.avif"><img alt="&quot;Screenshot of Polypane showing a page at three screen sizes simultaneously. On the page are lists with links laid out in Grid Lanes. Polypane shows numbers connected by lines indicating the tabbing order. Whenever there is a mismatch between the visual and the tabbing order, Polypane highlights the number.&quot;" height="553" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/00e8839c6e-1782284500/fieldguide4.avif" width="1000"></a></p>
<figcaption>Polypane shows a mismatch at different screen sizes</figcaption>
</figure>
<p>Setting the Flow Tolerance to <code>10rem</code>, gets rid of most issues.</p>
<figure>
<p><a href="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/f12fa10ab6-1782284500/fieldguide5_large.avif"><img alt="&quot;Same demo as before but with corect tabbing order.&quot;" height="553" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/db5de3044b-1782284499/fieldguide5.avif" width="1000"></a></p>
<figcaption>Polypane shows that the visual oder matches the DOM order at different screen sizes</figcaption>
</figure>
<p><strong>Update</strong>: </p>
<ol>
<li>As Patrick pointed out on Bluesky, you can also set <code>flow-tolerance</code> to <code>infinite</code>, which makes items distribute themselves strictly in order, without considering the length of the tracks at all.</li>
<li>Clarified that the Field Layout demos don't fail 2.4.3, but would if they contained focusable elements.</li>
</ol>
<h2 id="reading-flow">Reading flow</h2>
<p>Of course, I'm not the first to address this issue. There has been a lot of discussion, even dating back to when CSS Grid Layout was proposed. If you're interested, check out Adrian Roselli's post <a href="https://adrianroselli.com/2015/09/source-order-matters.html">Source Order Matters</a> for a chronological overview or read the following issues on GitHub:</p>
<ul>
<li><a href="https://github.com/w3c/csswg-drafts/issues/5675">Masonry layout in CSS Grid 3 has potential to cause accessibility problems with reading order</a>. </li>
<li><a href="https://github.com/w3c/csswg-drafts/issues/7387">Providing authors with a method of opting into following the visual order, rather than logical order</a>. </li>
</ul>
<p>There is awareness of this problem, and people have been discussing it a lot; the outcome was two new properties in CSS: <a href="https://drafts.csswg.org/css-display-4/#reading-flow">reading-flow</a> and <a href="https://drafts.csswg.org/css-display-4/#propdef-reading-order">reading-order</a>. I won't go into detail now, you can read about them on the <a href="https://developer.chrome.com/blog/reading-flow">Chrome blog</a> or <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/reading-flow">MDN</a>, but essentially, the reading-flow property can be used to ensure the reading and tabbing order reflects the visual order when the visual and DOM order don't match.</p>
<div class="supports-not-reading-flow section-content">
<hni-info>
<p>
  <strong>Note:</strong>
  Switch to a Chromium-based browser like Edge or Chrome to see an interactive demo.
</p>
</hni-info>
</div>
<div class="supports-reading-flow section-content">
<p>Let's say we have a grid like in the following example, where there is a clear mismatch between the visual order and the DOM order. When you press <kbd>Tab</kbd>, focus moves to the first, second, third, and finally the fourth item, following the logical order but not the visual order.</p>
<style>
.reading-flow {  background: var(--oli-g-colors-background); display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--oli-g-spacing-xs)}
.reading-flow-fixed { reading-flow: grid-rows; }
</style>
<button class="supports-grid-slanes focus-btn" style="--oli-s-text-exponent: 0"><span>Click here and then press <kbd>Tab</kbd></span></button>
<figure>
<div class="reading-flow">
  <button style="grid-column: 2 / -1">1</button>
  <button>2</button>
  <button style="grid-column: 1; grid-row: 1;">3</button>
  <button style="grid-column: 1 / span 2; grid-row: 2;">4</button>
</div>
<figcaption>Demo: A mix of 4 implictly and explicity placed items in two columns and two rows </figcaption>
</figure>
<p class="code-label">CSS</p>
<pre><code class="language-css">.reading-flow {
  display: grid; 
  grid-template-columns: repeat(3, 1fr); 
  gap: 10px;
}</code></pre>
<p class="code-label">HTML</p>
<pre><code class="language-html">&lt;div class="reading-flow"&gt;
  &lt;button style="grid-column: 2 / -1"&gt;1&lt;/button&gt;
  &lt;button&gt;2&lt;/button&gt;
  &lt;button style="grid-column: 1; grid-row: 1;"&gt;3&lt;/button&gt;
  &lt;button style="grid-column: 1 / span 2; grid-row: 2;"&gt;4&lt;/button&gt;
&lt;/div&gt;</code></pre>
<p>To fix that issue, you can set <code>reading-flow</code> to <code>grid-rows</code>. Now the reading order and focus order follow the visual order.</p>
<p class="code-label">CSS</p>
<pre><code class="language-css">.reading-flow {
  display: grid; 
  grid-template-columns: repeat(3, 1fr); 
  reading-flow: grid-rows;
}</code></pre>
<button class="supports-grid-slanes focus-btn" style="--oli-s-text-exponent: 0"><span>Click here and then press <kbd>Tab</kbd></span></button>
<figure>
<div class="reading-flow reading-flow-fixed">
  <button style="grid-column: 2 / -1">1</button>
  <button>2</button>
  <button style="grid-column: 1; grid-row: 1;">3</button>
  <button style="grid-column: 1 / span 2; grid-row: 2;">4</button>
</div>
<figcaption>Demo: A mix of 4 implictly and explicity placed items in two columns and two rows </figcaption>
</figure>
</div>
<p>Reading flow could address many of the issues (not all, because visual jumps could still be present) I described in this blog post, but it's still theoretical: it's only implemented in Chromium-based browsers and doesn't yet work with Grid Lanes. Also, the positions of WebKit and Mozilla aren't clear.  Safari still has some concerns and open issues, and Firefox hasn't published its position yet.</p>
<ul>
<li><a href="https://github.com/mozilla/standards-positions/issues/1056">CSS reading-flow standards position Mozilla</a></li>
<li><a href="https://github.com/WebKit/standards-positions/issues/378">CSS reading-flow standards position WebKit</a> </li>
</ul>
<p>I haven't been following the discussions around Reading Flow, and I haven't played with it yet. Not having spent too much time thinking about it, I'm a bit torn. On the one hand, Reading Flow seems to work and fix a real, common issue. On the other hand, it may lead developers to care even less about source order. Anyways, this post isn't the right place to discuss that, but I feel like it could be a good idea to set an implicit default value for Grid Lanes that follows the visual order, since Grid Lanes inherently break the tabbing order. That may not be a good idea, and it may not be possible, because there are different ways to lay out Grid Lanes, but as this blog post has shown, the default state is unacceptable in many cases. If you have ideas on how to address this issue, please add your opinion to one of the issues I linked, create your own issues or bug reports, or talk to browser people on social media.</p>
<h2 id="the-title-of-this-post">The title of this post</h2>
<p>Consequently, today, that means that if the heights of your grid items don't vary too much and if you increase Flow Tolerance, which you probably always have to do, you can make Grid Lanes more or less accessible. In the future, when all browsers support reading-flow and implement it for Grid Lanes, it may get even easier and you may not have to rely on flow-tolerance too much.</p>
<p>Now you may ask yourself, why is the title of this post still so negative? Why didn't I go for something like “Improving the accessibility of Grid Lanes“?</p>
<ol style="line-height: var(--oli-s-page-line-height-m)">
<li>
<p>It's too easy to create inaccessible Grid Lanes if you aren't aware of the implications. That's a big difference compared to regular grids because, to mess with them, you have to change the order or explicitly place items, which, by the way, is also possible with Grid Lanes and can potentially make the situation even worse.</p>
</li>
<li>
<p>I watched <a href="https://youtu.be/X6azWrtHS-k?t=1006">a talk by Rachel Andrew</a> in which they explain that they didn't want Grid Lanes to ship unless Reading Flow existed for the reasons described in this post, but that's exactly what happened in Safari. It supports Grid Lanes, but not Reading Flow. Chrome and Edge support <code>reading-flow</code> and Grid Lanes behind a flag, but as of today, Reading Flow is scoped to regular grids and flex containers.</p>
</li>
<li>
<p>Since Reading Flow for Grid Lanes is only an idea, and Safari made Grid Lanes very real by shipping it, we're left with Flow Tolerance for improving accessibility. Based on my limited research and experience, the default value for <code>flow-tolerance</code> seems too low. It should be high by default to reduce the likelihood of a mismatch between the logical and visual order. </p>
</li>
<li>
<p>Flow Tolerance is not a silver bullet, and DOM order is not the only thing that matters. Large differences in height between grid items can also affect keyboard accessibility.</p>
</li>
<li>
<p>The <a href="https://drafts.csswg.org/css-grid-3/#order-accessibility">specification</a> has a section about accessibility, but it's not detailed enough.</p>
</li>
<li>
<p>The WebKit field guide has nice demonstrations, but doesn't address the obvious accessibility issues or reading-flow as a possible solution. They only mention that spanning and explicit placement can be problematic. I find that questionable. The fact that their website isn't WCAG-compliant doesn't make it better.</p>
</li>
</ol>
<h2 id="bonus-another-solution">Bonus: Another solution</h2>
<p>I'm not sure I really want to recommend this solution, or even if it's a solution, but I was trying to figure out whether there is a way to improve the situation for keyboard users. And I tried the following, which surprisingly works.</p>
<p class="code-label">CSS</p>
<pre><code class="language-css">.grid-lanes:has(:focus-visible) {
  display: grid;
}</code></pre>
<button class="supports-grid-lanes focus-btn"><span>Click here and then press <kbd>Tab</kbd></span></button>
<figure>
<img alt="&quot;6 items placed in three columns. same es the second demo in this post but now order is fine because it a regular grid, showing large gaps, but at least it's accessible&quot;" class="supports-not-grid-lanes" height="331" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/grid-lanes-accessibility/2e601406d3-1782284494/demo5.avif" width="1000">
<div class="supports-grid-lanes">
<div class="grid-lanes grid-lanes-focus">
  <button class="l">1</button>
  <button class="m">2</button>
  <button class="l">3</button>
  <button class="m">4</button>
  <button class="m">5</button>
  <button class="s">6</button>
</div>
</div>
<figcaption>Demo: By default you get Grid Lanes, but if there's focus within the grid type switches to a regular grid </figcaption>
</figure>
<p>You can have your Grid Lanes in any order you want, because as soon as there is focus within, the display switches to <code>grid</code> for keyboard users. Of course, that doesn't fix the problem for sighted screen reader users who use the virtual cursor, and it can also be confusing if the layout suddenly changes on focus. I'm mentioning it because I find the idea interesting, and it can be a solution for some layouts.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I was never really excited about Grid Lanes to begin with, but that was just my personal opinion. After watching Patrick's presentation, I can see how there are more use cases than the classic Pinterest layout, but still now I'm even less excited due to the potentially poor baseline accessibility. And I'm afraid most people will use them out of the box without adjusting the Flow Tolerance or testing them with the keyboard or screen readers.</p>
<p>To sum it up, my recommendations are:</p>
<ul>
<li>Always test your Grid Lanes with the keyboard or a screen reader at different screen sizes. </li>
<li>If you find a mismatch between the DOM and the visual order, increase the <code>flow-tolerance</code> value. </li>
<li>As soon as it's available, try to use <code>reading-flow</code> instead.</li>
<li>If that doesn't work, consider using a regular grid.</li>
</ul><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CYour+Grid+Lanes+will+likely+fail+WCAG+2.4.3%E2%80%9D">blog@matuzo.at</a>.</p>        ]]></description>
      <pubDate>Wed, 24 Jun 2026 07:17:31 +0000</pubDate>
      <link>https://www.matuzo.at/blog/2026/grid-lanes-accessibility</link>
      <dc:creator>Manuel Matuzović - Blog</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5280715783</guid>
    </item>
    <item>
      <title><![CDATA[Introducing the Field Guide to Grid Lanes]]></title>
      <description><![CDATA[<p>This week, we launched the Field Guide to Grid Lanes at <a href="https://gridlanes.webkit.org/">gridlanes.webkit.org</a>.</p>
<figure class=""><picture><source srcset="https://webkit.org/wp-content/uploads/field-guide-top-dark-scaled.png" type="image/png" media="(prefers-color-scheme: dark)"><img decoding="async" src="https://webkit.org/wp-content/uploads/field-guide-top-light-scaled.png" alt="Field Guide to Grid Lanes website header" width="2560" height="1452" class="aligncenter size-full wp-image-18103" srcset="https://webkit.org/wp-content/uploads/field-guide-top-light-scaled.png 2560w, https://webkit.org/wp-content/uploads/field-guide-top-light-300x170.png 300w, https://webkit.org/wp-content/uploads/field-guide-top-light-1024x581.png 1024w, https://webkit.org/wp-content/uploads/field-guide-top-light-768x436.png 768w, https://webkit.org/wp-content/uploads/field-guide-top-light-1536x871.png 1536w, https://webkit.org/wp-content/uploads/field-guide-top-light-2048x1161.png 2048w" sizes="(max-width: 2560px) 100vw, 2560px"></picture></figure>
<p>If you ever bookmarked the CSS Tricks Complete Guide to Flexbox, HTML5 Rocks, or CSS Zen Garden, a guide like this might feel familiar. It’s designed to be an easy introduction, a reference guide — and just plain fun.</p>
<h2>The interactive playground</h2>
<p>At the top is a live, editable Grid Lanes layout. Switch between Waterfall and Brick. Try preset layouts. Drag the slider labeled “Flow tolerance” and click “Play tab order” to understand the impact of <code>flow-tolerance</code>.</p>
<figure class=""><picture><source srcset="https://webkit.org/wp-content/uploads/field-guide-playground-dark-scaled.png" type="image/png" media="(prefers-color-scheme: dark)"><img loading="lazy" decoding="async" src="https://webkit.org/wp-content/uploads/field-guide-playground-light-scaled.png" alt="Interactive drawing of website with items laid out with Grid Lanes. Multiple buttons, sliders, and a code editor make it possible to try out many combinations quickly &amp; see the results. " width="2560" height="2099" class="aligncenter size-full wp-image-18108" srcset="https://webkit.org/wp-content/uploads/field-guide-playground-light-scaled.png 2560w, https://webkit.org/wp-content/uploads/field-guide-playground-light-300x246.png 300w, https://webkit.org/wp-content/uploads/field-guide-playground-light-1024x840.png 1024w, https://webkit.org/wp-content/uploads/field-guide-playground-light-768x630.png 768w, https://webkit.org/wp-content/uploads/field-guide-playground-light-1536x1260.png 1536w, https://webkit.org/wp-content/uploads/field-guide-playground-light-2048x1679.png 2048w" sizes="auto, (max-width: 2560px) 100vw, 2560px"></picture></figure>
<p>Resize the demo browser window to test responsive behavior without resizing your whole window. Edit the CSS directly. Copy the code you create.</p>
<h2>The cheat sheet</h2>
<p>Next is the Field Guide itself — a single-page reference for every property, value, and option.</p>
<figure class=""><picture><source srcset="https://webkit.org/wp-content/uploads/field-guide-ref-dark-scaled.png" type="image/png" media="(prefers-color-scheme: dark)"><img loading="lazy" decoding="async" src="https://webkit.org/wp-content/uploads/field-guide-ref-light-scaled.png" alt="Screenshot of first part of the reference guide, showing lots of definitions, little diagrams, and tiny code snippets " width="2560" height="2282" class="aligncenter size-full wp-image-18110" srcset="https://webkit.org/wp-content/uploads/field-guide-ref-light-scaled.png 2560w, https://webkit.org/wp-content/uploads/field-guide-ref-light-300x267.png 300w, https://webkit.org/wp-content/uploads/field-guide-ref-light-1024x913.png 1024w, https://webkit.org/wp-content/uploads/field-guide-ref-light-768x685.png 768w, https://webkit.org/wp-content/uploads/field-guide-ref-light-1536x1369.png 1536w, https://webkit.org/wp-content/uploads/field-guide-ref-light-2048x1826.png 2048w" sizes="auto, (max-width: 2560px) 100vw, 2560px"></picture></figure>
<p>It has four sections:</p>
<ul>
<li><strong>Grid Lanes Basics</strong> — <code>display: grid-lanes</code>, plus the difference between waterfall and brick layouts</li>
<li><strong>Options for Lane Definitions</strong> — Grid track-sizing with <code>fr</code> units, fixed lengths, percentages, <code>auto</code>, <code>min-content</code> &amp; <code>max-content</code>, <code>fit-content()</code>, <code>minmax()</code>, <code>repeat()</code>, and <code>auto-fill</code> vs <code>auto-fit</code></li>
<li><strong>Options for Placement &amp; Spacing</strong> — <code>flow-tolerance</code>, <code>gap</code>, spanning tracks, and explicit placement</li>
<li><strong>Good to Know</strong> — info about source order, progressive enhancement, and switching layouts at different breakpoints</li>
</ul>
<h2>The demos</h2>
<p>To showcase the possibilities of Grid Lanes, we created six demos, each available in several variations:</p>
<figure class=""><picture><source srcset="https://webkit.org/wp-content/uploads/field-guide-demos-dark-scaled.png" type="image/png" media="(prefers-color-scheme: dark)"><img loading="lazy" decoding="async" src="https://webkit.org/wp-content/uploads/field-guide-demos-light-scaled.png" alt="The six demos listed on the homepage —&nbsp;with a screenshot of each." width="2560" height="1714" class="aligncenter size-full wp-image-18112" srcset="https://webkit.org/wp-content/uploads/field-guide-demos-light-scaled.png 2560w, https://webkit.org/wp-content/uploads/field-guide-demos-light-300x201.png 300w, https://webkit.org/wp-content/uploads/field-guide-demos-light-1024x686.png 1024w, https://webkit.org/wp-content/uploads/field-guide-demos-light-768x514.png 768w, https://webkit.org/wp-content/uploads/field-guide-demos-light-1536x1028.png 1536w, https://webkit.org/wp-content/uploads/field-guide-demos-light-2048x1371.png 2048w" sizes="auto, (max-width: 2560px) 100vw, 2560px"></picture></figure>
<ul>
<li><strong>Photos</strong> — just images, in a variety of aspect ratios</li>
<li><strong>Recipes</strong> — components containing both flexible images and varying lengths of text</li>
<li><strong>Newspaper</strong> — longer passages of text, with a few images (and a lot of CSS puns)</li>
<li><strong>Mega Menu</strong> — lists of very short text</li>
<li><strong>Timeline</strong> — text in brick layout</li>
<li><strong>Pinboard</strong> — mixed media</li>
</ul>
<p>Each demo opens with a floating control panel.</p>
<figure class=""><picture><source srcset="https://webkit.org/wp-content/uploads/field-guide-controls-dark-scaled.png" type="image/png" media="(prefers-color-scheme: dark)"><img loading="lazy" decoding="async" src="https://webkit.org/wp-content/uploads/field-guide-controls-light-scaled.png" alt="Screenshot of one of the demos, happens to be of photo layout. The controls are showing, as described in the article. " width="2560" height="1966" class="aligncenter size-full wp-image-18114" srcset="https://webkit.org/wp-content/uploads/field-guide-controls-light-scaled.png 2560w, https://webkit.org/wp-content/uploads/field-guide-controls-light-300x230.png 300w, https://webkit.org/wp-content/uploads/field-guide-controls-light-1024x786.png 1024w, https://webkit.org/wp-content/uploads/field-guide-controls-light-768x590.png 768w, https://webkit.org/wp-content/uploads/field-guide-controls-light-1536x1180.png 1536w, https://webkit.org/wp-content/uploads/field-guide-controls-light-2048x1573.png 2048w" sizes="auto, (max-width: 2560px) 100vw, 2560px"></picture></figure>
<p>“Layout” offers a dropdown of variations — showing off what Grid Lanes can do, and comparing it to Flexbox, Multicolumn, and Grid. “Numbers” shows item order. “Flow tolerance” lets you experiment with its effects. The code panel displays the key layout CSS.</p>
<p>“Hide controls” puts the focus on the demo itself. To get the controls back, click the gear that appears in the lower-right corner.</p>
<h2>Working with Safari’s developer tools</h2>
<p>Web Inspector knows about Grid Lanes, too. Toggle “Order Numbers” to reveal overlays marking the DOM order of items. These numbers are extremely useful when experimenting to find the best <code>flow-tolerance</code> value for your content.</p>
<figure class=""><picture><source srcset="https://webkit.org/wp-content/uploads/field-guide-inspector-dark-scaled.png" type="image/png" media="(prefers-color-scheme: dark)"><img loading="lazy" decoding="async" src="https://webkit.org/wp-content/uploads/field-guide-inspector-light-scaled.png" alt="Screenshot of photo demo, with Web Inspector open. The Grid Inspector is showing, with Item numbers are turned on. This creates lines all over the webpage, revealing the Grid Lanes layout, and marking each Item with a number. " width="2560" height="2099" class="aligncenter size-full wp-image-18116" srcset="https://webkit.org/wp-content/uploads/field-guide-inspector-light-scaled.png 2560w, https://webkit.org/wp-content/uploads/field-guide-inspector-light-300x246.png 300w, https://webkit.org/wp-content/uploads/field-guide-inspector-light-1024x840.png 1024w, https://webkit.org/wp-content/uploads/field-guide-inspector-light-768x630.png 768w, https://webkit.org/wp-content/uploads/field-guide-inspector-light-1536x1260.png 1536w, https://webkit.org/wp-content/uploads/field-guide-inspector-light-2048x1679.png 2048w" sizes="auto, (max-width: 2560px) 100vw, 2560px"></picture></figure>
<p>Learn more by reading <a href="https://webkit.org/blog/17746/new-safari-developer-tools-provide-insight-into-css-grid-lanes/">New Safari developer tools provide insight into CSS Grid Lanes</a>.</p>
<h2>What about other browsers?</h2>
<p>Grid Lanes works today in Safari 26.4+. For the latest information about other browsers, check <a href="https://caniuse.com/css-grid-lanes">Can I Use</a>. For progressive enhancement guidance, read <a href="https://webkit.org/blog/17758/when-will-css-grid-lanes-arrive-how-long-until-we-can-use-it/">When will CSS Grid Lanes arrive? How long until we can use it?</a></p>
<h2>Made by the people who shipped it</h2>
<p>The Field Guide was built by the same team behind Grid Lanes. We hope this is a fun experience that makes Grid Lanes easy to learn. Bookmark it, share it with colleagues, and let us know what you make.</p>
<p><a href="https://gridlanes.webkit.org/"><strong>Visit the Field Guide →</strong></a></p>
<h2>Feedback</h2>
<p>We’d love to hear from you. Find us online: Jen Simmons on <a href="https://bsky.app/profile/jensimmons.bsky.social">Bluesky</a> / <a href="https://front-end.social/@jensimmons">Mastodon</a>, Saron Yitbarek on <a href="https://bsky.app/profile/saron.bsky.social">Bluesky</a> / <a href="https://front-end.social/@saron">Mastodon</a>, and Jon Davis on <a href="https://bsky.app/profile/jondavis.bsky.social">Bluesky</a> / <a href="https://mastodon.social/@jondavis">Mastodon</a>. You can follow WebKit <a href="https://www.linkedin.com/in/apple-webkit/">on LinkedIn</a>.</p>
]]></description>
      <pubDate>Wed, 10 Jun 2026 07:30:35 +0000</pubDate>
      <link>https://webkit.org/blog/18098/introducing-the-field-guide-to-grid-lanes/</link>
      <dc:creator>WebKit</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5254426361</guid>
    </item>
    <item>
      <title><![CDATA[A future fable: how American tech workers went union]]></title>
      <description><![CDATA[<aside class="ednote">I don’t work in the tech industry myself but am closely connected with people who do. This is based on my observations and conversations with tech workers in the Seattle area.</aside>
<p>Ok so Ethan Marcotte <a href="https://ethanmarcotte.com/wrote/all-tomorrows-parties/">wrote this better</a>&nbsp;(to be fair he literally wrote <a href="https://ethanmarcotte.com/books/you-deserve-a-tech-union/">the book on tech unions</a>)… my version is a little more documentary.</p>
<h2>1. The American tech labor market softens</h2>
<h3>The workforce shrinks and developer jobs intensify</h3>
<p>The industry <a href="https://tech.yahoo.com/general/article/tech-layoffs-2026-more-than-150000-jobs-cut-at-meta-linkedin-salesforce-intuit-and-more-144545224.html">hemorrhages workers</a> to free up funds to invest in data centers. Workers try to organize, but are <a href="https://medium.com/@jakeorlowitz/wikipedia-is-doing-the-capitalist-thing-56a393232943">drummed out</a>. Massive layoffs at every large tech company leave thousands of formerly well-paid developers unemployed for months on end.</p>
<p><span id="more-38123"></span></p>
<p><strong>Senior and Principal developers are <a href="https://www.opm.gov/policy-data-oversight/workforce-restructuring/reductions-in-force-rif/">RIF</a>‘d perfunctorily</strong>, seemingly at random or <a href="https://www.businessinsider.com/microsoft-internal-document-shows-buyout-offers-to-us-employees-2026-5">based on arbitrary (and possibly ageist) standards</a>. Team members simply disappear as they are locked out of systems. Everyone else needs to take on more work to cover their lost colleagues; <strong>morale and productivity sink as every remaining worker fears they will be next</strong>.</p>
<p>Because training junior devs is an investment, fewer companies hire them to juice their short-term numbers.&nbsp;<strong>American companies offshore developer positions</strong> to countries with worse labor protections&nbsp;and/or lower wages.</p>
<h3>Work conditions worsen</h3>
<p><strong>Developers pull back from directly creating code to supervise AI agents and review generated code,</strong> becoming alienated from their labor and lacking opportunity to truly exercise their skills. Bonuses become more competitive as budgets squeeze, further <a href="https://x.com/IATSE/status/2067352584038277433">dampening morale</a> and disincentivizing collaboration. <a href="https://www.engadget.com/2186261/meta-will-reportedly-let-employees-take-30-minute-breaks-from-its-tracking-program/">Degrading surveillance</a>&nbsp; and <a href="https://x.com/deedydas/status/2067428043195621454">reassignment to data labeling</a> makes workers nihilistic.</p>
<p><strong>RTO orders force workers back into exhausting commutes</strong>, only to sit on video calls all day anyway because the workforce is internationally distributed. International workers in the US on visas are <strong>trapped between extortive labor conditions and hostile immigration policies</strong> — quitting could mean being deported. Companies double down on work intensification, and more workers burn out of the industry.</p>
<h2>2. Coders are deskilled</h2>
<h3>Incentives heighten developer reliance on generation</h3>
<p>The few junior devs that are hired have to meet higher production quotas upfront, while senior devs have less time to mentor them and review their work, driving <strong>early career reliance on code generation</strong>, meaning that they don’t develop their own coding skills as richly through trial and error.</p>
<p>Meanwhile, <strong>the <a href="https://terriblesoftware.org/2026/03/03/nobody-gets-promoted-for-simplicity/">incentive structures</a> for individual developers <a href="https://www.404media.co/amazon-shuts-down-internal-ai-leaderboard-after-employees-cheated/">encourages generating code</a> over stepping back to consider high-level logic</strong> as production expectations intensify and bonuses are tied to output. <a href="https://x.com/thdxr/status/2022574719694758147">When generating is perceived to be cheap</a>, developers don’t need to weigh the value of a solution before producing it, and they drown their colleagues in pull requests. In this system, it is usually <a href="https://deadsimpletech.com/blog/attack-on-competence">the most competent who are the least rewarded</a>.</p>
<p>The code quality produced by an LLM likely varies in quality depending on the experience of the prompter and the base of training data. As more developer jobs shift from coding to supervising AI, <strong>prompters have less and less experience</strong> developing code on their own to draw upon.</p>
<h2>3. What next?</h2>
<h3>Sidebar: the risk to companies of deskilling industries</h3>
<p>My dad worked as an electrical engineer at a TV station (KGO for the SFers) — one of his duties was repairing equipment. Every piece of tech the station needed to run, on-site and off, he could fix. But over the decades, the companies selling the equipment started making it less repairable, so more and more, they had to send it back to the manufacturer to repair or replace. That meant he was the tail end of the career… <strong>no one younger was trained to replace him</strong>.</p>
<p>Unfortunately for the station, there remain a handful of things that really must be done locally / in-house. Having no one they <em>could </em>hire, they begged him not to fully retire, and he remained on-call for five years before finally deciding it wasn’t his problem and he didn’t have to do shit jobs like drive the giant satellite truck or climb <a href="https://en.wikipedia.org/wiki/Sutro_Tower">Sutro Tower</a> to repair equipment anymore.</p>
<p>Why I am thinking of this now:</p>
<blockquote><p>“There’s some use in distinguishing here between the worker who, having learned the skills of writing software over many years, now turns to so-called AI to assist her in that task; and the worker who will follow her some years hence and may never learn those skills, but will know only the work of supervision.”<br>
— <a href="https://aworkinglibrary.com/writing/designed-to-be-specialists">Mandy Brown</a></p></blockquote>
<p>If genAI actually pans out (or merely <a href="https://www.normaltech.ai/p/why-ai-hasnt-replaced-software-engineers">serves its purpose of allowing companies to lay off workers</a> they over-hired during the pandemic / when money was cheap), many software engineering positions may go this route. And for a while, that might work out. But what happens when all the software engineers who are intimately familiar with the code are gone, and problems become exponentially harder to fix… perhaps beyond the technical capabilities of supervisor-coders? <strong>If the code was generated, then <em>there is no company to send it back to to fix</em>.</strong> It is work that must be done by a software engineer, somewhere.</p>
<h3>Workers regain power?</h3>
<p><strong>Unemployed developers are forced to confront the reality that <a href="https://www.workingtheorys.com/p/stability#:~:text=What%E2%80%99s%20actually%20scarce%20now%20isn%E2%80%99t%20opportunity%20or%20independence.%20It%E2%80%99s%20stability.">they are working class</a></strong> when the jobs don’t come back. Some are rehired as contractors for similar positions at lower pay with worse benefits, also losing the status of being an FTE. <strong>People who <a href="https://yourbrainonmoney.substack.com/p/i-make-good-money-why-do-i-still?ref=thepurse.co#:~:text=Research%20on%20perceived%20mobility%20and%20populism%20shows%20that%20it%E2%80%99s%20the%20awareness%20of%20downward%20mobility%20%E2%80%94%20not%20the%20objective%20conditions%20%E2%80%94%20that%20drives%20radicalization">think of themselves</a> as <a href="https://tracydurnell.com/2023/02/10/who-is-high-income-anyway/">upper-middle-class</a> lose their homes</strong> when they can’t pay their mortgages.</p>
<p><em><strong>Seeing former colleagues forced to take large cuts in pay and prestige, do remaining developers recognize their own precarity and the lie of “meritocracy”?</strong></em></p>
<p>Lower bonuses weaken the incentive structure.</p>
<p><em><strong>Do people become more cutthroat, throwing their colleagues under the bus for the sake of ultra-competitive promotions and limited bonuses? Or does it make workers realize that working 80+ hour weeks for the promise of unguaranteed future compensation is a bum deal?</strong></em></p>
<p>Over the medium term, the weak entry level developer job market and worsening job conditions <strong>discourage students from becoming developers</strong>. Fewer workers enter the labor market as developers. More and more workers simply don’t have the skills to compete for high level roles. As the labor market (and competition) shrinks, this grants the remaining high-skilled workers greater power for the final years of their careers.</p>
<p><em><strong>Do they take it as their due and demand better compensation and restore working conditions for themselves only?&nbsp;</strong></em><em><strong>Do they establish a new pecking order with themselves at top and supervisor-coders as a lower tier, like testers? </strong></em><em><strong>Or do they use their newfound power as leverage to protect conditions for all workers by unionizing?</strong></em></p>
<p>&nbsp;</p>
<p>Recommended reading:</p>
<p><a href="https://ethanmarcotte.com/wrote/all-tomorrows-parties/">All tomorrow’s parties.</a> by Ethan Marcotte (2026) — <em>if you didn’t read it up top</em></p>
<p><a href="https://www.patreon.com/CultureStudy/posts/why-office-didnt-140919326">why office workers didn’t unionize</a> by Anne Helen Petersen (2020)</p>
<p><a href="https://dl.acm.org/doi/10.1145/3706598.3713434">The Cruel Optimism of Tech Work: Tech Workers’ Affective Attachments in the Aftermath of 2022-23 Tech Layoffs</a> by So et al (2025) — <em>this is long but the parts I skimmed were interesting</em></p>
<p><a href="https://www.bloodinthemachine.com/p/how-ai-is-killing-jobs-in-the-tech-f39">AI Killed My Job: Tech workers</a> by Brian Merchant (2025)</p>
<p><a href="https://www.andreymir.com/p/affordances-we-cant-refuse">Affordances we can’t refuse</a> by Andrey Mir (2026)</p>
<p><a href="https://aworkinglibrary.com/writing/ten-times">Ten Times</a> by Mandy Brown (2026)</p>
<p>&nbsp;</p>
<p>See also:</p>
<p><a href="https://tracydurnell.com/2025/11/28/on-work-who-sets-the-defaults/">On work: who sets the defaults?</a></p>
<p><a href="https://tracydurnell.com/2023/02/21/the-dream-of-ai-is-the-dream-of-free-labor/">The dream of AI is the dream of free labor</a></p>
]]></description>
      <pubDate>Fri, 19 Jun 2026 01:43:06 +0000</pubDate>
      <link>https://tracydurnell.com/2026/06/18/a-future-fable-how-american-tech-workers-went-union/</link>
      <dc:creator>Tracy Durnell&#39;s Mind Garden</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5269004062</guid>
    </item>
    <item>
      <title><![CDATA[Ending Responsive Images]]></title>
      <description><![CDATA[
<figure class="wp-block-image size-large"><img fetchpriority="high" width="1024" height="576" src="https://cloudfour.com/wp-content/uploads/2026/06/feature-srcset-r1-1024x576.jpg" alt="An cartoon illustration of the grim reaper carrying a scythe on dark blue background. A word balloon shows the grim reaper saying one word: srcset. " class="wp-image-8566" srcset="https://cloudfour.com/wp-content/uploads/2026/06/feature-srcset-r1-1024x576.jpg 1024w, https://cloudfour.com/wp-content/uploads/2026/06/feature-srcset-r1-300x169.jpg 300w, https://cloudfour.com/wp-content/uploads/2026/06/feature-srcset-r1-768x432.jpg 768w, https://cloudfour.com/wp-content/uploads/2026/06/feature-srcset-r1-1536x864.jpg 1536w, https://cloudfour.com/wp-content/uploads/2026/06/feature-srcset-r1-2048x1152.jpg 2048w" sizes="(max-width: 1024px) 100vw, 1024px"></figure>



<p class="wp-block-paragraph">Mat Marquis, former <a href="https://www.w3.org/community/respimg/">Responsive Images Working Group</a> (RICG) chair and <a href="https://www.indiegogo.com/en/projects/matmarquis/wilto-s-first-fight-night">amateur boxer</a>, isn’t known for pulling his punches. And yet, Marquis stopped short when he declared <a href="https://piccalil.li/blog/the-end-of-responsive-images/" data-type="link" data-id="https://piccalil.li/blog/the-end-of-responsive-images/">The End of Responsive Images</a>. Mat left responsive images on the ropes. I hope to finish them.</p>



<p class="wp-block-paragraph">Or rather, finish most responsive images. The ones below the fold. Let me explain.</p>



<h2 class="wp-block-heading">The responsive images race condition</h2>



<p class="wp-block-paragraph">In the beginning of our search for a responsive images solution, the challenge seemed simple: the <code>img</code> element only supported one source file, and we needed multiple sources to support responsive designs. However, if we supply multiple image sources, how will the browser know which source to choose?</p>



<p class="wp-block-paragraph">Therein lies the rub. Browsers download images before they’ve calculated the layout of the page. And if the browser doesn’t know the page layout, it doesn’t know the size of the image in the page. As I put it back <a href="https://cloudfour.com/thinks/the-real-conflict-behind-picture-and-srcset/" data-type="link" data-id="https://cloudfour.com/thinks/the-real-conflict-behind-picture-and-srcset/">in 2012</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">How do we reconcile a pre-parser that wants to know what size image to download ahead of time with an image technique that wants to respond to its environment once the page layout has been calculated?</p>
</blockquote>



<p class="wp-block-paragraph">Solving this problem is how we ended up with <code>srcset</code> and <code>sizes</code>. One tells the browser what source options it can choose from and the other provides the browser with the information it needs to calculate the size of the image in the page based on a measurement the browser always knows—the viewport size.</p>



<h2 class="wp-block-heading">Lazy loading changes everything</h2>



<p class="wp-block-paragraph">In the years since, lazy-loading images has become a common practice. We can safely defer loading images outside the initial view (aka, below the fold) in order to ensure that the browser’s resources are focused on the assets that are immediately visible.</p>



<p class="wp-block-paragraph">So many developers adopted this practice that it is now a web standard. Adding <code>loading="lazy"</code> to any image will tell the browser to delay downloading the image until it will be visible in the viewport.</p>



<p class="wp-block-paragraph">Because lazy-loaded images won’t download until they are visible, the race condition described earlier no longer exists. When lazy-loaded images are downloaded, the browser knows their size.</p>



<p class="wp-block-paragraph">Lazy-loaded images now allow us to simplify the markup for <code>sizes</code> as Mat Marquis explained:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">A few weeks ago, two patches landed in&nbsp;<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1819581">Gecko</a>&nbsp;and&nbsp;<a href="https://bugs.webkit.org/show_bug.cgi?id=310025">WebKit</a>&nbsp;— championed by Simon Pieters and Yoav Weiss, respectively, two of the RICG’s finest. These patches landed to little fanfare, quietly aligning Gecko and WebKit with&nbsp;<a href="https://issues.chromium.org/issues/40862170#comment27">Blink</a>&nbsp;in supporting a relatively recent addition to the HTML specification: support for an&nbsp;<code>auto</code>&nbsp;value in&nbsp;<code>sizes</code>&nbsp;attributes. Automatic&nbsp;<code>sizes</code>&nbsp;— the potential sizes of the rendered image, left up to the browser to determine alongside all those other factors. Fully automatic responsive images. Supply the browser with a list of candidates using&nbsp;<code>srcset</code>, bolt on&nbsp;<code>sizes="auto"</code>, and let the browser do the rest.</p>
</blockquote>



<p class="wp-block-paragraph">So long as an image has <code>loading="lazy"</code> you can use <code>sizes="auto"</code>. Here is the sample Mat provided (with a few cat-related tweaks on my part):</p>


<pre class="wp-block-code"><span><code class="hljs language-handlebars"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">img</span> 
  <span class="hljs-attr">loading</span>=<span class="hljs-string">"lazy"</span>
  <span class="hljs-attr">src</span>=<span class="hljs-string">"cat.jpeg"</span> 
  <span class="hljs-attr">srcset</span>=<span class="hljs-string">"cat-650.jpeg 650w, cat-960.jpeg 960w, cat-1400.jpeg 1400w"</span>
  <span class="hljs-attr">sizes</span>=<span class="hljs-string">"auto, (min-width: 1040px) 650px, calc(94.44vw - 15px)"</span>
  <span class="hljs-attr">width</span>=<span class="hljs-string">"650"</span>
  <span class="hljs-attr">height</span>=<span class="hljs-string">"400"</span>
  <span class="hljs-attr">alt</span>=<span class="hljs-string">"My cat Boyle is super cute."</span>&gt;</span></span></code></span></pre>


<p class="wp-block-paragraph">You’ll notice that Mat includes fallback <code>sizes</code> values to support older browsers. But with Safari and Firefox now supporting <code>sizes="auto"</code> we likely won’t need the fallback for long.</p>



<p class="has-gray-lighter-background-color has-background wp-block-paragraph"><strong>Caution:</strong> <em><a href="https://cloudfour.com/thinks/stop-lazy-loading-product-and-hero-images/">Images above the fold shouldn’t be lazy-loaded</a></em>. In fact, hero images are likely your <a href="https://web.dev/articles/lcp">Largest Content Paint (LCP)</a> image in <a href="https://web.dev/explore/learn-core-web-vitals" data-type="link" data-id="https://web.dev/explore/learn-core-web-vitals">Web Core Vitals</a> and should be loaded as early as possible. Therefore, images in the initial viewport will likely always require responsive images syntax.</p>



<h2 class="wp-block-heading">Removing srcset</h2>



<p class="wp-block-paragraph">Simplifying <code>sizes</code> is a good start, but I believe we can go further and radically simplify the markup for lazy-loaded images using <a href="https://wicg.github.io/responsive-image-client-hints/">Responsive Image Client Hints</a>. Responsive Image Client Hints is a draft specification being stewarded by <a href="https://ericportis.com/" data-type="link" data-id="https://ericportis.com/">Eric Portis</a> also of RICG fame.</p>



<p class="wp-block-paragraph">Responsive Image Client Hints builds on the existing Client Hints standard that has been implemented by Blink and thus supported by Chrome, Edge, and other browsers built on the Blink engine. Web page authors can declare support for Client Hints and ask for specific hints like the size of a requested image. The browser then sends this information in HTTP headers.</p>



<p class="wp-block-paragraph">For example, a server might send the following HTTP headers along with an HTML document:</p>


<pre class="wp-block-code"><span><code class="hljs language-php">Accept-CH: Sec-CH-Width
Permissions-Policy: ch-width=(<span class="hljs-keyword">self</span> <span class="hljs-string">"https://images.cloudfour.com"</span>)</code></span></pre>


<p class="wp-block-paragraph">The first line says that the server will accept client hints and is looking for Secure Client Hints Width (<code>Sec-CH-Width</code>). The permissions policy line helps reduce the potential for passive fingerprinting by restricting access to the specified domains. A similar declaration can be made via a meta tag:</p>


<pre class="wp-block-code"><span><code class="hljs language-handlebars"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"delegate-ch"</span> 
      <span class="hljs-attr">content</span>=<span class="hljs-string">"sec-ch-width https://images.cloudfour.com;"</span>&gt;</span></span></code></span></pre>


<p class="wp-block-paragraph">And then when an image on the page is requested by the browser, it will supply the requested Client Hints—in this case, the width of the image in the page:</p>


<pre class="wp-block-code"><span><code class="hljs language-yaml"><span class="hljs-string">GET</span> <span class="hljs-string">cat.jpg</span>
<span class="hljs-attr">Accept:</span> <span class="hljs-string">image/webp,image/*,*/*;q=0.8</span>
<span class="hljs-attr">Sec-CH-Width:</span> <span class="hljs-number">650</span></code></span></pre>


<p class="wp-block-paragraph"><code>Sec-CH-Width</code> value will contain the pixel width of the image in the page multipled by the display density. So if the image is 400 pixels wide on a 2x display, the value sent to the server will be <code>Sec-CH Width: 800</code>. </p>



<p class="wp-block-paragraph">If both the browser and server support client hints, and the image is lazy-loaded, we can greatly simplify our markup:</p>


<pre class="wp-block-code"><span><code class="hljs language-handlebars"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">img</span> 
  <span class="hljs-attr">loading</span>=<span class="hljs-string">"lazy"</span>
  <span class="hljs-attr">src</span>=<span class="hljs-string">"cat.jpeg"</span>
  <span class="hljs-attr">width</span>=<span class="hljs-string">"650"</span>
  <span class="hljs-attr">height</span>=<span class="hljs-string">"400"</span>
  <span class="hljs-attr">sizes</span>=<span class="hljs-string">"auto"</span>
  <span class="hljs-attr">alt</span>=<span class="hljs-string">"My cat Boyle is super cute."</span>&gt;</span></span></code></span></pre>


<p class="wp-block-paragraph">We no longer need <code>srcset</code> because the server knows what image sizes are available or can resize the image as needed based on the image size supplied by Client Hints. </p>



<p class="wp-block-paragraph">Because the image is lazy-loaded, we also know the size of the image in the page which means we shouldn’t need <code>sizes</code> at all. Unfortunately, the Client Hints standard expects the <code>sizes</code> attribute so we still need to supply <code>sizes="auto"</code>. Perhaps this can change in the future and we can remove the <code>sizes</code> attribute as well.</p>



<h2 class="wp-block-heading">How is an image source selected?</h2>



<p class="wp-block-paragraph">If we’re not supplying a list of image sources in <code>srcset</code> to the browser, how will one be selected? For this to work, you need an image resizing service on the server to respond to the image request. If you don’t have one, you can continue to use <code>srcset</code> and <code>sizes</code>.</p>



<p class="wp-block-paragraph">If this approach takes off, image resizing services may want to change the way they select what image size to return. Instead of working from a list of image sizes that the web page author <em>thinks</em> might be needed, the server can monitor what sizes are requested the most and make sure those sizes are cached. The server can optimize the cached image sizes based on real world usage.</p>



<h2 class="wp-block-heading">What about browsers that don’t support Client Hints?</h2>



<p class="wp-block-paragraph">Client Hints aren’t new. Chrome shipped them in v46 back in 2015. I <a href="https://cloudfour.com/thinks/responsive-images-201-client-hints/">wrote about their possible uses</a> in 2016. But developers haven’t widely adopted Client Hints because Firefox and Safari don’t support them.</p>



<p class="wp-block-paragraph">Because of this reality, I gave up on Clients Hints for many years. Three developments have caused me to revisit them:</p>



<ol class="wp-block-list">
<li>The proliferation of lazy-loading images and the standard to support them means we have a large body of images for which the speculative downloader’s race condition doesn’t apply.</li>



<li>In <a href="https://cloudfour.com/thinks/imageengine-deserves-a-second-look/" data-type="link" data-id="https://cloudfour.com/thinks/imageengine-deserves-a-second-look/">my review of ImageEngine</a>, I was impressed with how simple it is to use. Some of this simplicity is due to leveraging Client Hints when they are available.</li>



<li>Eric Portis has created a <a href="https://github.com/eeeps/w_auto-without-ch">small JavaScript library</a> that provides similar functionality for <a href="https://cloudinary.com/">Cloudinary</a> users in browsers that don’t support Client Hints.</li>
</ol>



<p class="wp-block-paragraph">Eric’s script works like this:</p>



<ul class="wp-block-list">
<li>If the browser supports Responsive Image Client Hints, the script does nothing.</li>



<li>If Client Hints aren’t supported, the script looks for images with the <code>loading="lazy"</code> attribute.</li>



<li>It also checks for some specific Cloudinary parameters in image’s <code>src</code> URL which tell the script that this image is one that it should modify.</li>



<li>The script then updates the width value in the Cloudinary URL to send along the actual size of the image in the page.</li>
</ul>



<p class="wp-block-paragraph">I’ve modified the sample markup from above to show how it would look using Cloudinary and supporting Eric’s script:</p>


<pre class="wp-block-code"><span><code class="hljs language-handlebars"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">img</span> 
  <span class="hljs-attr">loading</span>=<span class="hljs-string">"lazy"</span>
  <span class="hljs-attr">src</span>=<span class="hljs-string">"[…]/upload/c_limit,w_auto:breakpoints:650/v1716911991/catbeans_zbh3iu.jpg"</span>
  <span class="hljs-attr">width</span>=<span class="hljs-string">"650"</span>
  <span class="hljs-attr">height</span>=<span class="hljs-string">"366"</span>
  <span class="hljs-attr">sizes</span>=<span class="hljs-string">"auto"</span>
  <span class="hljs-attr">alt</span>=<span class="hljs-string">"My cat Boyle is super cute."</span>&gt;</span></span></code></span></pre>


<p class="wp-block-paragraph">The key section in the image URL is <code><a href="https://cloudinary.com/documentation/transformation_reference#w_auto">w_auto:breakpoints:650</a></code>:</p>



<ul class="wp-block-list">
<li><code>w_auto</code> — This tells Cloudinary that the image is responsive and should be dynamically resized to fit the size of the image in the page.</li>



<li><code>breakpoints</code> — This tells Cloudinary to pick image sizes using a performance budget—a <a href="https://cloudfour.com/thinks/responsive-images-breakpoint-generator/" data-type="link" data-id="https://cloudfour.com/thinks/responsive-images-breakpoint-generator/">feature</a> they created based on <a href="https://cloudfour.com/thinks/sensible-jumps-in-responsive-image-file-sizes/" data-type="link" data-id="https://cloudfour.com/thinks/sensible-jumps-in-responsive-image-file-sizes/">one of my articles</a>. If performance budgets aren’t your speed, you can set a value for how many pixels before a new image is created on the server.</li>



<li><code>650</code> — this is the fallback width that should be used if the browser doesn’t send additional information via Client Hints.</li>
</ul>



<p class="wp-block-paragraph">If the image in the page was actually 900 pixels wide at 2x display density, the script would update the <code>src</code> to be:</p>


<pre class="wp-block-code"><span><code class="hljs language-bash">  src=<span class="hljs-string">"[…]/upload/c_limit,w_auto:breakpoints:1800/v1716911991/catbeans_zbh3iu.jpg"</span></code></span></pre>


<p class="wp-block-paragraph">For browsers that don’t support Client Hints, the actual size of the image in the page is added to the URL. This technique could be modified to work with other image resizing servers.</p>



<p class="wp-block-paragraph">There are a few caveats with this script:</p>



<ul class="wp-block-list">
<li>It must be placed in the <code>&lt;head&gt;</code></li>



<li>It has to be a blocking script. You can’t <code>async</code>, <code>defer</code>, or use <code>type="module"</code>.</li>
</ul>



<p class="wp-block-paragraph">Eric <a href="https://github.com/eeeps/w_auto-without-ch" data-type="link" data-id="https://github.com/eeeps/w_auto-without-ch">explains</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">This script needs to see&nbsp;<code>&lt;img&gt;</code>s as soon as they are parsed, so that it can prepare to re-write their URLs as soon as they’re laid out. If the script executes after an&nbsp;<code>&lt;img&gt;</code>&nbsp;has been parsed, it won’t get a chance to do any of that; it’ll do nothing.</p>



<p class="wp-block-paragraph">Luckily, it’s only ~950 bytes (~600B compressed).</p>
</blockquote>



<p class="wp-block-paragraph">Eric has created a couple of CodePen examples that show the script in action. One uses <a href="https://codepen.io/editor/eeeps/pen/019eae66-575e-77ba-948d-68059a65ec5a">placeholder images</a> and the other <a href="https://codepen.io/editor/eeeps/pen/019eb2ca-c06f-709e-885f-573ef897ff56">real images from WikiPedia</a>. </p>



<p class="wp-block-paragraph">I normally wouldn’t advocate for a blocking script, but this seems lightweight and the tradeoff is worth evaluating. Make sure to measure the performance impact on your site.</p>



<h2 class="wp-block-heading">A JavaScript Solution? Really?</h2>



<p class="wp-block-paragraph">During the halcyon days of the RICG, not a month went by without someone proposing a way to solve responsive images using JavaScript. We repeatedly and patiently explained why JavaScript couldn’t be used because images are downloaded before JavaScript is processed.</p>



<p class="wp-block-paragraph">Lazy-loading images outside the initial viewport changes this equation. We now have a small window of opportunity to modify lazy-loaded images so we can add the equivalent of Client Hints to their URLs when their size is known.</p>



<h2 class="wp-block-heading">The End of Most Responsive Images</h2>



<figure class="wp-block-image size-large"><img width="1024" height="576" src="https://cloudfour.com/wp-content/uploads/2026/06/catbeans-1024x576.jpg" alt="Our adorable black and white cat Boyle sleeps on the back of the sofa. His little paws are out in front of him like he is super man. " class="wp-image-8569" srcset="https://cloudfour.com/wp-content/uploads/2026/06/catbeans-1024x576.jpg 1024w, https://cloudfour.com/wp-content/uploads/2026/06/catbeans-300x169.jpg 300w, https://cloudfour.com/wp-content/uploads/2026/06/catbeans-768x432.jpg 768w, https://cloudfour.com/wp-content/uploads/2026/06/catbeans.jpg 1200w" sizes="(max-width: 1024px) 100vw, 1024px"><figcaption class="wp-element-caption">Meet Boyle, the super cute cat I alluded to in earlier examples. </figcaption></figure>



<p class="wp-block-paragraph">Responsive images syntax will always have a role to play. Not every web page will have a image resizing server and images visible in the initial viewport will still face the race condition that gave the RICG fits. Any images in the initial viewport <a href="https://cloudfour.com/thinks/stop-lazy-loading-product-and-hero-images/">shouldn’t be lazy-loaded</a>. </p>



<p class="wp-block-paragraph">Fortunately, most images on the web live below the fold and thus can be lazy-loaded. If they can be lazy-loaded, then we no longer face the intractable race condition that forced us to place presentation information in the <code>sizes</code> attribute where it doesn’t belong.</p>



<p class="wp-block-paragraph">Moving to simpler responsive images will require some work. According to CanIUse.com, <a href="https://caniuse.com/?search=client+hints">78% of users have access to Client Hints</a>. That’s a great start, but ideally we’d get other browsers on board. Firefox and Safari expressed privacy concerns with client hints which is why they didn’t implement them. I’m hopeful that some combination a mandatory permissions policy (e.g., primary domain and subdomains only would be ideal) and a more limited set of hints (image width and dpr only?), that we might be able to get other browser makers on board.</p>



<p class="wp-block-paragraph">But even without these changes, it is possible now to remove <code>srcset</code> and simplify <code>sizes</code> using a combination of lazy-loading, Client Hints, and the techniques that Eric Portis has pioneered. </p>



<p class="wp-block-paragraph">Mat concluded his article by saying:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">I won’t miss all those hand-hewn&nbsp;<code>sizes</code>&nbsp;attributes; I never had any love for them to begin with. I will never experience a shred of nostalgia for a thing that I helped make real and inexorably bound to my name. A syntax was never the goal; the goal was always a&nbsp;<em>mechanism</em>. At the time, the web platform lacked a way for browsers to make smarter decisions about what image asset to request and when, and no amount of clever scripting or markup trickery would ever result in an asset request as fast or efficient as one the browser itself could make.</p>
</blockquote>



<p class="wp-block-paragraph">None of us working in the RICG were ever enamored with our solutions. We wanted something simpler, but <code>srcset</code> and <code>sizes</code> was the best we could do with the constraints we were given. Now that lazy-loading has removed one of the constraints, I believe we can do better.</p>



<p class="wp-block-paragraph"><em>Special thanks to Eric Portis, Mat Marquis, and Scott Vandehey for their feedback and to Tyler Sticka for the grim reaper illustration.</em></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>Thu, 11 Jun 2026 15:54:13 +0000</pubDate>
      <link>https://cloudfour.com/thinks/ending-responsive-images/</link>
      <dc:creator>Sharing what we learn about the responsive web – Cloud Four</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5256096645</guid>
    </item>
    <item>
      <title><![CDATA[Important Good Internet announcement]]></title>
      <description><![CDATA[<p>In late May, <em>Good Internet</em> had a breach into the web server hosting it. I want to start off by saying thank you to everyone who gave me a head's up that the website was being weird! I ended up transferring hosts, and I'm currently still restoring anything that was lost in the migration.</p><p><em>No database breach was found</em>, so no action is required on your part. The breach only sought to redirect links to a malicious download, and no personal information was accessed.</p><hr><p>With this situation – as I've been working on only it for the past two weeks – I have realized this project may have reached the ceiling of effort on my part. I have been partially over my head technically and partially burnt out from the process.</p><p>After the Autumn/Winter issue, I wrote a personal blog post about <a href="https://library.xandra.cc/starting-a-print-magazine/?ref=goodinternetmagazine.com" rel="noreferrer">how it felt to publish a magazine</a>. In response, I received a bounty of responses of folks willing to learn the programs or experienced in design. I cried at the response and found myself unable to reply, in shock at the thought that folks would actually step up and help. Thank you to everyone who reached out, genuinely. I wish I had asked for help sooner.</p><p>I must confess: I have entirely lost money on <em>Good Internet</em>, despite a not-insignificant amount of folks who supported the project. I undercut myself and tried to keep the magazine affordable. Without scale, print media is – <em>shocker to no one </em>– expensive. Unfortunately, my well has gone dry, both energetically and financially.</p><div class="kg-card kg-toggle-card" data-kg-toggle-state="close">
            <div class="kg-toggle-heading">
                <h4 class="kg-toggle-heading-text"><span style="white-space: pre-wrap;">Some personal context</span></h4>
                <button class="kg-toggle-card-icon" aria-label="Expand toggle to read content">
                    <svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                        <path class="cls-1" d="M23.25,7.311,12.53,18.03a.749.749,0,0,1-1.06,0L.75,7.311"></path>
                    </svg>
                </button>
            </div>
            <div class="kg-toggle-content"><p><span style="white-space: pre-wrap;">After five years of deep involvement in the personal web and trying to help others in expressing themselves online, I want to pivot my focus on my life and creating the vision of my next chapter. I've been able to focus deeply on projects like </span><i><em class="italic" style="white-space: pre-wrap;">Good Internet </em></i><span style="white-space: pre-wrap;">over the last year while being unemployed, but my projects now outnumber my spoons as my life has changed; my projects have kept me inside and online, even when I don't want to be.</span></p><p><span style="white-space: pre-wrap;">Journalism, writing, and digital content is indeed my background and professional field, but after seventeen years of being publicly perceived in my writing, my efforts, my projects – I want to live without observation for a while and create without pressure.</span></p><p><span style="white-space: pre-wrap;">When I found the indie web, I was in a completely different place. I was single and living alone in a studio apartment in Nob Hill, San Francisco, thinking the world as we knew it was ending.</span></p><p><span style="white-space: pre-wrap;">I'm now 35. I'm getting married in September to my partner, with whom I live in a completely different state. I want to make our house a home together. I am involved in a lawsuit that I can't talk about, and our trial, if it goes to that, begins at the end of the year. I'm still looking for a job. (And if you know of any content-related roles available, reach out.)</span></p><p><span style="white-space: pre-wrap;">My life is completely different from where I started. It's different than when I started </span><i><em class="italic" style="white-space: pre-wrap;">Good Internet</em></i><span style="white-space: pre-wrap;">.</span></p></div>
        </div><p><em>Good Internet</em> completed a full volume, but I do not feel comfortable maintaining subscriptions during this indefinite hiatus.</p><p>Immediately:</p><ol><li>All subscriptions, monthly and annual, will be canceled.</li><li>Print editions of Spring 2025 are <strong>still available </strong>for purchase.</li><li>Any further issues will be announced via email.</li></ol><p>High resolution and low resolution <a href="https://www.goodinternetmagazine.com/members-issues/" rel="noreferrer">Digital Editions</a> will be available for download to all free members in a couple of weeks.</p><p>All money that was received from 2026 subscriptions are going into the digital infrastructure of <em>Good Internet</em>, including hosting through our new host and email services. Please reach out if you'd like a refund, which I entirely understand at this stage.</p><p>I'd like to keep the site up for archive, so at least through the end of the year. I'd like to reassess then, also, and consider if <em>Good Internet </em>comes back in 2027. I'll notify you as soon as I know. If you'd like to continue supporting the website and project, feel free to <a href="https://www.goodinternetmagazine.com/#/portal/support" rel="noreferrer">contribute</a> – you'd be helping me a lot.</p><p>I know this decision will be met with mixed reactions, but I want to thank you so much for your continued support and belief in this project. I love each and every single one of you. From the bottom of my heart, thank you.</p><p><strong>x </strong><a href="https://xandra.cc/?ref=goodinternetmagazine.com" rel="noreferrer"><strong>Alexandra</strong></a><strong> </strong><br>personal email: al@xandra.cc </p>]]></description>
      <pubDate>Mon, 08 Jun 2026 20:00:42 +0000</pubDate>
      <link>https://www.goodinternetmagazine.com/important/</link>
      <dc:creator>Good Internet</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5252772234</guid>
    </item>
    <item>
      <title><![CDATA[Going Full Circle on CSS Toggle Transitions]]></title>
      <description><![CDATA[
                
                    <p>I recently worked on some “accordion” component with a custom marker to indicate open and closed states. I had it set up so the marker, a chevron, would rotate by 180&nbsp;degrees when the component was in its open state. Add a CSS transition (when motion preferences allow it), job done.</p>
<p>But… it would animate backwards upon closing, “rewind” so to speak, which is exactly how CSS should work, but I wanted it to “complete” the rotation, so:</p>
<ol>
<li>idle: start at 0&nbsp;degrees</li>
<li>expanded: go to 180&nbsp;degrees</li>
<li>closed again: go to 360&nbsp;degrees</li>
<li>idle: start at 0&nbsp;degrees</li>
<li>… and so on.</li>
</ol>
<p>Let’s take a look at what I tried, and which solution I landed on.</p>
<section class="callout" aria-labelledby="co-6dnm6uc2d2"><p id="co-6dnm6uc2d2" class="callout-label | h3">No Gecko, amigo</p><p>This works in two browser engines, Chromium and WebKit, and my assumption is that they both are working as the specification has been, well… specified. Firefox, however, skips the closing transition. If you have any idea why, please let me know!</p></section>
<p>First, here is the base markup and styles for all the demos:</p>
<code-wrap class="codeblock" copy-label="📋 Copy" copy-class="button" copy-done-label="Copied!" actionwrap-class="codewrap-action cellstack cellstack--center"><figure><figcaption class="codeblock-toolbar"><span class="codeblock-toolbar-label"><svg xmlns:xlink="http://www.w3.org/1999/xlink" class="inline-icon inline-icon--center" aria-hidden="true" width="16" height="16"><use xlink:href="#svg__html-icon" width="100%" height="100%"></use></svg> HTML</span></figcaption><pre tabindex="0" class="language-html codeblock-pre" translate="no"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>details</span><span class="token punctuation">&gt;</span></span>
	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>summary</span><span class="token punctuation">&gt;</span></span>
		<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span><span class="token punctuation">&gt;</span></span>Learn more about this!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">&gt;</span></span>
		<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>24<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>24<span class="token punctuation">"</span></span> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 24 24<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>details-marker<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
			<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>path</span> <span class="token attr-name">d</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>M6 9l6 6 6-6<span class="token punctuation">"</span></span> <span class="token attr-name">fill</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>none<span class="token punctuation">"</span></span> <span class="token attr-name">stroke</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>currentColor<span class="token punctuation">"</span></span> <span class="token attr-name">stroke-width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
		<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">&gt;</span></span>
	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>summary</span><span class="token punctuation">&gt;</span></span>
	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>details-content<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
		<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">&gt;</span></span>I have important things to say!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">&gt;</span></span>
	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>details</span><span class="token punctuation">&gt;</span></span></code></pre>
</figure></code-wrap><code-wrap class="codeblock" copy-label="📋 Copy" copy-class="button" copy-done-label="Copied!" actionwrap-class="codewrap-action cellstack cellstack--center"><figure><figcaption class="codeblock-toolbar"><span class="codeblock-toolbar-label"><svg xmlns:xlink="http://www.w3.org/1999/xlink" class="inline-icon inline-icon--center" aria-hidden="true" width="16" height="16"><use xlink:href="#svg__css-icon" width="100%" height="100%"></use></svg> CSS</span></figcaption><pre tabindex="0" class="language-css codeblock-pre" translate="no"><code class="language-css"><span class="token selector">details</span> <span class="token punctuation">{</span>
	<span class="token property">--DET-transition-duration</span><span class="token punctuation">:</span> 300ms<span class="token punctuation">;</span>
	<span class="token property">--DET-transition-easing</span><span class="token punctuation">:</span> ease-in-out<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment">/* Rewinds upon closing */</span>
<span class="token selector">.details-marker</span> <span class="token punctuation">{</span>
	<span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
		<span class="token property">transition</span><span class="token punctuation">:</span> transform <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-easing<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-duration<span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>

	<span class="token selector">details[open] &amp;</span> <span class="token punctuation">{</span>
		<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>180deg<span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token comment">/* Aesthetics skipped for brevity but available in the CodePen demos */</span></code></pre>
</figure></code-wrap><p>And here’s what that looks like: it works everywhere but the marker “rewinds” — keep an eye on that chevron at the end of the line there when opening and closing the details/expander/accordion/stretchyboi:</p>
<div class="cp_embed_wrapper codepen | cellstack cellstack--center" data-height="300" data-default-tab="result" data-slug-hash="vEyjvrb" data-version="1">
			<div class="single-media">
				<img src="https://res.cloudinary.com/chriskirknielsen/image/fetch/https://shots.codepen.io/username/pen/vEyjvrb-512.webp?version=1788220909749" alt="" class="codepen-thumbnail" width="512" height="288" loading="lazy" decoding="async" style="height: 300px;">
			</div>
			<a href="https://codepen.io/chriskirknielsen/pen/vEyjvrb" class="button">View on CodePen</a>
		</div><script async="" src="https://public.codepenassets.com/embed/index.js"></script>
<p>I managed to get most of the way using a <em>registered</em> custom property to hold the “opened” state until everything was closed, using a transition on this property with a <code>step-end</code> timing function, which holds a value at its initial state until the duration has passed (e.g. from <code>0</code> to <code>1</code> over a second, it’ll stay at <code>0</code> for a second, then immediately flip to <code>1</code> without any interpolation). My original (and promising) result looked like this:</p>
<code-wrap class="codeblock" copy-label="📋 Copy" copy-class="button" copy-done-label="Copied!" actionwrap-class="codewrap-action cellstack cellstack--center"><figure><figcaption class="codeblock-toolbar"><span class="codeblock-toolbar-label"><svg xmlns:xlink="http://www.w3.org/1999/xlink" class="inline-icon inline-icon--center" aria-hidden="true" width="16" height="16"><use xlink:href="#svg__css-icon" width="100%" height="100%"></use></svg> CSS</span></figcaption><pre tabindex="0" class="language-css codeblock-pre" translate="no"><code class="language-css"><span class="token atrule"><span class="token rule">@property</span> --DET-state</span> <span class="token punctuation">{</span>
	<span class="token property">syntax</span><span class="token punctuation">:</span> <span class="token string">'&lt;number&gt;'</span><span class="token punctuation">;</span>
	<span class="token property">initial-value</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
	<span class="token property">inherits</span><span class="token punctuation">:</span> true<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token selector">details</span> <span class="token punctuation">{</span>
	<span class="token property">--DET-state</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
	<span class="token property">--DET-transition-duration</span><span class="token punctuation">:</span> 300ms<span class="token punctuation">;</span>
	<span class="token property">--DET-transition-easing</span><span class="token punctuation">:</span> ease-in-out<span class="token punctuation">;</span>
	
	<span class="token property">transition</span><span class="token punctuation">:</span> --DET-state <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-duration<span class="token punctuation">)</span> step-end<span class="token punctuation">;</span>

	<span class="token selector">&amp;[open]</span> <span class="token punctuation">{</span>
		<span class="token property">--DET-state</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token comment">/* "Works" in Chrome and Safari, but unwinds after closing */</span>
<span class="token selector">.details-marker</span> <span class="token punctuation">{</span>
	<span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
		<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span><span class="token function">calc</span><span class="token punctuation">(</span>360deg * <span class="token function">var</span><span class="token punctuation">(</span>--DET-state<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token property">transition</span><span class="token punctuation">:</span> transform <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-easing<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-duration<span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>

	<span class="token selector">details[open] &amp;</span> <span class="token punctuation">{</span>
		<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>180deg<span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
</figure></code-wrap><div class="cp_embed_wrapper codepen | cellstack cellstack--center" data-height="300" data-default-tab="result" data-slug-hash="OPbZdOb" data-version="1">
			<div class="single-media">
				<img src="https://res.cloudinary.com/chriskirknielsen/image/fetch/https://shots.codepen.io/username/pen/OPbZdOb-512.webp?version=1788220909749" alt="" class="codepen-thumbnail" width="512" height="288" loading="lazy" decoding="async" style="height: 300px;">
			</div>
			<a href="https://codepen.io/chriskirknielsen/pen/OPbZdOb" class="button">View on CodePen</a>
		</div>
<p>It did almost everything I wanted (in Chrome and Safari…), going from 0&nbsp;degrees, to 180, and to 360 on close, except… when the rotation was completed, and <code>--DET-state</code> returned to <code>0</code>, the marker would also transition from 360&nbsp;degrees to 0&nbsp;degrees, so it looked kind of loopy (pun intended, thank you very much, I’ll be here all week).</p>
<p>I tried a few things, like adjusting the timing function between open and closed states. Ultimately, I came to the conclusion that I needed the closing transition to run normally while <code>--DET-state</code> was held at <code>1</code>, but the second transition due to <code>--DET-state</code> flipping back to <code>0</code> should be skipped. I could achieve this by adjusting the <code>transition-duration</code> itself, but this doesn’t work in Firefox <em>and</em> Chrome — Safari does what I expect of it, in version 18.6 as well as Technology Preview:</p>
<code-wrap class="codeblock" copy-label="📋 Copy" copy-class="button" copy-done-label="Copied!" actionwrap-class="codewrap-action cellstack cellstack--center"><figure><figcaption class="codeblock-toolbar"><span class="codeblock-toolbar-label"><svg xmlns:xlink="http://www.w3.org/1999/xlink" class="inline-icon inline-icon--center" aria-hidden="true" width="16" height="16"><use xlink:href="#svg__css-icon" width="100%" height="100%"></use></svg> CSS</span></figcaption><pre tabindex="0" class="language-css codeblock-pre" translate="no"><code class="language-css"><span class="token comment">/* This does not work in Firefox and Chrome! */</span>
<span class="token selector">.details-marker</span> <span class="token punctuation">{</span>
	<span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
		<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span><span class="token function">calc</span><span class="token punctuation">(</span>360deg * <span class="token function">var</span><span class="token punctuation">(</span>--DET-state<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token property">transition-property</span><span class="token punctuation">:</span> transform<span class="token punctuation">;</span>
		<span class="token property">transition-timing-function</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-easing<span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token property">transition-duration</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--DET-state<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-duration<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

		<span class="token selector">details[open] &amp;</span> <span class="token punctuation">{</span>
			<span class="token property">transition-duration</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-duration<span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token punctuation">}</span>
	<span class="token punctuation">}</span>

	<span class="token selector">details[open] &amp;</span> <span class="token punctuation">{</span>
		<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>180deg<span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
</figure></code-wrap><div class="cp_embed_wrapper codepen | cellstack cellstack--center" data-height="300" data-default-tab="result" data-slug-hash="XJNqGbL" data-version="1">
			<div class="single-media">
				<img src="https://res.cloudinary.com/chriskirknielsen/image/fetch/https://shots.codepen.io/username/pen/XJNqGbL-512.webp?version=1788220909749" alt="" class="codepen-thumbnail" width="512" height="288" loading="lazy" decoding="async" style="height: 300px;">
			</div>
			<a href="https://codepen.io/chriskirknielsen/pen/XJNqGbL" class="button">View on CodePen</a>
		</div>
<p>Instead of quashing the duration, I tried using a negative <code>transition-delay</code>, and I clicked in disbelief several times to see the marker animate exactly as I wanted. My final result is as follows:</p>
<code-wrap class="codeblock" copy-label="📋 Copy" copy-class="button" copy-done-label="Copied!" actionwrap-class="codewrap-action cellstack cellstack--center"><figure><figcaption class="codeblock-toolbar"><span class="codeblock-toolbar-label"><svg xmlns:xlink="http://www.w3.org/1999/xlink" class="inline-icon inline-icon--center" aria-hidden="true" width="16" height="16"><use xlink:href="#svg__css-icon" width="100%" height="100%"></use></svg> CSS</span></figcaption><pre tabindex="0" class="language-css codeblock-pre" translate="no"><code class="language-css"><span class="token comment">/* This does not work in Firefox! */</span>
<span class="token selector">.details-marker</span> <span class="token punctuation">{</span>
	<span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
		<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span><span class="token function">calc</span><span class="token punctuation">(</span>360deg * <span class="token function">var</span><span class="token punctuation">(</span>--DET-state<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token property">transition</span><span class="token punctuation">:</span> transform <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-easing<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-duration<span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token property">transition-delay</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--DET-state<span class="token punctuation">)</span> - 1<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-duration<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token comment">/*
			The delay above could also be expressed like so, if you find it easier to read:
			transition-delay: calc(-1 * (1 - var(--DET-state)) * var(--DET-transition-duration));
		*/</span>

		<span class="token selector">details[open] &amp;</span> <span class="token punctuation">{</span>
			<span class="token property">transition-delay</span><span class="token punctuation">:</span> 0s<span class="token punctuation">;</span>
		<span class="token punctuation">}</span>
		
		<span class="token atrule"><span class="token rule">@-moz-document</span> <span class="token function">url-prefix</span><span class="token punctuation">(</span><span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
			<span class="token property">transition</span><span class="token punctuation">:</span> transform <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-easing<span class="token punctuation">)</span> <span class="token function">var</span><span class="token punctuation">(</span>--DET-transition-duration<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">/* Standard look in Firefox */</span>
		<span class="token punctuation">}</span>
	<span class="token punctuation">}</span>

	<span class="token selector">details[open] &amp;</span> <span class="token punctuation">{</span>
		<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>180deg<span class="token punctuation">)</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
</figure></code-wrap><div class="cp_embed_wrapper codepen | cellstack cellstack--center" data-height="300" data-default-tab="result" data-slug-hash="NPbMJdB" data-version="1">
			<div class="single-media">
				<img src="https://res.cloudinary.com/chriskirknielsen/image/fetch/https://shots.codepen.io/username/pen/NPbMJdB-512.webp?version=1788220909749" alt="" class="codepen-thumbnail" width="512" height="288" loading="lazy" decoding="async" style="height: 300px;">
			</div>
			<a href="https://codepen.io/chriskirknielsen/pen/NPbMJdB" class="button">View on CodePen</a>
		</div>
<p>However, as mentioned at the start of this article, <strong>this does not work in Firefox</strong>. Which sucks! These properties are not exactly bleeding edge… I know I’m manipulating time in weird ways here, but this doesn’t require a flux capacitor… it feels like it should work.</p>
<p>In any case, in Firefox, with a little browser-specific hack, the transition is reverted and the chevron rewinds back to 0&nbsp;degrees instead, which means it’s still absolutely usable, and I consider this a nice little progressive enhancement.</p>
<p>I am certain there are ways to make this work: maybe some trick using <code>mod()</code> off by <code>0.01deg</code>, or adding in a CSS keyframe animation (I specifically avoided keyframes as you cannot smoothly reverse the transition if you interrupt it midway). Definitely easy with some JavaScript (either Web Animation API or an event listener), but that’s no fun!</p>
<p>If you have ideas, I’d be happy to hear some details and expand this article accordingly (<em>eehhh?</em>).</p>
<p>PS: As a bonus, we can totally have the chevron face to the right when closed, down when open, and still do a full rotation!</p>
<div class="cp_embed_wrapper codepen | cellstack cellstack--center" data-height="300" data-default-tab="result" data-slug-hash="gbLKbLZ" data-version="1">
			<div class="single-media">
				<img src="https://res.cloudinary.com/chriskirknielsen/image/fetch/https://shots.codepen.io/username/pen/gbLKbLZ-512.webp?version=1788220909749" alt="" class="codepen-thumbnail" width="512" height="288" loading="lazy" decoding="async" style="height: 300px;">
			</div>
			<a href="https://codepen.io/chriskirknielsen/pen/gbLKbLZ" class="button">View on CodePen</a>
		</div>
<h2 id="update" tabindex="-1">
<a class="heading-anchor" href="https://chriskirknielsen.com/blog/going-full-circle-css-toggle-transitions/#update">Update</a></h2>
<p>vrugtehagel and Temani Afif chimed in on Mastodon with different solutions, which work cross-browser! Not only that but they both require less code, so I’m all for it.</p>
<p>vrugtehagel suggested using <code>linear()</code> to create a negative version of the easing, and it works! A little bit of a pain to maintain if you want to adjust the easing but I wouldn’t imagine you’d change that often, so this is really neat!</p>
<div class="cp_embed_wrapper codepen | cellstack cellstack--center" data-height="300" data-default-tab="result" data-slug-hash="PwbaEZy" data-version="2">
			<div class="single-media">
				<img src="https://res.cloudinary.com/chriskirknielsen/image/fetch/https://shots.codepen.io/username/pen/PwbaEZy-512.webp?version=1788220909749" alt="" class="codepen-thumbnail" width="512" height="288" loading="lazy" decoding="async" style="height: 300px;">
			</div>
			<a href="https://codepen.io/vrugtehagel/pen/PwbaEZy" class="button">View on CodePen</a>
		</div>
<p>Temani suggested something even simpler, that kind of resonates with my negated duration approach, but applied to a different property. Because the chevron is symmetrical, mirroring it allows to get the same effect: rotate then mirror.</p>
<div class="cp_embed_wrapper codepen | cellstack cellstack--center" data-height="300" data-default-tab="result" data-slug-hash="VYmdBpj" data-version="1">
			<div class="single-media">
				<img src="https://res.cloudinary.com/chriskirknielsen/image/fetch/https://shots.codepen.io/username/pen/VYmdBpj-512.webp?version=1788220909749" alt="" class="codepen-thumbnail" width="512" height="288" loading="lazy" decoding="async" style="height: 300px;">
			</div>
			<a href="https://codepen.io/t_afif/pen/VYmdBpj" class="button">View on CodePen</a>
		</div>
                    
                
            ]]></description>
      <pubDate>Thu, 04 Jun 2026 04:04:00 +0000</pubDate>
      <link>https://chriskirknielsen.com/blog/going-full-circle-css-toggle-transitions/</link>
      <dc:creator>chriskirknielsen</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5249218014</guid>
    </item>
    <item>
      <title><![CDATA[Are you standard.site?]]></title>
      <description><![CDATA[<p><a href="https://standard.site/" rel="noopener noreferrer" target="_blank">Standard.site</a> provides shared <glossary-term id="--term-atproto"><a href="https://atproto.com/" rel="noopener noreferrer" target="_blank">AT Protocol</a></glossary-term> lexicons. Atproto is just spicy <glossary-term id="--term-json"><a href="https://www.json.org/" rel="noopener noreferrer" target="_blank">JSON</a></glossary-term> and asymmetric cryptography. I’ve <a href="https://dbushell.com/2026/03/10/building-on-at-protocol/">tried to explain atproto</a> in more detail before.</p><p>Bluesky has always supported a few <a href="https://ogp.me/" rel="noopener noreferrer" target="_blank">open graph meta tags</a> which I use to <a href="https://dbushell.com/2024/11/15/generate-open-graph-images-with-playwright/">generate images</a> for blog posts. That’s part of the social media game; get in people’s faces as loudly as possible. Now the game has changed!</p><h2 id="fancy-button">Fancy button</h2><p>I return Monday <a href="https://dbushell.com/2026/06/01/challenge-and-opportunity/">ready to work</a> and suddenly I start seeing a fancy new “View publication” button appear in my Bluesky feed. I’ve never wanted nor needed a button before but now that people are rocking buttons, what am I supposed to be, a buttonless pleb?</p><p>I got my own button it looks like this:</p><figure class="Image"><img src="https://dbushell.com/images/blog/2026/bluesky-view-publication.avif" alt="example of a Bluesky embed with the 'View publication' button" width="606" height="480" decoding="async" fetchpriority="low" loading="lazy" id="--img-e6e30467"></figure><p>Mat Marquis, fellow button connoisseur, was quick with a guide to <a href="https://wil.to/posts/standard-site/" rel="noopener noreferrer" target="_blank">“Implementing Standard.Site”</a> which I hastily copied.</p><h2 id="automation">Automation</h2><p>Mat used an <a href="https://pdsls.dev/" rel="noopener noreferrer" target="_blank">atproto explorer</a> to edit records which is akin to rawdoggin’ SQL in production. Given the weekly GitHub and NPM malware party this is probably a safer play than running <code>npm install</code> yourself. I’m never going to remember to publish manually though.</p><p>I have a <a href="https://dbushell.com/2025/05/11/the-static-site-churns/">janky build script</a> and some experience with the <a href="https://github.com/mary-ext/atcute/tree/trunk/packages/clients/client" rel="noopener noreferrer" target="_blank">@atcute libraries</a>. How hard can it be? My script begins by generating a manifest of pages by parsing markdown before rendering the HTML template.</p><p>I added a new step that fetches all atproto records in the <code>site.standard.document</code> collection. It cross-references the paths in my manifest. Any unknown path has the record deleted. It then iterates the manifest and either updates the atproto record (if title or description has changed), or creates a new record if none existed. Finally it adds the atproto URI to the manifest for the <code>&lt;link&gt;</code> element.</p><p>Now my blog is <a href="https://standard.site/" rel="noopener noreferrer" target="_blank">standard.site</a> and I have a fancy button to prove it.</p>
<hr>
<p>
Thanks for reading! Follow me on <a href="https://dbushell.com/mastodon/">Mastodon</a> and <a href="https://dbushell.com/bluesky/">Bluesky</a>.
Subscribe to my <a href="https://dbushell.com/rss.xml">Blog</a> and <a href="https://dbushell.com/notes/rss.xml">Notes</a> or <a href="https://dbushell.com/merge/rss.xml">Combined</a> feeds.
</p>
]]></description>
      <pubDate>Fri, 05 Jun 2026 11:13:47 +0000</pubDate>
      <link>https://dbushell.com/2026/06/05/are-you-standard-site/</link>
      <dc:creator>dbushell.com (blog)</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5249573353</guid>
    </item>
    <item>
      <title><![CDATA[I did the Standard.site thing]]></title>
      <description><![CDATA[
<p>It seems like <a href="https://wil.to/posts/standard-site/">everyone</a> is integrating <a href="https://standard.site/">Standard.site</a> on their personal websites (and beyond) at the moment. It’s mainly been encouraged by Bluesky’s recent update to the treatment of sites integrated with Standard.site. <a href="https://mozzius.dev/post/3mn3z7u7jgsgl">Sam has a good explainer here</a>.</p>



<p>It’s a cool feature! There’s loads of different platforms doing cool stuff, all using different data structures, so the standardisation was really needed. Integrating is <em>fairly</em> straightforward too.</p>



<p>If you’re using WordPress, for example, it’s as simple as <a href="https://activitypub.blog/2026/05/20/atmosphere-1-0-0-liftoff/">using a plugin</a>. There’s an <a href="https://npmx.dev/package/@bryanguffey/astro-standard-site">Astro plugin</a> too, along with all of your favourites (I imagine).</p>



<p>I’ll write up the details as part of my <a href="https://piccalil.li/projects/personal-site/">ongoing series</a>. I’ll definitely be getting on that towards the end of the month too as my <a href="https://set.studio/">studio</a> workload quietens down a bit.</p>



<p>Was it all a bit of a faff to integrate? Yes. Did I get rate limited multiple times? Absolutely. But do I think it’s worth doing? I think Standard.site is going to do <em>wonders</em> for <strong>website</strong> discovery, so without a doubt.</p>



<p></p>
]]></description>
      <pubDate>Fri, 05 Jun 2026 16:27:41 +0000</pubDate>
      <link>https://bell.bz/i-did-the-standard-site-thing/</link>
      <dc:creator>Andy Bell</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5249936997</guid>
    </item>
    <item>
      <title><![CDATA[Understanding Standard.Site]]></title>
      <description><![CDATA[
        
          I tell you, I have high hopes for this one. I hope it sticks.
        
        
				  <p>I don’t mind saying that the first thing that sprung to mind when I saw the recent chatter about <a href="https://standard.site" rel="noopener">Standard.Site</a> was:</p>
<blockquote>
<p>“At last,” said the most tired-looking man you have ever seen in your life. “A standard for fancy little social media preview cards.”</p>
</blockquote>
<p>Well, that wasn’t entirely fair. For a lot of people, the immediate appeal is a shiny new preview card when posting a link to a service that runs on the AT Protocol, and Bluesky being the biggest and most visible of such services means that this feature landed as a kind of novelty — a way to add a themed button to links <em>posted to Bluesky</em>. I assumed this would be in exchange for yet another thing crammed into the <code>head</code> of a document alongside all the <code>meta</code> tags for <a href="https://www.opengraph.xyz/" rel="noopener">Open Graph</a>, defunct Twitter cards, and whatever nonsense LinkedIn or Pinterest or LiveJournal or Friendster or <em>whatever</em> currently expects of us.</p>
<p>There’s a more to it than that, both technically and conceptually — and potentially quite a bit more to the latter for us die-hard <a href="https://www.youtube.com/watch?v=X3SrZuH00GQ&amp;t=835s" rel="noopener">Publish [on your] Own Site, Syndicate Elsewhere</a> (POSSE) enthusiasts in the years to come.</p>
<p>As it stands now, there’s a necessarily manual step in the POSSE process when it comes to social media: the syndication efforts fall to you. If you’re doing it right, you’ve rigged up some kind of process that posts links to your work to various social media sites for you — as long as you have an account on all of them, and none of their APIs change, and you didn’t get rate-limited, and you didn’t forget any. If you’re doing it like <em>me</em>, you’re copying and pasting your freshly-published link into <code>textarea</code>s provided by four different websites, one of which contains a typo, and the fifth of which you forgot about until a few days later. In either case, this syndication step is <em>effortful</em> — a necessary step that needs to be taken to broadcast your work to the world, wholly dependent on you doing it, and you knowing where your potential audiences are. For example, I don’t post anything I write on Twitter due to my having self-respect, so nothing I write appears there, full stop. I have no doubt that there are a half-dozen other social media websites out there where I don’t have an account — or ones I straight-up don’t know about — where I certainly wouldn’t <em>mind</em> seeing my work appear, but it likewise does not. No matter how robust or mechanically-streamlined your approach to POSSE might be, the “SE” part of the equation still involves <em>work</em> for the part where you chuck “social cards” at various and sundry services like social media Gambit, even if your publishing process does that work for you.</p>
<p>Now, compare this pattern to good ol’ RSS, darling of POSSE enthusiasts the world over — you publish your post, you publish a <em>feed</em> that contains that post, and the contents of that feed are out there in the world for sites and services to consume. At no point do you need to “remember” to post the title and a link to, say, Feedly — someone using that service has subscribed to your site, so that post appears in their feed. You own your platform, you own your content, the end user decides on the medium.</p>
<p>Standard.Site aims to do something similar for any platform powered by the <a href="https://atproto.com/" rel="noopener">AT Protocol</a>. The Standard.Site schema provides you with a set of <strong>lexicons</strong> you use to describe your website and the individual pages and posts on it in a way where they can be parsed by a service that runs on the AT Protocol — an “App View,” in the language of their standard — which would allow users to subscribe, via that service, to <em>your domain</em>. The most obvious use case is allowing users on a platform like Bluesky to subscribe to an RSS-like feed of, say, blog posts, the way they would follow any other account on the service. If you tease that use case out just a little from there, it paves the the way for microblogging <em>powered</em> by your own website, with the content hosted and fully owned by you. If a specific platform used to subscribe to your website becomes, say, too morally reprehensible to stomach, or they trip over a network cable and spill a big bowl of spaghetti all over their servers, or both, the result is a situation more “end of Google Reader” than “end of Twitter” for you and for anyone consuming your content. Anyone can subscribe to your website — where all your content lives — via any AT-Protocol-powered service, since they’re all speaking the same fundamental language.</p>
<p>It all sounds great on paper for sure, but I had the same kind of “own everything, syndicate everywhere” hopes for Mastodon, too — I still kinda do, truth be told. I set up my own little Fediverse test server back at the height of Mastodon and, no, not for me; I like using it just fine, but owning it all was too much of a lift. I want to own my content, but I don’t want to own <em>running an instance</em> — something with its own UI, something with uploads in need of managing, something my experience of a larger network is routed through, something in need of privacy controls and preferences and settings and <em>management</em>. I put borders on stuff for a living. I just want a <em>website</em>; I want <em>my</em> website. In theory, that’s the intent of AT Protocol, as I understand it: my website, used and updated the way I want to, acting as an API.</p>
<p>Also, okay, yes, in the much nearer term, I want a neat little preview card with a theme-able button. Of <em>course</em> I want a neat little preview card with a theme-able button. I’m not made of <em>stone</em>.</p>
<p>So, my goal was to tinker with the steps outlined in the <a href="https://standard.site/docs/quick-start/" rel="noopener">Standard.Site quick-start guide</a> with as little overhead as possible, and that meant reusing the Personal Data Server (PDS) already <a href="https://pdsls.dev/at://did:plc:mlmtsmr3fcbhffjoydyg3663" rel="noopener">provided to me by Bluesky</a> rather than <a href="https://github.com/bluesky-social/pds" rel="noopener">spinning up my own</a> — not the <em>long</em>-term plan, but enough to get a feel for things. I pulled myself up on <a href="https://pdsls.dev/" rel="noopener">Atmosphere Explorer</a>, logged in, and added a record for my “publication” — my website — named <code>site.standard.publication</code>:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"$type"</span><span class="token operator">:</span> <span class="token string">"site.standard.publication"</span><span class="token punctuation">,</span>
  <span class="token property">"url"</span><span class="token operator">:</span> <span class="token string">"https://wil.to"</span><span class="token punctuation">,</span>
  <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Wil.to"</span><span class="token punctuation">,</span>
  <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"My name is Mat Marquis, and I believe in the web."</span><span class="token punctuation">,</span>
  <span class="token property">"preferences"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"showInDiscover"</span><span class="token operator">:</span> <span class="token boolean">true</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Check.</p>
<p>Then, over here on the site, I spun up a new page pointing to that newly-created record — <code>did:plc:mlmtsmr3fcbhffjoydyg3663</code> being my account, <code>site.standard.publication</code> being the (standardized) name of the record, and <code>3mnf7wqnkaf2b</code> being the generated key for the publication record (placeholdered in the Standard.Site documentation as <code>rkey</code>):</p>
<pre class="language-md"><code class="language-md"><span class="token front-matter-block"><span class="token punctuation">---</span>
<span class="token front-matter yaml language-yaml"><span class="token key atrule">permalink</span><span class="token punctuation">:</span> /.well<span class="token punctuation">-</span>known/site.standard.publication</span>
<span class="token punctuation">---</span></span>
at://did:plc:mlmtsmr3fcbhffjoydyg3663/site.standard.publication/3mnf7wqnkaf2b</code></pre>
<p>And to <a href="https://standard.site/docs/verification/#discovery-hint" rel="noopener">cover my bases</a>, added a <code>link</code> to the <code>head</code> of my site:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span>
  <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>site.standard.publication<span class="token punctuation">"</span></span>
  <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>at://did:plc:mlmtsmr3fcbhffjoydyg3663/site.standard.publication/3mnf7wqnkaf2b<span class="token punctuation">"</span></span>
<span class="token punctuation">/&gt;</span></span></code></pre>
<p>That’s the easy part done. So far as AT Protocol is concerned, my website now exists as a publication with no documents. So, just to get my bearings, I’ll start by adding a document manually: <a href="https://wil.to/posts/standard-site/">this very post</a>. Same process as adding a publication record — I hop over to <a href="https://pdsls.dev/" rel="noopener">Atmosphere Explorer</a> and create a new record named <code>site.standard.document</code>:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"$type"</span><span class="token operator">:</span> <span class="token string">"site.standard.document"</span><span class="token punctuation">,</span>
  <span class="token property">"site"</span><span class="token operator">:</span> <span class="token string">"at://did:plc:mlmtsmr3fcbhffjoydyg3663/site.standard.publication/3mnf7wqnkaf2b"</span><span class="token punctuation">,</span>
  <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"Implementing Standard.Site"</span><span class="token punctuation">,</span>
  <span class="token property">"path"</span><span class="token operator">:</span> <span class="token string">"/posts/standard-site"</span><span class="token punctuation">,</span>
  <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"I tell you, I have high hopes for this one. I hope it sticks."</span><span class="token punctuation">,</span>
  <span class="token property">"publishedAt"</span><span class="token operator">:</span> <span class="token string">"2026-06-03T15:26:19.516Z"</span>
<span class="token punctuation">}</span></code></pre>
<p>At time of writing, the docs have a record key in place of an <code>rkey</code> placeholder, so watch out for that — that <code>site</code> property should end with the record key for the publication record you created earlier. I’m not including the <code>textContent</code> property mentioned in the documentation, because that’s not something I want to copy and paste and un-format for a test run; I’ll want that eventually though, once this process is automated, so I could theoretically syndicate entire posts.</p>
<p>The next step calls for a <code>link</code> in the head of the associated document:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span>
  <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>site.standard.document<span class="token punctuation">"</span></span>
  <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>at://did:plc:mlmtsmr3fcbhffjoydyg3663/site.standard.document/3mnfcoxoqsy2w<span class="token punctuation">"</span></span>
<span class="token punctuation">/&gt;</span></span></code></pre>
<p>Can’t just drop that in across the board if I want it associated with the <em>one</em> post, so a quick <a href="https://jargondb.org/glossary/kludge" rel="noopener">kludge</a> to get things minimally-viable:</p>
<pre class="language-html"><code class="language-html">{% if permalink ==  "/posts/standard-site/index.html" %}
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span>
  <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>site.standard.document<span class="token punctuation">"</span></span>
  <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>at://did:plc:mlmtsmr3fcbhffjoydyg3663/site.standard.document/3mnfcoxoqsy2w<span class="token punctuation">"</span></span>
<span class="token punctuation">/&gt;</span></span>
{% endif %}</code></pre>
<p>I push all that up to production, fire up the <a href="https://site-validator.fly.dev/" rel="noopener">Standard.Site validator</a>, quietly fix the one place where I had left a stray <code>rkey</code> placeholder, push to production again, <em>et voila</em>: a new preview card.</p>
<p></p><figure><picture><source type="image/webp" srcset="https://wil.to/posts/standard-site/jNcbiOzR_t-650.webp 650w" sizes="auto"><img loading="lazy" decoding="async" src="https://wil.to/posts/standard-site/jNcbiOzR_t-650.jpeg" alt="Cropped screenshot of the " new="" post"="" window="" on="" bluesky,="" containing="" a="" link="" to="" this="" post.="" below="" the="" textarea="" is="" preview="" of="" an="" open="" graph="" thumbnail="" and="" brand="" "view="" publication"="" button"="" title="I wouldn’t be doing this for a living if I wasn’t a sucker for theming buttons." width="650" height="646"></picture><figcaption>I wouldn’t be doing this for a living if I wasn’t a sucker for theming buttons.</figcaption></figure><p></p>
<p>Working as a prototype, at least. As it stands now, that preview card is a mish-mash of Open Graph and Standard.Site — the thumbnail, for example, is coming from the former. The Standard.Site <a href="https://standard.site/docs/lexicons/document/" rel="noopener">document lexicon</a> covers things like the thumbnail (<code>coverImage</code>), of course — Bluesky is aware of the thumbnail thanks to the <code>og:image</code>, but anything parsing this as-syndicated across the I-have-too-much-dignity-to-say-“ATmosphere” wouldn’t necessarily pick that up. I’ll need to get that situated at some point.</p>
<p>More pressingly, this is <em>entirely manual</em>, just to get my bearings — only this one document is set up with a <code>site.standard.document</code> record, copied-and-pasted into place. I’ll need to wire something up so that anything I publish here includes a step where an associated <code>site.standard.document</code> is created via API. That permalink string-check for the <code>meta</code> tag has to go, too. In the grand scheme, I’d want to move this off of the Bluesky PDS and fire up my own PDS, on my own server, of course.</p>
<p>But more than any of that, I think we can all agree on the most important part of this process, which takes precedence above such petty issues as “automation” or “maintainability.” <em>I gotta theme that button</em>:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"url"</span><span class="token operator">:</span> <span class="token string">"https://wil.to"</span><span class="token punctuation">,</span>
  <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Wil.to"</span><span class="token punctuation">,</span>
  <span class="token property">"$type"</span><span class="token operator">:</span> <span class="token string">"site.standard.publication"</span><span class="token punctuation">,</span>
  <span class="token property">"basicTheme"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"$type"</span><span class="token operator">:</span> <span class="token string">"site.standard.theme.basic"</span><span class="token punctuation">,</span>
    <span class="token property">"accent"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"b"</span><span class="token operator">:</span> <span class="token number">239</span><span class="token punctuation">,</span>
      <span class="token property">"g"</span><span class="token operator">:</span> <span class="token number">65</span><span class="token punctuation">,</span>
      <span class="token property">"r"</span><span class="token operator">:</span> <span class="token number">54</span><span class="token punctuation">,</span>
      <span class="token property">"$type"</span><span class="token operator">:</span> <span class="token string">"site.standard.theme.color#rgb"</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"background"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"b"</span><span class="token operator">:</span> <span class="token number">252</span><span class="token punctuation">,</span>
      <span class="token property">"g"</span><span class="token operator">:</span> <span class="token number">252</span><span class="token punctuation">,</span>
      <span class="token property">"r"</span><span class="token operator">:</span> <span class="token number">252</span><span class="token punctuation">,</span>
      <span class="token property">"$type"</span><span class="token operator">:</span> <span class="token string">"site.standard.theme.color#rgb"</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"foreground"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"b"</span><span class="token operator">:</span> <span class="token number">63</span><span class="token punctuation">,</span>
      <span class="token property">"g"</span><span class="token operator">:</span> <span class="token number">7</span><span class="token punctuation">,</span>
      <span class="token property">"r"</span><span class="token operator">:</span> <span class="token number">18</span><span class="token punctuation">,</span>
      <span class="token property">"$type"</span><span class="token operator">:</span> <span class="token string">"site.standard.theme.color#rgb"</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"accentForeground"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
      <span class="token property">"b"</span><span class="token operator">:</span> <span class="token number">252</span><span class="token punctuation">,</span>
      <span class="token property">"g"</span><span class="token operator">:</span> <span class="token number">252</span><span class="token punctuation">,</span>
      <span class="token property">"r"</span><span class="token operator">:</span> <span class="token number">252</span><span class="token punctuation">,</span>
      <span class="token property">"$type"</span><span class="token operator">:</span> <span class="token string">"site.standard.theme.color#rgb"</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"My name is Mat Marquis, and I believe in the web."</span><span class="token punctuation">,</span>
  <span class="token property">"preferences"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"showInDiscover"</span><span class="token operator">:</span> <span class="token boolean">true</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Now we’re talking.</p>

        
 			]]></description>
      <pubDate>Wed, 03 Jun 2026 00:00:00 +0000</pubDate>
      <link>https://wil.to/posts/standard-site/</link>
      <dc:creator>Wil.to</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5247489868</guid>
    </item>
    <item>
      <title><![CDATA[Context-aware headings in HTML]]></title>
      <description><![CDATA[
                                <p>A common issue that we've probably all faced at some point is having a component that includes a heading, which sometimes should be an H2 and sometimes maybe an H3, depending on where it's used.</p><img alt="Part of a page about museums. h1 followed by an image and a h2, paragraph, and link, followed by an h2 and a pair of h3s, paragraphs, and links." height="527" loading="eager" src="https://www.matuzo.at/media/pages/blog/2026/content-aware-headings/aea2b5f61b-1780687565/vienna.avif" width="864">
<p>We solved this by giving editors an option to pick the appropriate heading level in the CMS. That works, but it means more work for them, because they need to keep track of the document outline, and it's also a potential source of error.</p>
<h2 id="the-headingoffset-attribute">The headingoffset attribute</h2>
<p>Yesterday, I heard about a new experimental attribute that offers a better solution to this problem. The <a href="https://html.spec.whatwg.org/multipage/sections.html#heading-levels-&amp;-offsets">heading offset content attribute</a> allows us to offset heading levels for descendants.<br>
Let's take the outline from the page in the screenshot and see how we can make it more context-aware.</p>
<pre><code class="language-html">&lt;h1&gt;Museums&lt;/h1&gt;
&lt;section&gt;
  &lt;h2&gt;Free entry to the museum&lt;/h2&gt;
&lt;/section&gt;
&lt;section&gt;
  &lt;h2&gt;All museums and collections&lt;/h2&gt;
  &lt;h3&gt;Aphabetical&lt;/h3&gt;
  &lt;h3&gt;By district&lt;/h3&gt;
&lt;/section&gt;</code></pre>
<p>First, we turn all headings into H1s. Then we add the <code>headingoffset</code> attribute with a value of 1 to every parent element where we want to increase the heading level by 1.</p>
<pre><code class="language-html">&lt;h1&gt;Museums&lt;/h1&gt; &lt;!-- heading level 1 --&gt;
&lt;section headingoffset="1"&gt;
  &lt;h1&gt;Free entry to the museum&lt;/h1&gt; &lt;!-- heading level 2 --&gt;
&lt;/section&gt;
&lt;section headingoffset="1"&gt;
  &lt;h1&gt;All museums and collections&lt;/h1&gt; &lt;!-- heading level 2 --&gt;
  &lt;div class="cards" headingoffset="1"&gt;
    &lt;h1&gt;Aphabetical&lt;/h1&gt; &lt;!-- heading level 3 --&gt;
    &lt;h1&gt;By district&lt;/h1&gt; &lt;!-- heading level 3 --&gt;
  &lt;/div&gt;
&lt;/section&gt;</code></pre>
<p>The heading offset attribute increases the current heading level by the specified number; in this case, by 1 (h1 + 1 = h2). The H1s inside the sections become H2s and the H1s nested in another heading offset container become H3s.</p>
<img alt="Firefox Dev Tools screenshot. The headings look correct and have the correct heading level." height="666" loading="lazy" src="https://www.matuzo.at/media/pages/blog/2026/content-aware-headings/d8e06d275d-1780687565/firefox.avif" width="864">
<p>In the accessibility panel in Firefox, you can see that the tag for the “All museums and collections” heading is H1, but the level is 2.</p>
<p>Now that we know how it works, let's do a quick Q&amp;A based on the questions I had when I first saw this.</p>
<h2 id="on-a-really-large-page-i-would-need-a-bachelor-s-degree-in-maths-to-create-the-document-outline-is-using-an-h1-for-everything-re">On a really large page, I would need a bachelor's degree in maths to create the document outline. Is using an h1 for everything really a good idea?</h2>
<p>I don't think so.</p>
<p>I've been waiting for this feature for years. I think it's fantastic and super helpful, but the example I showed you is not necessarily the best use case. I probably wouldn't use heading offsets for static HTML documents, because the document is much easier to read if the tag name represents the actual heading level.</p>
<p>It gets really interesting when you don't have immediate access to the headings or control over the page's structure. For example, when you're working with components.</p>
<p>Here's an example of the same structure built with a web component. When we write the markup, we don't see the heading tags because the web component renders them for us.</p>
<pre><code class="language-html">&lt;the-section heading="Free entry to the museum"&gt;&lt;/the-section&gt;
&lt;the-section heading="All museums and collections"&gt;
    &lt;the-section heading=""&gt;
        &lt;div class="cards"&gt;
          &lt;h1&gt;Aphabetical&lt;/h1&gt; &lt;!-- heading level 3 --&gt;
          &lt;h1&gt;By district&lt;/h1&gt; &lt;!-- heading level 3 --&gt;
        &lt;/div&gt;
    &lt;/the-section&gt;
&lt;/the-section&gt;</code></pre>
<pre><code class="language-js">class Section extends HTMLElement {
  static observedAttributes = ["heading"];

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'heading') {
      this.shadowRoot.innerHTML = `
      &lt;section headingoffset="1"&gt;
        ${newValue != '' ? `&lt;h1&gt;${newValue}&lt;/h1&gt;` : ''}
        &lt;slot&gt;&lt;/slot&gt;
      &lt;/section&gt;
      `
    }
  }

constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }
}

customElements.define("the-section", Section);</code></pre>
<p>For the page I showed at the very beginning of this blog post, this would be perfect because we, as developers, have little to no control over how editors will use the components.</p>
<h2 id="can-i-opt-out-of-the-algorithm-for-specific-headings">Can I opt out of the algorithm for specific headings?</h2>
<p>Yes, by using the <code>headingreset</code> attribute on a parent or the heading itself.</p>
<pre><code class="language-html">&lt;h1&gt;Museums&lt;/h1&gt;
&lt;the-section heading="Free entry to the museum"&gt;&lt;/the-section&gt;
&lt;the-section heading="All museums and collections"&gt;
  &lt;the-section heading=""&gt;
    &lt;div class="cards" headingreset&gt;
      &lt;h3&gt;Aphabetical&lt;/h3&gt; &lt;!-- heading level 3, not affected by the headingoffset --&gt;
      &lt;h3&gt;By district&lt;/h3&gt; &lt;!-- heading level 3, not affected by the headingoffset --&gt;
    &lt;/div&gt;
  &lt;/the-section&gt;
&lt;/the-section&gt;</code></pre>
<h2 id="do-i-always-have-to-use-h1">Do I always have to use H1?</h2>
<p>No, you can use whatever you want, but in our example, h2s would become h3s.</p>
<pre><code class="language-html">&lt;h1&gt;Museums&lt;/h1&gt;
&lt;section headingoffset="1"&gt;
  &lt;h2&gt;Free entry to the museum&lt;/h2&gt; &lt;!-- heading level 3 --&gt;
&lt;/section&gt;
…</code></pre>
<h2 id="what-happens-if-you-set-the-value-to-2">What happens if you set the value to 2?</h2>
<p>The h2 becomes an h3 because h1 +2 = h3.</p>
<pre><code class="language-html">&lt;h1&gt;Museums&lt;/h1&gt;
&lt;section headingoffset="2"&gt;
  &lt;h1&gt;Free entry to the museum&lt;/h1&gt; &lt;!-- heading level 3 --&gt;
&lt;/section&gt;</code></pre>
<h2 id="what-happens-if-you-set-the-value-to-14">What happens if you set the value to 14?</h2>
<p>The h1 turns into a heading level 9. I'm not exactly sure why, but the maximum offset is 8, and if the heading level is greater than 9, it always returns 9.</p>
<pre><code class="language-html">&lt;h1&gt;Museums&lt;/h1&gt;
&lt;section headingoffset="14"&gt;
  &lt;h1&gt;Free entry to the museum&lt;/h1&gt; &lt;!-- heading level 9 --&gt;
&lt;/section&gt;</code></pre>
<h2 id="can-i-use-a-negative-number-for-the-offset">Can I use a negative number for the offset?</h2>
<p>No.</p>
<h2 id="how-can-i-style-an-h3-when-there-s-no-h3-tag">How can I style an H3 when there's no H3 tag?</h2>
<p>You can use the headings pseudo-class, which is currently only supported by Firefox Nightly.</p>
<pre><code class="language-css">:heading(3) {
  color: fuchsia;
}</code></pre>
<h2 id="do-screen-readers-support-it">Do screen readers support it?</h2>
<p>I don't think screen readers need to support anything; if the information is correct in the accessibility tree, they'll read the heading levels accordingly. I tested it with VoiceOver and it always announced the correct heading levels.</p>
<h2 id="how-s-browser-support">How's browser support?</h2>
<p>This feature is brand new and currently available only in Firefox Nightly, behind a flag. If you want to test it, type <code>about:config</code> into the address bar, proceed to the next step, search for <code>dom.headingoffset.enabled</code>, and enable it.</p>
<p>If you like this feature, then make some noise and encourage other browser vendors to implement it.</p>
<h2 id="conclusion">Conclusion</h2>
<p><a href="https://html.spec.whatwg.org/multipage/sections.html#heading-levels-&amp;-offsets">Heading offset</a> is a long-awaited feature, and I hope it will soon make its way into other browsers as well. The implementation, as it stands, seems very sensible to me. Thanks, Keith, et al.</p><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CContext-aware+headings+in+HTML%E2%80%9D">blog@matuzo.at</a>.</p>        ]]></description>
      <pubDate>Wed, 03 Jun 2026 12:53:17 +0000</pubDate>
      <link>https://www.matuzo.at/blog/2026/content-aware-headings</link>
      <dc:creator>Manuel Matuzović - Blog</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5247264505</guid>
    </item>
    <item>
      <title><![CDATA[Did your editor font go default serif on WordPress 7.0?]]></title>
      <description><![CDATA[
<p class="wp-block-paragraph">Mine did. Like this:</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="290" data-attachment-id="13589" data-permalink="https://chriscoyier.net/2026/05/27/did-your-editor-font-go-default-serif-on-wordpress-7-0/screenshot-2026-05-27-at-7-25-31-am/" data-orig-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/Screenshot-2026-05-27-at-7.25.31-AM.png?fit=1666%2C472&amp;ssl=1" data-orig-size="1666,472" 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;,&quot;alt&quot;:&quot;&quot;}" data-image-title="Screenshot 2026-05-27 at 7.25.31 AM" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/Screenshot-2026-05-27-at-7.25.31-AM.png?fit=1024%2C290&amp;ssl=1" src="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/Screenshot-2026-05-27-at-7.25.31-AM.png?resize=1024%2C290&amp;ssl=1" alt="Text asking if the editor font changed to default serif in WordPress 7.0" class="wp-image-13589" srcset="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/Screenshot-2026-05-27-at-7.25.31-AM.png?resize=1024%2C290&amp;ssl=1 1024w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/Screenshot-2026-05-27-at-7.25.31-AM.png?resize=300%2C85&amp;ssl=1 300w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/Screenshot-2026-05-27-at-7.25.31-AM.png?resize=768%2C218&amp;ssl=1 768w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/Screenshot-2026-05-27-at-7.25.31-AM.png?resize=1536%2C435&amp;ssl=1 1536w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/Screenshot-2026-05-27-at-7.25.31-AM.png?w=1666&amp;ssl=1 1666w" sizes="auto, (max-width: 1000px) 100vw, 1000px"><figcaption class="wp-element-caption">so meta.</figcaption></figure>



<p class="wp-block-paragraph">I guess WordPress 7.0 assumes you are using a <code>theme.json</code> file these days. I’m not doing that yet on any of the sites I work on. If you want to embrace that future, you could add a <code>theme.json</code> file to the root of your theme, and put some typography basics only in there:</p>


<pre class="wp-block-code"><span><code class="hljs language-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://schemas.wp.org/trunk/theme.json"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-number">2</span>,
  <span class="hljs-attr">"settings"</span>: {
    <span class="hljs-attr">"appearanceTools"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"layout"</span>: {
      <span class="hljs-attr">"contentSize"</span>: <span class="hljs-string">"720px"</span>
    },
    <span class="hljs-attr">"typography"</span>: {
      <span class="hljs-attr">"fontFamilies"</span>: [
        {
          <span class="hljs-attr">"fontFamily"</span>: <span class="hljs-string">"system-ui, sans-serif"</span>,
          <span class="hljs-attr">"name"</span>: <span class="hljs-string">"System Font"</span>,
          <span class="hljs-attr">"slug"</span>: <span class="hljs-string">"system-font"</span>
        }
      ]
    }
  },
  <span class="hljs-attr">"styles"</span>: {
    <span class="hljs-attr">"typography"</span>: {
      <span class="hljs-attr">"fontFamily"</span>: <span class="hljs-string">"var(--wp--preset--font-family--system-font)"</span>,
      <span class="hljs-attr">"lineHeight"</span>: <span class="hljs-string">"1.75"</span>
    }
  }
}
</code></span></pre>


<p class="wp-block-paragraph">That will be much nicer than no styles at all.</p>



<p class="wp-block-paragraph">Or if you don’t want to mess with that you can go to the upper-right three-dot menu (“Option”), choose <strong>Preferences</strong> then go to the <strong>Appearance</strong> tab and make sure <strong>Use theme styles</strong> is unchecked.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="629" data-attachment-id="13590" data-permalink="https://chriscoyier.net/2026/05/27/did-your-editor-font-go-default-serif-on-wordpress-7-0/cleanshot-2026-05-27-at-07-28-302x/" data-orig-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/CleanShot-2026-05-27-at-07.28.30%402x.png?fit=1472%2C904&amp;ssl=1" data-orig-size="1472,904" 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;,&quot;alt&quot;:&quot;&quot;}" data-image-title="CleanShot 2026-05-27 at 07.28.30@2x" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/CleanShot-2026-05-27-at-07.28.30%402x.png?fit=1024%2C629&amp;ssl=1" src="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/CleanShot-2026-05-27-at-07.28.30%402x.png?resize=1024%2C629&amp;ssl=1" alt="Settings interface showing 'Appearance' options with toggles for 'Top toolbar', 'Distraction free', 'Spotlight mode', and 'Use theme styles' marked with an arrow." class="wp-image-13590" srcset="https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/CleanShot-2026-05-27-at-07.28.30%402x.png?resize=1024%2C629&amp;ssl=1 1024w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/CleanShot-2026-05-27-at-07.28.30%402x.png?resize=300%2C184&amp;ssl=1 300w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/CleanShot-2026-05-27-at-07.28.30%402x.png?resize=768%2C472&amp;ssl=1 768w, https://i0.wp.com/chriscoyier.net/wp-content/uploads/2026/05/CleanShot-2026-05-27-at-07.28.30%402x.png?w=1472&amp;ssl=1 1472w" sizes="auto, (max-width: 1000px) 100vw, 1000px"></figure>



<p class="wp-block-paragraph">I think it’s <em>sorta</em> neat that you could use the same typography setup for both the front-end and back of your site this way, and if you use <code>theme.json</code> for the front, might as well use it for the back. But I also personally don’t mind if the back-end is entirely default styles. It’s a reminder that you’re in a CMS, and content is data, not WYSIWYG. </p>
]]></description>
      <pubDate>Wed, 27 May 2026 14:39:04 +0000</pubDate>
      <link>https://chriscoyier.net/2026/05/27/did-your-editor-font-go-default-serif-on-wordpress-7-0/</link>
      <dc:creator>Chris Coyier</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5239860855</guid>
    </item>
    <item>
      <title><![CDATA[LLemdashes]]></title>
      <description><![CDATA[
        
          It was only a matter of time before I caught a stray — you know, <em>considering</em>.
        
        
				  <p>I know this is gonna sound a little navel-gazesque from the jump, but yesterday I had something I’d written dismissed as the “work” of an LLM. According to their comment, the reader saw a bunch of emdashes up-front and bailed on the rest of the article. If you can believe it, this was the first such comment I’ve gotten, despite the hundreds of thousands of words — and almost as many emdashes, case in point — that I’ve written over the past few years. I won’t link to it here because I wouldn’t want anyone piling onto a thread on an unrelated post, and because I don’t really fault the person for saying it; leave 'em be.</p>
<p>For one thing, I <em>do</em> use too many emdashes. Listen, I read a bunch of Kerouac a long time ago. I don’t apologize for the emdashes; I do apologize for my reading habits back when I was twenty-<em>mumble</em> years old, but that’s only semi-related.</p>
<p>Mostly though, I don’t fault them for the impulse that drives that kind of comment. Hell, I agree with it: generated text, assembled by nobody and increasingly <em>for</em> nobody, <em>should</em> be dismissed. Even leaving aside the lack of craft, dubious accuracy, and insultingly cloying and sycophantic tones: why should anyone care about reading something nobody cared enough to write? If a publication is willing to pad itself out with the prose equivalent of <a href="https://en.wikipedia.org/wiki/Circus_peanut" rel="noopener">Circus Peanuts</a> — to take you, the reader, for an easy mark — I agree that you should hold them in lower esteem, if not outright <em>scorn</em>. If I thought I was being tricked into eating a meal stretched with sawdust, I’d be left feeling angry and dismissive of the meal, chef, and restaurant too. I’ve been there — reading-wise, I mean, not eating-wise. I know that “hah, gotcha; they’ll never put one over on me” feeling. It feels ~good to catch on to the fact that you’re being tricked, the way it feels ~good to prod at an aching tooth. It could be that I’ve been wrong about it before too — I bet I have been, but I’ll never know. I can’t know, by design.</p>
<p>The only “advancement” in generative AI, since its inception, has been in slowly getting better at fooling us into thinking its output <em>isn’t</em> generative AI. It is software designed to put strings in the most statistically likely order that an average human would. It is meant to trick us, at its core; an engine with “passable sentence” as its sole ideal output. Why else would it make us so angry? Why else would so many of us want to lash out at the sight of it, whether real or imagined? Why else would we “boo” these hollow-eyed, out-of-touch ghouls that try to advertise their “AI” investments during commencement speeches? Hell yeah; <em>good</em>. Send a message — call it out for what it is, and dismiss it loudly and in no uncertain terms.</p>
<p>Which is how my work got dismissed. It stung — it <em>stings</em>, sure — I mean, c’mon, same team — but I get it. I really do. I mean, <em>look</em> at the start of this paragraph, which I’m now refusing to go back and edit for illustrative purposes. Hell, <em>I’m</em> starting to suspect me a little. Emdashes have come to represent the big glowing weak point on the boss monster that threatens to consume the livelihood of so many of us; something recognizable, from a hundred paces, as a sign that something <em>is</em> dismissable. Of <em>course</em> we’re gonna attack it on sight. I <em>also</em> want to take a swipe at the <a href="https://en.wikipedia.org/wiki/Gray_goo" rel="noopener">grey goo</a> threatening to smother both the web that I’ve worked so hard to build and the career I’ve been so fortunate to have.</p>
<p>But my <em>second</em> reaction, after the flash of “the robots can have my emdashes when they pry them from my cold, dead hands” anger and just a <em>whisper</em> of “what’s the point of even writing” was that I’m privileged in that I <em>can</em> shrug this off. I am <em>established</em> — I’m <em>here</em>, and you’re reading this now, for some wild reason. I’m “a long time out from reading Kerouac,” as we emdashers of a certain age call it — forty-<em>mumble</em> years old, nearly half of which was spent <em>here, doing this</em>. I’ve done a lot of writing over a lot of years, and I’ve been fortunate enough to get it in front of a lot of people. I’m not the thing that gets us out of this mess. I <em>can’t</em> be.</p>
<p>These tools, to the extent they can or should ever be personified, want rid of me pretty bad — but not half as badly as they want to prevent another one of me from ever happening again. The explicit goal of generative AI is to fill the role of “junior-level” <em>everything</em> the way insulating spray-foam in a car’s exhaust pipe might help prevent excessive engine noise. This technology exists to keep entry-level workers pruned all the way back to the roots, in the interest of keeping wages low, employment tenuous, staff nervous, and the unfathomably rich insulated from the potential financial repercussions of destroying countless lives. If you can never advance beyond “press the button, generate the thing,” you’re as replaceable as the next person — if there’s nothing beyond entry-level experience, they’ll never have to pay you more than an entry-level salary. They’ll destroy you, me, the web, the environment — anything but their valuations — to finally realize their long-held dream of “entry level position, five years of experience required.”</p>
<p>That’s why I’ve been writing so much more lately. That’s why I spun up this blog, finally, for-realsies, as small as its voice is. That’s why I’ve been trying to eke out more of a living writing courses and articles and <em>picking fights about web standards, a thing I swore I would never do again</em>, despite that being exceptionally difficult to do in this climate; despite the constant, crushing discouragement — a topic for another post altogether.</p>
<p>I’m doing this because I <em>also</em> want to take a swipe at these garbage machines, and this is the only infinitesimally small way that I think I could: by helping people to learn and improve out of spite for the people who’ve foisted these machines upon us, and would see the world burn to fuel them. I want us to want <em>more</em> — more than “press the button to generate the thing,” more than a life of “junior-level.” I want to chip at the wall they’re trying to build between you and “more.” I want new voices sharing what they know and what they’re learning — they’re doing so against <em>unfathomable</em> odds, in a climate far more difficult than things were back when I first clawed my way here. I’m barely hanging on here, and again: easiest difficulty setting. Don’t risk making things even <em>harder</em> for them.</p>
<blockquote>
<p>For anyone stumbling on this thread, this post unequivocally [was not generated by an LLM], either in whole or in part. I do not use LLMs, full stop.</p>
<p>Now, whether or not you personally believe that is your business. What I would ask you to do, though, is imagine that an author might be someone that bristles every bit as much as you do at the idea of technical writing — something they do professionally, for personal fulfillment, or both — being systemically devalued by these tools. Imagine then having their work dismissed as slop; imagine the kind of “why bother” message that would send to an author less established than I am. Imagine your comment on a junior developer’s proud first post.</p>
<p>I agree that you shouldn’t value generated text. You don’t have to value my writing either, honestly — plenty don’t, I’m sure. But if you value real, human writing the way I think you do, I’d ask you to please be more careful about dismissing it as the very thing trying to rob it of value. I’m not going anywhere, but a brand new voice might, and we need those <em>desperately</em> right now.</p>
</blockquote>
<p>I know you’re angry; I am too. I know you want better for us too — don’t let that anger risk silencing new voices, though. Don’t let it make you think that you’ve stumbled onto some secret, glandular instinct that makes you — and you alone — immune to something specifically engineered — to the tune of <em>trillions</em> of dollars — to trick you. Do not risk silencing even <em>one</em> new voice — our only way through this mess — just to take an easy and self-satisfying swing at a <code>switch</code> statement that can’t hear you anyway. Some of us just write this way.</p>
<p>There are real people deserving of that anger. Save it for them. Make sure they’re the ones that hear it. Say it to their faces as you climb over them.</p>

        
 			]]></description>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <link>https://wil.to/posts/llemdashes/</link>
      <dc:creator>Wil.to</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5232033588</guid>
    </item>
    <item>
      <title><![CDATA[Maybe Don’t Rely on Google’s “Modern Web Guidance”]]></title>
      <description><![CDATA[<p>
Just in time for Google I/O, the Chrome for Developers site announced <em>Modern Web Guidance</em> (MWG):
</p>

<span id="LanternWrap"><img loading="lazy" src="https://adrianroselli.com/wp-content/uploads/2026/05/modern-web-guidance.svg" alt="Orange star between two curly braces, topped with a circle-line and a flat line at the bottom, appearing almost as an ASCII art lantern." width="300" height="300" class="right"></span>

<blockquote>
 <p>
 Modern Web Guidance is a set of evergreen and expert-vetted skills that guide your AI coding agents across many common use cases to build modern web experiences that are <mark>accessible</mark>, performant, and secure.
 </p>
 <footer><cite><a href="https://developer.chrome.com/docs/modern-web-guidance?hl=en">Build with Modern Web Guidance</a> (highlight in quote mine)</cite></footer>
</blockquote>



<p>
I appreciate this set of LLM skills lists accessibility first, something I feel strongly <em>every</em> MVP (or <q>early preview</q> as Google calls this) must:
</p>

<blockquote>
 <p>
 Simply, if your MVP does not have accessibility factored in, it is not viable. If you claim accessibility is a core principle but your version 0 does not include it, then it is not a core principle.
 </p>
 <footer><cite><a href="https://adrianroselli.com/2022/11/accessibility-gaps-in-mvps.html">Accessibility ‘Gaps’ in MVPs</a> by me, 5 November 2022</cite></footer>
</blockquote>



<p>
Last year I made a straightforward and simple request to Google (that also respects the law in most jurisdictions where Google does business):
</p>

<blockquote>
 <p>
 Please, if your team cannot explain how <em>the thing</em> satisfies all <a href="https://www.w3.org/WAI/WCAG22/quickref/">WCAG Success Criteria</a> at Level AA, then don’t release <em>the thing</em>.
 </p>
 <footer><cite><a href="https://adrianroselli.com/2025/05/my-request-to-google-on-accessibility.html">My Request to Google on Accessibility</a> by me, 22 May 2026</cite></footer>
</blockquote>



<p>
Unfortunately, MWG accessibility doesn’t satisfy. Partly because not every pattern has tailored guidance. For example, you may remember when Google ran at a toast pattern before abandoning it:
</p>

<blockquote>
 <p>
 Again, the people designing the feature neither understood accessibility nor had consulted anyone who did while they designed it, instead relegating accessibility to the later stages of their design and assuming it could simply be remediated with a modest effort and without any serious redesign or rethink.
 </p>
 <footer><cite><a href="https://alice.boxhall.au/articles/a-threat-model-for-accessibility-on-the-web/">A threat model for accessibility on the web</a> by Alice Boxhall, 2 October 2025</cite></footer>
</blockquote>



<p>
I hoped Google also remembered and applied that experience here, making a skill tailored to toasts to avoid another debacle. Nope.
</p>

<blockquote>
 <p>
 Reopening because this guide needs accessibility guidance in some form as there’s no general toast guidance.
 </p>
 <footer><cite><a href="https://github.com/GoogleChrome/modern-web-guidance-src/issues/393#issuecomment-4522438175">22 May comment</a> reopening issue #393 “Create guide and evals for the persistent-toast-notifications use case,” originally marked complete 4 April 2026</cite></footer>
</blockquote>



<p>
But this is an <q>early preview</q> of what developers can do with MWG. It’s not going to cover every pattern. That would be absurd. So let’s look to a pattern Google decided to showcase at launch via this first example prompt on the page:
</p>

<blockquote>
 <p>
 Create an accordion-style stats component that smoothly animates on open and close.
 </p>
 <footer><cite>First prompt on MWG home page, under <q>Build new user experiences</q> heading.</cite></footer>
</blockquote>



<p>
I expected a pile of <code>&lt;details name&gt;</code> elements, maybe CSS <code>block-size</code> with a JavaScript fallback, probably a couple placeholder images for generic stats, possibly a well-formed table to accompany them, and of course all of it passing WCAG. This is what Antigravity produced with MWG:
</p>

<p class="codepen" data-height="300" data-pen-title="Google Modern Web Accordion" data-default-tab="html,result" data-slug-hash="xbRdrrG" data-user="aardrian" style="height: 300px; 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/aardrian/pen/xbRdrrG">
  Google Modern Web Accordion</a> by Adrian Roselli (<a href="https://codepen.io/aardrian">@aardrian</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>


<p>
Antigravity with MWG went well beyond the prompt, providing a theme switcher, configuration settings, and plenty of calls to Google fonts. But it failed to do what I asked. The animation doesn’t work in Firefox. This despite the promise of baseline / Baseline support and fallbacks:
</p>

<blockquote>
 <ul>
 <li><strong>Progressive Enhancement &amp; Nuanced Fallbacks</strong>: We distinguish between purely additive enhancements (like speculative preloading) which are safe to let older browsers silently ignore, and critical behaviors (like dialog controls or network beacons) where we write highly considered, low-overhead fallbacks.</li>
  <li>[…]</li>
  <li><strong>Baseline-Aware Integration</strong>: We leverage real-time compatibility data from the <strong>Baseline</strong> project so agents can dynamically adapt to current browser support and any browser support preferences.</li>
 </ul>
 <footer><cite>The “<a href="https://github.com/GoogleChrome/modern-web-guidance-src#safe-adoption-of-modern-features">Safe Adoption of Modern Features</a>” heading in the project’s GitHub README.</cite></footer>
</blockquote>



<p>
This is already worrisome given we know that three years on, Baseline still doesn’t really address baseline support:
</p>

<blockquote>
 <p>
 The relatively new <em>Web Platform Baseline</em> offering does not track browser support for accessibility features built into the web platform. If you need to understand whether browsers support accessibility features as your own base level set of requirements, for legal or other compliance reasons, then <em>Web Platform Baseline</em> does not represent a baseline.
 </p>
 <footer><cite><a href="https://adrianroselli.com/2023/12/baseline-does-not-really-cover-baseline-support.html">Baseline Does Not Really Cover Baseline Support</a> by me, 16 December 2023</cite></footer>
</blockquote>



<p>
Let’s ignore the non-DRY code, like duplicated HTML text in the script and stringing out the <code>transition</code> instead of using <code>transition-property</code>. <em>Antigravity with MWG didn’t even create a WCAG-conformant example from its own prompt.</em> I found errors within moments. MWG failed its core promises of creating accessible and performant code. Despite its own explicit guidance.
</p>

<blockquote>
 <p>
 For the first two of these MWG already had guidance, and the a11y skill just wasn’t picked up. Overall activation aside, this highlights the importance of including contextual a11y information.
 </p>
 <p>
 […] [W]e should try to have one guide for each common interactive element summarizing and linking/embedding relevant bits of guidance (e.g. tabs, accordions, drawers, etc). The more scattered the use case guidance is, the less likely it is that agents will actually pick up all the pieces.
 </p>
 <footer><cite><a href="https://github.com/GoogleChrome/modern-web-guidance-src/issues/875">Issue #875 “Accessibility feedback (+ accordion use case)”</a>, 21 May 2026</cite></footer>
</blockquote>


<p>
And there’s the rub. There’s no way to guarantee you’ll get what you ask for, that you won’t get something else, and that these guides — the entire promise of MWG — will be honored. MWG asks authors to trust MWG when using an LLM to code interfaces. But MWG knows its guidance may be ignored:
</p>

<blockquote>
 <p>
 LLMs are non-deterministic. Even if we do everything right, there is no guarantee any guideline will be used for any given prompt. Even if the relevant guide is picked up, it may not be read fully. Case in point: As you say, MWG already had guidelines for many of these things!
 </p>
 <footer><cite><a href="https://front-end.social/@leaverou/116612818122307133">Mastodon post</a> from a <q>domain expert consultant</q> to Google for MWG, 21 May 2026</cite></footer>
</blockquote>



<p>
The underlying code generation technology can’t be trusted to follow the rules and the rules aren’t robust enough to blunt that reality.
</p>

<span id="ToiletWrap"><img loading="lazy" src="https://adrianroselli.com/wp-content/uploads/2026/05/modern-web-guidance_vonnegut.svg" alt="Crude orange Vonnegut-style asterisk between two curly braces angled out a bit, topped with a rounded rectangle and a curved line at the bottom, appearing almost as an ASCII art toilet." width="300" height="300" class="right"></span>

<p>
If your job requires you to burn tokens to satisfy some manager’s dashboard, then yes, grab the MWD skills files and integrate them into your LLM work. You’ll almost definitely get better output than if you do nothing at all. There are smart people working on this and many of the accessibility rules I saw were on-point.
</p>

<p>
But that output will be wrong in subtler and more nuanced ways across potentially fatter piles of code with more unrelated output and unwanted dependencies. It will require a more specialized set of skills to identify errors. And those errors will be different every time the code is generated.
</p>

<p>
So on the side, keep supporting any existing pattern library. If it’s built well, it’s copy-ready code that you can stuff into a project just as quickly as a chatbot could generate it. It’s deterministic, so you get the same code every time. Ideally it’s been vetted, so you don’t need to look for errors. It’s tailored to your context, meaning you don’t need to yank out random style switchers and typefaces (hoping you got all the dependencies).
</p>



<p>
If your job <em>doesn’t</em> require you to burn tokens, then probably ignore MWG and, by extensions, LLM for code. In case the environmental damage, intellectual property theft, wealth redistribution to the rich, encoded biases, and built-in support of fascism weren’t enough.
</p>




<h2 id="Update01">Update: 24 June 2026</h2>

<p>
The Silktide blog has this banger of a quote:
</p>

<blockquote>
 <p>
 Across the board, [Google Antigravity using Modern Web Guidance] was a deprioritization of accessibility requirements in favor of visual presentation.
 </p>
 <footer><cite><a href="https://silktide.com/blog/google-antigravity-accessibility-issues/">Google Built Antigravity for Everyone Except People Who Need It</a>, Kessler West at the Silktide blog, 2 June 2026</cite></footer>
</blockquote>

<p>
I don’t agree Antigravity nor MWG should use APG, which <a href="https://adrianroselli.com/2019/02/uncanny-a11y.html#APG">a regular visitor here may know</a>, but the author is correct the APG disclosure pattern is mostly harmless.
</p>

<p>
But Silktide is right that the, once again, Google built the wrong thing for the wrong people and then proudly shoved it out into the world with no hint of self-awareness.
</p>



<style>
@media (min-width: 25em) {
  #LanternWrap {
    /*   outline: 1px dotted #f00; */
    float: right;
    padding-left: 2em;
    shape-margin: 0;
    max-width: 49%;
    shape-outside: polygon(40% 10%, 49% 0%, 100% 0%, 100% 100%, 40% 100%, 25% 86%, 19% 73%, 14% 61%, 14% 50%, 22% 29%, 35% 21%);
  }
  #ToiletWrap {
    /*   outline: 1px dotted #f00; */
    float: right;
    padding-left: 2em;
    shape-margin: 0;
    max-width: 49%;
    shape-outside: polygon(13% 9%, 19% 0%, 100% 0%, 100% 100%, 52% 100%, 33% 93%, 24% 71%, 18% 62%, 23% 54%, 23% 35%, 13% 23%);
  }

  #LanternWrap img {
    float: none;
    margin-left: 0;
    max-width: 100%;
    /*   outline: 1px dotted #00f; */
  }

  #ToiletWrap img {
    float: none;
    margin-left: 0;
    max-width: 100%;
    /*   outline: 1px dotted #00f; */
  }
}
</style>]]></description>
      <pubDate>Mon, 25 May 2026 14:08:02 +0000</pubDate>
      <link>https://adrianroselli.com/2026/05/maybe-dont-rely-on-googles-modern-web-guidance.html</link>
      <dc:creator>Adrian Roselli</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5237376463</guid>
    </item>
    <item>
      <title><![CDATA[When to use (and not use) CSS shorthand properties]]></title>
      <description><![CDATA[<p>Ana Tudor <a href="https://bsky.app/profile/anatudor.bsky.social/post/3mkpbqpkr3k2e">posted about this recently</a> and I agree: CSS shorthand properties are genuinely useful, but they’re not universally applicable. There are no hard and fast rules here. The question to ask yourself isn’t “can I shorthand this?” but “will someone reading this later, including future me, understand what’s happening?” It comes down to readability and intention, and those are going to look different for everyone. Verbosity isn’t always the enemy.</p>
<h2 id="what39s-a-shorthand-property">
  
    What’s a shorthand property?
  
</h2>

<p>CSS shorthand properties let you set a bunch of related values in a single declaration. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascade/Shorthand_properties">There are quite a few of them</a>, some more widely used than others.</p>

<p>One you may be familiar with is the <code>background</code> property, which can set the image source, position, size, repetition rules, origin, color, clip threshold, and scroll behavior all at once:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nf">url</span><span class="p">(</span><span class="err">“</span><span class="o">/</span><span class="n">images</span><span class="o">/</span><span class="n">my-image</span><span class="p">.</span><span class="n">jpg</span><span class="err">”</span><span class="p">)</span> <span class="m">1rem</span> <span class="m">1rem</span> <span class="o">/</span> <span class="m">50%</span> <span class="m">75%</span> <span class="nb">no-repeat</span> <span class="nb">fixed</span> <span class="nb">content-box</span> <span class="n">padding-box</span> <span class="nx">orange</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>Do you know what each of those values corresponds to? Some are obvious. <code>orange</code> is clearly the background color, and the URL is the image. But the numeric values and the <code>content-box</code>/<code>padding-box</code> pair? I have to look those up every single time.</p>

<p>That’s exactly the kind of situation where I’d rather just write it out.</p>
<h2 id="when-to-use-it-when-to-skip-it">
  
    When to use it, when to skip it
  
</h2>
<h3 id="when-the-ordering-is-intuitive">
  
    When the ordering is intuitive
  
</h3>

<p>Directional properties like <code>padding</code> and <code>margin</code> are generally fine to shorthand. I’ve committed the ordering to memory (clockwise from the top side), which is what makes them feel safe. The number of values determines what gets applied.</p>

<p>One value applies to all four sides:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">padding</span><span class="p">:</span> <span class="m">1vw</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>Two values split into y-axis then x-axis:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">padding</span><span class="p">:</span> <span class="m">1px</span> <span class="m">10%</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>Four values apply clockwise from the top (top, right, bottom, left):</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">padding</span><span class="p">:</span> <span class="m">1rem</span> <span class="m">10rem</span> <span class="m">3rem</span> <span class="m">2rem</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>The three-value version, though? I always forget that the second value applies to <em>both</em> right and left. I’d rather write them out individually than trip over that every time I read it back.</p>

<p>So this:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">padding</span><span class="p">:</span> <span class="m">1px</span> <span class="m">5rem</span> <span class="m">2rem</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>Becomes this:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">padding-bottom</span><span class="p">:</span> <span class="m">2rem</span><span class="p">;</span>
  <span class="nl">padding-left</span><span class="p">:</span> <span class="m">5rem</span><span class="p">;</span>
  <span class="nl">padding-right</span><span class="p">:</span> <span class="m">5rem</span><span class="p">;</span>
  <span class="nl">padding-top</span><span class="p">:</span> <span class="m">1px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>An even more resilient pattern is to use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Logical_properties_and_values">logical properties</a> rather than strict directional ones, to account for layout changes across different languages and writing modes. In a left-to-right language like English, <code>padding-inline</code> maps to left and right, and <code>padding-block</code> maps to top and bottom. In a right-to-left language like Arabic, the inline axis flips.</p>

<p>You can get more specific with <code>padding-inline-start</code>, <code>padding-inline-end</code>, <code>padding-block-start</code>, and <code>padding-block-end</code> to target individual sides. In this example, a left-to-right reader sees <code>2rem</code> on the left and <code>6rem</code> on the right; a right-to-left reader sees those values swapped, with the same top and bottom padding either way:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="py">padding-block</span><span class="p">:</span> <span class="m">10%</span><span class="p">;</span>
  <span class="py">padding-inline-start</span><span class="p">:</span> <span class="m">2rem</span><span class="p">;</span>
  <span class="py">padding-inline-end</span><span class="p">:</span> <span class="m">6rem</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p><code>border-radius</code> falls into this category, too. One value applying to all four corners is intuitive enough that shorthand may be fine:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">border-radius</span><span class="p">:</span> <span class="m">8px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>And when we have a variety of values, it’s good to know what we intend with longhand syntax. Sometimes I’ll even write out a zero-ed out property (like border-bottom-right-radius) in this example to communicate exactly what’s needed for this element’s shape:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">border-top-left-radius</span><span class="p">:</span> <span class="m">2rem</span><span class="p">;</span>
  <span class="nl">border-top-right-radius</span><span class="p">:</span> <span class="m">8rem</span><span class="p">;</span>
  <span class="nl">border-bottom-right-radius</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
  <span class="nl">border-bottom-left-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><h3 id="when-each-value-is-a-distinct-type">
  
    When each value is a distinct type
  
</h3>

<p><code>transition</code> and <code>animation</code> are both shorthands that pack in a lot: duration, easing, delay, and the property being targeted. For simple cases, the shorthand may hold up when each value is a distinct type: a name, a duration, an easing function.</p>

<p>This example may be easier to parse without ambiguity:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">animation</span><span class="p">:</span> <span class="n">spin</span> <span class="m">1s</span> <span class="nb">ease-in-out</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>But even when the types are distinct, ordering can become its own problem at scale. If one part of a codebase has:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">animation</span><span class="p">:</span> <span class="n">spin</span> <span class="m">1s</span> <span class="nb">ease-in-out</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>And another one has:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">animation</span><span class="p">:</span> <span class="nb">linear</span> <span class="n">fade</span> <span class="m">2s</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>Both are valid, but the inconsistency means you’re re-parsing the order every time you encounter the shorthand. Writing them out individually enforces a consistent structure by default. There’s no order to forget or vary.</p>

<p>When things get specific with different delays per property or timing, I’ll pull out the individual properties anyways. Ana’s post has a great example of this: if you have multiple properties transitioning at the same duration, writing the full shorthand for each one is just repeating yourself:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">animation</span><span class="p">:</span> <span class="m">1s</span> <span class="nb">ease-in-out</span><span class="p">;</span>
  <span class="nl">animation-name</span><span class="p">:</span> <span class="n">spin</span><span class="p">,</span> <span class="n">fade</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p><code>animation</code> in particular has enough sub-properties (<code>animation-fill-mode</code>, <code>animation-iteration-count</code>, <code>animation-direction</code>, and more) that the shorthand may get unwieldy fast. In practice, once I’m reaching for any of those, I just write them out all individually.</p>
<h3 id="when-the-syntax-has-its-own-rules">
  
    When the syntax has its own rules
  
</h3>

<p>Some shorthands have parsing rules specific enough that you may need to have the spec loaded to read them. These are the ones I almost always write out.</p>

<p><code>background</code> is my go-to example. Unless you’re setting something dead simple like a solid color or a plain image, the full shorthand becomes a puzzle. This is still readable:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nf">url</span><span class="p">(</span><span class="s1">"/images/my-image.jpg"</span><span class="p">)</span> <span class="nb">no-repeat</span> <span class="nb">center</span> <span class="o">/</span> <span class="nb">cover</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>But once you’re reaching for position offsets, size, origin, and clip together, the shorthand stops carrying its weight:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nf">url</span><span class="p">(</span><span class="s1">"/images/my-image.jpg"</span><span class="p">)</span> <span class="m">1rem</span> <span class="m">1rem</span> <span class="o">/</span> <span class="m">50%</span> <span class="m">75%</span> <span class="nb">no-repeat</span> <span class="nb">fixed</span> <span class="nb">content-box</span> <span class="n">padding-box</span> <span class="nx">orange</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>It fails here not because there are many values, but because the values are opaque without knowing the specific syntax. The <code>/</code> separates position from size, so <code>1rem 1rem / 50% 75%</code> isn’t four coordinates, it’s two pairs with different meanings. The <code>content-box</code> <code>padding-box</code> pair sets origin and clip in that order, which isn’t something you can always intuit. And that’s a different problem from <code>padding</code> having too many values, because at least those follow a spatial logic.</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">background-attachment</span><span class="p">:</span> <span class="nb">fixed</span><span class="p">;</span>
  <span class="nl">background-clip</span><span class="p">:</span> <span class="n">padding-box</span><span class="p">;</span>
  <span class="nl">background-color</span><span class="p">:</span> <span class="nx">orange</span><span class="p">;</span>
  <span class="nl">background-image</span><span class="p">:</span> <span class="nf">url</span><span class="p">(</span><span class="s1">"/images/my-image.jpg"</span><span class="p">);</span>
  <span class="nl">background-origin</span><span class="p">:</span> <span class="nb">content-box</span><span class="p">;</span>
  <span class="nl">background-position</span><span class="p">:</span> <span class="m">1rem</span> <span class="m">1rem</span><span class="p">;</span>
  <span class="nl">background-repeat</span><span class="p">:</span> <span class="nb">no-repeat</span><span class="p">;</span>
  <span class="nl">background-size</span><span class="p">:</span> <span class="m">50%</span> <span class="m">75%</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p><code>border</code> is also interesting because it’s a layered shorthand situation: <code>border</code> is a shorthand, and so is <code>border-left</code>. The catch is that <code>border</code> applies to all four sides. You have to go down to the directional properties or the individual sub-properties to set different sides. I usually write <code>border</code> shorthand when I want a consistent border all the way around, but I’ll break it out when I’m doing something more specific with colors or styles on individual sides.</p>

<p>You can also write the sub-properties directionally, which is a nice middle ground:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">border-color</span><span class="p">:</span> <span class="nx">red</span> <span class="nx">blue</span> <span class="nx">green</span> <span class="nx">yellow</span><span class="p">;</span>
  <span class="nl">border-style</span><span class="p">:</span> <span class="nb">solid</span> <span class="nb">dashed</span><span class="p">;</span>
  <span class="nl">border-width</span><span class="p">:</span> <span class="m">10px</span> <span class="m">3px</span> <span class="m">50px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p><code>grid</code> falls into this category too. The shorthand syntax can be genuinely hard to parse at a glance:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="py">grid</span><span class="p">:</span> <span class="n">auto-flow</span> <span class="n">dense</span> <span class="m">40px</span> <span class="o">/</span> <span class="nf">repeat</span><span class="p">(</span><span class="m">3</span><span class="p">,</span> <span class="m">1fr</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>This feels easier to read to me:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">grid-auto-flow</span><span class="p">:</span> <span class="n">dense</span><span class="p">;</span>
  <span class="nl">grid-auto-rows</span><span class="p">:</span> <span class="m">40px</span><span class="p">;</span>
  <span class="nl">grid-template-columns</span><span class="p">:</span> <span class="nf">repeat</span><span class="p">(</span><span class="m">3</span><span class="p">,</span> <span class="m">1fr</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>I write <code>grid</code> and <code>flex</code> out fully, almost every time.</p>
<h3 id="when-you-can’t-retain-it">
  
    When you can’t retain it
  
</h3>

<p>Some shorthands just don’t stick, and that’s reason enough to write them out. This is also an honest admission that the thresholds I’m describing aren’t necessarily universal; they’re shaped by what I’ve internalized over time. These are conditions as I experience them and not objective rules.</p>

<p>I use the longhand syntax for <code>text-decoration</code> more often than not, even though the individual property names are fairly obvious. I just can’t seem to retain the shorthand structure.</p>

<p>I always have to think about this:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span> <span class="nb">dotted</span> <span class="nx">red</span> <span class="m">2px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>And to me, this syntax reads like a list of decisions:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">text-decoration-color</span><span class="p">:</span> <span class="nx">red</span><span class="p">;</span>
  <span class="nl">text-decoration-line</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
  <span class="nl">text-decoration-style</span><span class="p">:</span> <span class="nb">dotted</span><span class="p">;</span>
  <span class="py">text-decoration-thickness</span><span class="p">:</span> <span class="m">2px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p><code>gap</code> is another directional one: it only deals with row gap and column gap in CSS grid. I usually set the same value for both, so shorthand is fine. But when they’re different, I’ll often write <code>row-gap</code> and <code>column-gap</code> separately, mostly because I can never remember which order they go in. I’ve somehow committed <code>padding</code> and <code>margin</code> to memory but not this one.</p>

<p><code>font</code> I have literally never written shorthand in my life. It just doesn’t feel natural, especially with <code>font-family</code>, which often has spaces and fallback stacks and needs its own line anyway.</p>

<p><code>font-variant</code> is a fancy one I reach for when handling special typefaces, usually for turning on ligatures. I can never remember all the individual values, so I write them out. And since I’m typically only tweaking one or two things, it doesn’t add much overhead.</p>
<h2 id="it’s-not-all-or-nothing">
  
    It’s not all or nothing
  
</h2>

<p>You don’t have to choose between going full shorthand or writing out every individual property (spoiler though: <a href="https://thoughtbot.com/blog#writing-only-what-you-mean">I often <em>do</em> make that choice</a>). You can mix them by using the shorthand to set a baseline and then applying a specific longhand when you need precision for one value.</p>

<p><code>border</code> is a solid example of this. Set the baseline with shorthand, then override what changes in a variant:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.button</span> <span class="p">{</span>
  <span class="nl">border</span><span class="p">:</span> <span class="m">2px</span> <span class="nb">solid</span> <span class="nx">navy</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.button--destructive</span> <span class="p">{</span>
  <span class="nl">border-color</span><span class="p">:</span> <span class="nx">red</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>The goal is always clarity. Use the version that makes your intent most obvious.</p>
<h2 id="writing-only-what-you-mean">
  
    Writing only what you mean
  
</h2>

<p>Despite what I’ve just written in the <a href="https://thoughtbot.com/blog#it%E2%80%99s-not-all-or-nothing">previous section</a>, I don’t <em>usually</em> mix short and longhand and am most often choosing the latter.</p>

<p>Longhand can communicate scope, and is a key reason I often err on that side rather than brevity.</p>

<p>Even for something as simple as background color, I’d rather write:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">background-color</span><span class="p">:</span> <span class="nx">green</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>Instead of:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="nx">green</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>The shorthand works, but leaves the door open. If I only mean to set a color, <code>background-color</code> says that plainly and there’s no slot for anything else other than that to accumulate.</p>

<p>The same logic applies when I want a handful of sub-properties. If I only want a selector to have a background image, color, and repeat value, I write exactly those three properties:</p>
<div class="highlight"><pre class="highlight css"><code><span class="nc">.selector</span> <span class="p">{</span>
  <span class="nl">background-color</span><span class="p">:</span> <span class="nx">#b4da55</span><span class="p">;</span>
  <span class="nl">background-image</span><span class="p">:</span> <span class="nf">url</span><span class="p">(</span><span class="err">“</span><span class="n">images</span><span class="o">/</span><span class="n">my-image</span><span class="p">.</span><span class="n">jpg</span><span class="err">”</span><span class="p">);</span>
  <span class="nl">background-repeat</span><span class="p">:</span> <span class="nb">no-repeat</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>A <code>background</code> shorthand would create a natural place for other sub-properties to creep in. Someone (including future me) might reasonably add <code>background-size</code> or <code>background-position</code> to that declaration later. And while those are fine additions, it signals intention to me when they’re written out as their sub-properties instead of values within the shorthand.</p>

<p>At the end of the day, shorthand properties are a tool, not a mandate. And if your answer differs from mine: yeah, well you know, that’s just like uh, your opinion, man.</p>

<aside class="related-articles"><h2>If you enjoyed this post, you might also like:</h2>
<ul>
<li><a href="https://thoughtbot.com/blog/rust-doesn-t-have-named-arguments-so-what">Rust Doesn’t Have Named Arguments. So What?</a></li>
<li><a href="https://thoughtbot.com/blog/positioning">Positioning elements on the web with CSS</a></li>
<li><a href="https://thoughtbot.com/blog/taking-the-most-out-of-stimulus">Taking the most out of Stimulus.js</a></li>
</ul></aside>
<img src="https://feed.thoughtbot.com/link/24077/17340884.gif" height="1" width="1">]]></description>
      <pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate>
      <link>https://feed.thoughtbot.com/link/24077/17340884/when-to-use-and-not-use-css-shorthand-properties</link>
      <dc:creator>Giant Robots Smashing Into Other Giant Robots</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5224948125</guid>
    </item>
    <item>
      <title><![CDATA[Repeating Square Dots Backgrounds in CSS]]></title>
      <description><![CDATA[
<p class="wp-block-paragraph">I saw this reasonable ask for help the other day.</p>



<blockquote class="mastodon-embed" data-embed-url="https://mastodon.social/@pawelgrzybek/116510590035197395/embed" style="background: #FCF8FF; border-radius: 8px; border: 1px solid #C9C4DA; margin: 0 0 2rem 0; max-width: 540px; min-width: 270px; overflow: hidden; padding: 0;"> <a href="https://mastodon.social/@pawelgrzybek/116510590035197395" target="_blank" style="align-items: center; color: #1C1A25; display: flex; flex-direction: column; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Roboto, sans-serif; font-size: 14px; justify-content: center; letter-spacing: 0.25px; line-height: 20px; padding: 24px; text-decoration: none;"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M63 45.3v-20c0-4.1-1-7.3-3.2-9.7-2.1-2.4-5-3.7-8.5-3.7-4.1 0-7.2 1.6-9.3 4.7l-2 3.3-2-3.3c-2-3.1-5.1-4.7-9.2-4.7-3.5 0-6.4 1.3-8.6 3.7-2.1 2.4-3.1 5.6-3.1 9.7v20h8V25.9c0-4.1 1.7-6.2 5.2-6.2 3.8 0 5.8 2.5 5.8 7.4V37.7H44V27.1c0-4.9 1.9-7.4 5.8-7.4 3.5 0 5.2 2.1 5.2 6.2V45.3h8ZM74.7 16.6c.6 6 .1 15.7.1 17.3 0 .5-.1 4.8-.1 5.3-.7 11.5-8 16-15.6 17.5-.1 0-.2 0-.3 0-4.9 1-10 1.2-14.9 1.4-1.2 0-2.4 0-3.6 0-4.8 0-9.7-.6-14.4-1.7-.1 0-.1 0-.1 0s-.1 0-.1 0 0 .1 0 .1 0 0 0 0c.1 1.6.4 3.1 1 4.5.6 1.7 2.9 5.7 11.4 5.7 5 0 9.9-.6 14.8-1.7 0 0 0 0 0 0 .1 0 .1 0 .1 0 0 .1 0 .1 0 .1.1 0 .1 0 .1.1v5.6s0 .1-.1.1c0 0 0 0 0 .1-1.6 1.1-3.7 1.7-5.6 2.3-.8.3-1.6.5-2.4.7-7.5 1.7-15.4 1.3-22.7-1.2-6.8-2.4-13.8-8.2-15.5-15.2-.9-3.8-1.6-7.6-1.9-11.5-.6-5.8-.6-11.7-.8-17.5C3.9 24.5 4 20 4.9 16 6.7 7.9 14.1 2.2 22.3 1c1.4-.2 4.1-1 16.5-1h.1C51.4 0 56.7.8 58.1 1c8.4 1.2 15.5 7.5 16.6 15.6Z" fill="currentColor"></path></svg> <div style="color: #787588; margin-top: 16px;">Post by @pawelgrzybek@mastodon.social</div> <div style="font-weight: 500;">View on Mastodon</div> </a> </blockquote> <script data-allowed-prefixes="https://mastodon.social/" async="" src="https://mastodon.social/embed.js"></script>



<p class="wp-block-paragraph"><span style="box-sizing: border-box; margin: 0px; padding: 0px;">Note that the example above has little&nbsp;<em>circular</em>&nbsp;dots, and what Paweł is asking about are little&nbsp;<em>square</em>&nbsp;dots.</span> This is the example look from atproto.com:</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="361" src="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/Screenshot-2026-05-12-at-7.58.48-AM.png?resize=1024%2C361&amp;ssl=1" alt="" class="wp-image-9624" srcset="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/Screenshot-2026-05-12-at-7.58.48-AM.png?resize=1024%2C361&amp;ssl=1 1024w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/Screenshot-2026-05-12-at-7.58.48-AM.png?resize=300%2C106&amp;ssl=1 300w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/Screenshot-2026-05-12-at-7.58.48-AM.png?resize=768%2C271&amp;ssl=1 768w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/Screenshot-2026-05-12-at-7.58.48-AM.png?resize=1536%2C541&amp;ssl=1 1536w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/Screenshot-2026-05-12-at-7.58.48-AM.png?resize=2048%2C722&amp;ssl=1 2048w" sizes="auto, (max-width: 1000px) 100vw, 1000px"></figure>



<h2 class="wp-block-heading">Repeating a Small Area</h2>



<p class="wp-block-paragraph">My first thought is we’re obviously not drawing all these dots in one big graphic. We’re drawing one, leaving empty space, and using our ol’ pal <code>background-repeat: repeat;</code></p>



<p class="wp-block-paragraph">In order for the repeating to work, we need to define a smaller space via <code>background-size</code>. So let’s say it’s…</p>


<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-selector-tag">html</span> {
  <span class="hljs-attribute">background-size</span>: <span class="hljs-number">100px</span> <span class="hljs-number">100px</span>;
  <span class="hljs-attribute">background-repeat</span>: repeat;
}</code></span></pre>


<p class="wp-block-paragraph">Now we’ve got a 100px square that will repeat over the entire background. We just need to fill that 100px square with a <em>smaller</em> square that repeats. </p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="614" src="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/repeating-dots.png?resize=1024%2C614&amp;ssl=1" alt="" class="wp-image-9622" srcset="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/repeating-dots.png?resize=1024%2C614&amp;ssl=1 1024w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/repeating-dots.png?resize=300%2C180&amp;ssl=1 300w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/repeating-dots.png?resize=768%2C461&amp;ssl=1 768w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/repeating-dots.png?resize=1536%2C922&amp;ssl=1 1536w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/repeating-dots.png?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px"></figure>



<h2 class="wp-block-heading">Drawing the Square Dot</h2>



<p class="wp-block-paragraph">Again my first thought was that we could make it <em>look</em> like we’re drawing a small square dot by actually letting a <code>background-color</code> show through and covering up everywhere else. So like a three-layer system: </p>


<div class="wp-block-image">
<figure class="aligncenter size-large is-resized"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="512" src="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/layers.png?resize=1024%2C512&amp;ssl=1" alt="" class="wp-image-9623" style="width:602px;height:auto" srcset="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/layers.png?resize=1024%2C512&amp;ssl=1 1024w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/layers.png?resize=300%2C150&amp;ssl=1 300w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/layers.png?resize=768%2C384&amp;ssl=1 768w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/layers.png?resize=1536%2C768&amp;ssl=1 1536w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/layers.png?w=1613&amp;ssl=1 1613w" sizes="auto, (max-width: 1000px) 100vw, 1000px"></figure>
</div>

<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-selector-tag">html</span> {
  <span class="hljs-attribute">background</span>: 
    <span class="hljs-built_in">linear-gradient</span>(to bottom, transparent <span class="hljs-number">10px</span>, white <span class="hljs-number">10px</span>),
    <span class="hljs-built_in">linear-gradient</span>(to right, transparent <span class="hljs-number">10px</span>, white <span class="hljs-number">10px</span>),
    black;
}</code></span></pre>


<p class="wp-block-paragraph">If we flatten out that visual, you can see the three shapes smoosh down into a small square, which we then repeat.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="512" src="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/square.png?resize=1024%2C512&amp;ssl=1" alt="" class="wp-image-9625" srcset="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/square.png?resize=1024%2C512&amp;ssl=1 1024w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/square.png?resize=300%2C150&amp;ssl=1 300w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/square.png?resize=768%2C384&amp;ssl=1 768w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/square.png?resize=1536%2C768&amp;ssl=1 1536w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/square.png?w=1613&amp;ssl=1 1613w" sizes="auto, (max-width: 1000px) 100vw, 1000px"></figure>



<p class="wp-block-paragraph">That’s what’s going on here exactly:</p>



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



<h2 class="wp-block-heading">Uh Oh — Transparency?</h2>



<p class="wp-block-paragraph">The problem with the above is that we need a solid color to be the “mask” that covers all the area except the mask. This means we can’t have, for example, one big image behind the dots or a gradient or anything (without more exotic trickery). The problem is we don’t have proper transparency. </p>



<p class="wp-block-paragraph">What we want to be doing is drawing the dot with one “gradient”, if we can, and leaving the rest of the area empty/transparent.</p>



<h2 class="wp-block-heading">Enter Conic Gradients</h2>



<p class="wp-block-paragraph">I didn’t think of this! <a href="https://mastodon.social/@Meyerweb/116510694762069077">Eric Meyer had the clever idea.</a> The idea is that you can use <code>conic-gradient</code> to describe what we want in one go. “Hard stop” color stops are in use here, the classic trick to make a gradient just bands of solid color, not actually gradients. But in this case, three-quarters of the area is transparent, and the last bit is the dot color.</p>


<pre class="wp-block-code"><span><code class="hljs language-css"><span class="hljs-selector-tag">html</span> {
  <span class="hljs-attribute">background-image</span>: 
    <span class="hljs-built_in">conic-gradient</span>(
      from <span class="hljs-number">0deg</span> at <span class="hljs-number">5px</span> <span class="hljs-number">5px</span>, 
      transparent <span class="hljs-number">75%</span>, black <span class="hljs-number">75%</span> <span class="hljs-number">100%</span>
    );
}</code></span></pre>


<p class="wp-block-paragraph">This took me a sec to understand, but it’s essentially setting the center of the conic gradient at a specific spot, then forcing most of the way around to be transparent and leaving the last bit as the dot.</p>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="510" src="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/CleanShot-2026-05-12-at-09.12.40%402x.png?resize=1024%2C510&amp;ssl=1" alt="" class="wp-image-9628" srcset="https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/CleanShot-2026-05-12-at-09.12.40%402x.png?resize=1024%2C510&amp;ssl=1 1024w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/CleanShot-2026-05-12-at-09.12.40%402x.png?resize=300%2C149&amp;ssl=1 300w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/CleanShot-2026-05-12-at-09.12.40%402x.png?resize=768%2C383&amp;ssl=1 768w, https://i0.wp.com/frontendmasters.com/blog/wp-content/uploads/2026/05/CleanShot-2026-05-12-at-09.12.40%402x.png?w=1383&amp;ssl=1 1383w" sizes="auto, (max-width: 1000px) 100vw, 1000px"></figure>



<p class="wp-block-paragraph">Then we use the same concept as before where we set a smaller <code>background-size</code> and let it naturally repeat, and we’ve got square dots with a transparent background! </p>



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



<p class="wp-block-paragraph"></p>
]]></description>
      <pubDate>Tue, 12 May 2026 16:23:42 +0000</pubDate>
      <link>https://frontendmasters.com/blog/repeating-square-dots-backgrounds-in-css/</link>
      <dc:creator>Master.dev Blog RSS Feed</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5223342807</guid>
    </item>
    <item>
      <title><![CDATA[Lazy and Prompt]]></title>
      <description><![CDATA[
                    
																																	<p>Chrome 148 shipped this week, and in <a href="https://developer.chrome.com/release-notes/148">the release notes</a> you’ll find one of the best things to happen to web performance in a long time: <code>loading=&amp;quot;lazy&amp;quot;</code> for <code>&amp;lt;video&amp;gt;</code> and <code>&amp;lt;audio&amp;gt;</code> elements. </p>
<p><a href="https://scottjehl.com/">Scott Jehl</a>, an engineer at Squarespace focusing on performance and accessibility, has been the driving force behind this. Scott and a small team of engineers <a href="https://engineering.squarespace.com/blog/2026/squarespace-and-web-standards-how-we-helped-bring-html-video-and-audio-lazy-loading-to-todays-browsers">proposed the spec change, wrote the implementation, and got it through the WHATWG HTML spec process</a> with positive signals from all three major browser engines. If you want to understand the full story behind it, Scott <a href="https://shoptalkshow.com/712/">talked about it in detail on ShopTalk Show</a> and Squarespace published an excellent <a href="https://engineering.squarespace.com/blog/2026/how-to-use-standard-html-video-and-audio-lazy-loading-on-the-web-today">post on how to use the feature today</a>. This is what working with the web platform looks like: identify a real problem, propose a solution, build consensus, ship it. A good story.</p>
<p>But that’s not the story of Chrome 148. The story of Chrome 148 is that Google – in the very same release – shipped the Prompt API, a feature that was met with explicit opposition from both Mozilla and WebKit, that the W3C TAG raised deep concerns about, and that the web community has been vocally and overwhelmingly against.</p>
<p><a href="https://wil.to/posts/googles-prompt-api/">Mat Marquis wrote the definitive piece on this</a>, and I won’t retell it here because he puts it in much better words than I ever could. Go read his post. But to understand the scope of the problem: the Prompt API, as shipped, is not a standardized interface for interacting with language models in general. It’s an API for interacting with Google’s Gemini Nano – a 4 GB model that Chrome has already silently downloaded to your machine, without asking, and that it will re-download if you remove it. Any website you visit can now send prompts to that model, using your processing resources, without requesting permission. When both other browser engines explicitly oppose a feature, calling it a “web standard” is a bit of a stretch. I’m not involved in any of this. But after working on the Web for over 25 years, I know how web standards work. This is not one. This is a Chrome feature wearing the costume of a web standard.</p>
<p>And that’s not all. To use the API as a developer, you must agree to Google’s Generative AI Prohibited Uses Policy – a corporate policy attached to what is supposed to be a web platform API. <a href="https://jakearchibald.com/">Jake Archibald</a>, who is now at Mozilla, <a href="https://github.com/mozilla/standards-positions/issues/1213#issuecomment-4347988313">called this out explicitly</a> (and got over 800 likes for it btw): elements of this policy go beyond law, and it sets a dangerous precedent for APIs that come with vendor-specific terms of use. As Mat writes, imagine the Geolocation API requiring you to agree to Google Maps’ Terms and Conditions. Think about what this means if we accept it: the next browser API could come with its own corporate terms, too. And the one after that. The web platform has always been open, implementable by anyone, and governed by specs – not by a single company’s usage policy. This is a line that, once crossed, is very hard to uncross.</p>
<p>There’s also the question of what the browser is even <em>for</em>. A browser is a <em>user agent</em> – it’s supposed to act on behalf of the user, not the vendor. Silently downloading 4 GB of AI model to your machine, re-downloading it if you remove it, and then letting any website access it without your permission – that’s Chrome acting as Google’s agent, not yours. And it’s hard not to notice how many Google products stand to benefit from having an LLM pre-installed on every Chrome user’s machine. With opt-out, of course.</p>
<p>And how did Google justify shipping it over the opposition of the other browser makers? By listing developer sentiment as “strongly positive” in their <a href="https://groups.google.com/a/chromium.org/g/blink-dev/c/iR6R7-nQeHI/m/gN4iEGEdAQAJ">Intent to Ship</a>. But the evidence behind that claim doesn’t really hold up. The blink-dev thread where the feature was approved consisted entirely of browser engine insiders – Chrome engineers and one Microsoft Edge representative. No external developers participated. A <a href="https://github.com/webmachinelearning/prompt-api/issues/74">GitHub issue</a> Google created specifically to capture developer interest in the API received exactly two responses in seven months. Satisfaction scores came from a survey Google administered to its own <a href="https://developer.chrome.com/origintrials/#/trials/active">Origin Trial</a> participants — self-selected early adopters who had actively signed up to test experimental Chrome APIs — with no sample sizes disclosed. The <a href="https://github.com/webmachinelearning/prompt-api/blob/main/README.md#stakeholder-feedback">stakeholder feedback page</a> cited one promotional blog post, one post on Twitter, and a now-broken link. This seems to be the foundation on which “strongly positive” was built.</p>
<p><a href="https://adactio.com/journal/tags/chrome">Jeremy Keith has documented this pattern</a> over the years: Google’s habit of shipping first and standardizing later, or not at all. AMP. FLoC. Privacy Sandbox. Chrome ploughing ahead and unilaterally implementing whatever it wants, even if there’s no consensus from other browser makers. The Prompt API isn’t an exception. It fits the pattern.</p>
<p>What makes this particular instance so frustrating is that the contrast is right there in front of us, in the same set of release notes. There’s lazy loading for audio and video: carefully proposed, properly standardized, welcomed by all implementers. A true gift to the web community. And then, there’s the Prompt API: rammed through despite broad opposition. One feature shows how the standards process can and should work. The other shows what happens when a company with dominant browser market share decides that the process doesn’t apply to them. The fact that my posts on <a href="https://bsky.app/profile/did:plc:go53xbllmkjw3vp65vmrcjol/post/3ml7k34qjes2z">Bluesky</a> and <a href="https://mastodon.social/@matthiasott/116529519003053585">Mastodon</a> sharing Mat’s article are (still) getting hundreds of reactions while Google's own <a href="https://github.com/webmachinelearning/prompt-api/issues/74">call for developer interest</a> in the Prompt API received two responses in seven months tells you everything you need to know.</p>
<p>Chrome 148 could have been a celebration. It should have been about lazy loading. About the power of the web standards process. Instead, Google made sure that all anyone’s talking about is how they’ve once again shoved something down our throats that nobody asked for.</p>

																												            	]]></description>
      <pubDate>Thu, 07 May 2026 13:56:00 +0000</pubDate>
      <link>https://matthiasott.com/notes/lazy-and-prompt</link>
      <dc:creator>Matthias Ott</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5218019684</guid>
    </item>
    <item>
      <title><![CDATA[Do graphs and charts need to be accessible?]]></title>
      <description><![CDATA[<p>Everything should be accessible. Or should it? Charts and graphs are a great example of where you <em>can</em> make something accessible, but maybe you shouldn’t. Bear with me here.</p>
<p>Pie charts, line graphs, bar and column charts (let’s just call them all ‘charts’ for ease) are really useful, visual ways to present data, and we’d include one on a webpage using an image, like a PNG or SVG.</p>
<h2 id="accessible-pngs">Accessible PNGs</h2>
<p>I say ‘PNGs’, but these could be any other image format, like WebP, AVIF, or even JPG. The mechanism we’d use to embed them on our page would be the <code>&lt;img /&gt;</code> element:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>my-great-chart.png<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>A description of the information the chart conveys<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span></code></pre>
<p>Depending on the complexity of the chart, our <code>alt</code> text could be doing a lot of work here. In principle, that’s fine, as the <code>alt</code> attribute can contain as many characters as you need, but the problem is that there’s no way of introducing any meaning to the content in <code>alt</code> text; it’s just a wall of unstructured text.</p>
<p>Here’s an example with some made-up figures about people’s favourite characters from the original Star Wars trilogy:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>star-wars-favourites.png<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Pie chart showing that 22% chose Han Solo, 18% chose Darth Vader, 15% chose Princess Leia, 12% chose Chewbacca, 9% chose Luke Skywalker, 7% chose Yoda, 6% chose Obi-Wan Kenobi, 4% chose Lando Calrissian, 3% chose R2-D2, 2% chose C-3PO, and 2% chose other characters.<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span></code></pre>
<p>That’s a fairly typical amount of information for a chart, and it takes a fair amount of concentration to make sense of, especially with so much repetitive language. Imagine more complex charts, like a <a href="https://en.wikipedia.org/wiki/Bar_chart#Grouped_(clustered)_and_stacked">stacked column chart</a> or a <a href="https://en.wikipedia.org/wiki/Line_chart#Best-fit">best-fit line chart</a>.</p>
<h3 id="before-you-mention-longdesc">Before you mention <code>longdesc</code>…</h3>
<p>I know what you’re thinking: there’s the <code>longdesc</code> attribute, which allows us to reference a structured HTML document:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>star-wars-favourites.png<span class="token punctuation">"</span></span> <span class="token attr-name">longdesc</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>star-wars-favourites-description.html<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span></code></pre>
<p>Although browser support still appears to be there, <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/longDesc"><code>longdesc</code> is long deprecated</a> so definitely not recommended.</p>
<h2 id="accessible-svgs">Accessible SVGs</h2>
<p>So if <code>alt</code> text isn’t ideal, and <code>longdesc</code> is out, maybe a more complex image format like SVG is the answer?</p>
<p>We could reference an SVG file just as we did with the PNG in our last example, but <a href="https://cariefisher.com/a11y-svg-alts/">for inline SVGs</a> the result is exactly the same; a big lump of unstructured descriptive text:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">role</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>img<span class="token punctuation">"</span></span> <span class="token attr-name">aria-labelledby</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>title description<span class="token punctuation">"</span></span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>title<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Pie chart showing people's favourite characters from the original Star Wars trilogy<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>desc</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>description<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>22% chose Han Solo, 18% chose Darth Vader, 15% chose Princess Leia, 12% chose Chewbacca, 9% chose Luke Skywalker, 7% chose Yoda, 6% chose Obi-Wan Kenobi, 4% chose Lando Calrissian, 3% chose R2-D2, 2% chose C-3PO, and 2% chose other characters.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>desc</span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- SVG coordinates --&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">&gt;</span></span></code></pre>
<h3 id="aria-in-svg">ARIA in SVG</h3>
<p>But we’ve been ignoring SVG’s superpowers! SVG is markup, so ARIA can be used to add semantics to the elements that make it up.</p>
<p>In <a href="https://tink.uk/accessible-svg-line-graphs/">Accessible SVG line graphs</a>, Léonie Watson goes into detail about how we can make purposeful use of SVG’s group (<code>&lt;g&gt;</code>) element and various ARIA role attributes to give the chart table semantics. A chart is a visual representation of tabular data, after all!</p>
<p>For our Star Wars pie chart, we could use:</p>
<ul>
<li><code>role="table"</code> for the containing group</li>
<li><code>role="row"</code> for each character</li>
<li><code>role="columnheader"</code> to label each column of information (character name, and percentage of votes)</li>
<li><code>role="rowheader"</code> for each character name</li>
<li><code>role="cell"</code> for each percentage value</li>
</ul>
<p>Visually a chart; semantically a table.</p>
<h2 id="one-thing-visually-another-non-visually">One thing visually, another non-visually</h2>
<p>I love the ARIA in SVG approach, but it’s fiddly enough to make me wonder if there’s a simpler approach. We could separate the two things:</p>
<ul>
<li>An SVG chart that’s hidden from assistive technologies with <code>role="presentation"</code> or <code>aria-hidden="true"</code></li>
<li>An HTML table that’s <a href="https://www.tempertemper.net/blog/hierarchy-in-tables#visually-hidden-text">hidden visually with CSS, but still available to screen reader users</a></li>
</ul>
<p>This way, sighted users would see the chart and screen reader users would get the data in an HTML table. That would work, but it still assumes the table is only useful to screen reader users.</p>
<h2 id="just-show-the-underlying-data">Just show the underlying data</h2>
<p>Maybe we should stop treating the table as an accessibility-only fallback? I reckon it could be even better to present both the visualisation and the underlying data. After all, some people might prefer rows and columns to a chart.</p>
<p>The way I’d probably approach this is to ensure the chart doesn’t get picked up by assistive technology like screen reader software at all, as we did before:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">role</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>presentation<span class="token punctuation">"</span></span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- SVG contents --&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">&gt;</span></span></code></pre>
<p>And then our table:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>table</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>caption</span><span class="token punctuation">&gt;</span></span>Favourite characters from the original Star Wars trilogy<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>caption</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>tr</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>th</span><span class="token punctuation">&gt;</span></span>Character<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>th</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>th</span><span class="token punctuation">&gt;</span></span>Percentage of votes<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>th</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>tr</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>tr</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>td</span><span class="token punctuation">&gt;</span></span>Han Solo<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>td</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>td</span><span class="token punctuation">&gt;</span></span>22%<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>td</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>tr</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>tr</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>td</span><span class="token punctuation">&gt;</span></span>Darth Vader<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>td</span><span class="token punctuation">&gt;</span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>td</span><span class="token punctuation">&gt;</span></span>18%<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>td</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>tr</span><span class="token punctuation">&gt;</span></span>
    <span class="token comment">&lt;!-- The rest of the characters --&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>table</span><span class="token punctuation">&gt;</span></span></code></pre>
<p>If your table has a lot of data, you could save some unnecessary scrolling for users who prefer the chart by wrapping it in a <code>&lt;details&gt;</code>/<code>&lt;summary&gt;</code> element.</p>
<p><i>Note: it’s totally valid to expose the chart to screen reader users if the visual presentation is relevant; just make sure the alternative text describes the visual presentation, not the data that’s already available in the table.</i></p>
<h2 id="so-what-to-do">So what to do?</h2>
<p>For screen reader users, a simple chart with the data in the <code>alt</code> text might be okay, but more often than not we probably want to present the data in tabular format, which is easier to navigate, compare, and understand. Going one step further by accompanying the chart with a table is an even more robust approach as it also allows sighted users to choose how best to absorb the information.</p>
<p>So do charts and graphs need to be accessible? Yes and no. The chart image itself doesn’t always need to be exposed to assistive technologies, but the information it communicates must be accessible.</p>
<hr>
<p>The article <a rel="nofollow" href="https://www.tempertemper.net/blog/do-graphs-and-charts-need-to-be-accessible">Do graphs and charts need to be accessible?</a> appeared first on <a rel="nofollow" href="https://www.tempertemper.net">www.tempertemper.net</a>.</p>
]]></description>
      <pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://www.tempertemper.net/blog/do-graphs-and-charts-need-to-be-accessible</link>
      <dc:creator>tempertemper</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5208870290</guid>
    </item>
    <item>
      <title><![CDATA[What would a 2026 CSS Anthology look like?]]></title>
      <description><![CDATA[
<p class="wp-block-paragraph">In 2004 I published the <a href="https://rachelandrew.co.uk/archives/2004/11/09/the-css-anthology/">first edition of The CSS Anthology with Sitepoint</a>. The idea for the book was to take the entire CSS 2.1 specification, and come up with 101 examples to show people how to use all of the CSS that existed. I have a copy on my shelf, but recently discovered you can read it on <a href="https://archive.org/details/cssanthology00rach_0/mode/2up">archive.org</a>.</p>



<p class="wp-block-paragraph">Despite the personal horror of realising that book <em>is old enough to buy alcohol in the USA</em>, it’s fun to look at as a snapshot of a certain point in the history of the web. In 2004 many sites were still laid out with tables, several of the examples explain how to do a certain layout “without tables”. It’s also notable how I refer throughout to specific browser versions, because in 2004 a browser version would last for at least a year and usually much longer. Chrome is about to begin a two-week release cycle, we’ve come a long way.</p>



<p class="wp-block-paragraph">It’s also interesting how long we had to wait for interoperable, robust ways to achieve some of the solutions. On page 328 I explain how to create coloured scrollbars, using the <code>scrollbar-*</code> that only worked in Internet Explorer. Over 20 years later the <code><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/scrollbar-color">scrollbar-color</a></code> property became Baseline Newly available.</p>



<p class="wp-block-paragraph">I ultimately published four editions of The CSS Anthology. People still tell me how they learned CSS from that book, something which I find amazing and wonderful. I often wonder what a modern version of that book would look like, and how big it would need to be to attempt to cover all of CSS in 2026!</p>
]]></description>
      <pubDate>Thu, 23 Apr 2026 07:40:29 +0000</pubDate>
      <link>http://rachelandrew.co.uk/archives/2026/04/23/what-would-a-2026-css-anthology-look-like/</link>
      <dc:creator>Rachel Andrew</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5202146908</guid>
    </item>
    <item>
      <title><![CDATA[I got tired of correcting machines, so I gave them five rules]]></title>
      <description><![CDATA[<p>Although I feel deeply conflicted, I’ve been using AI coding tools more often to speed up particular aspects of development. Recently, I added an <code>AGENTS.md</code> to my setup so I don’t need to repeat my preferences.</p> <figure>
<img src="https://stuffandnonsense.co.uk/content/img/2026-04-08.png" alt="Hello fleshy ones">
<figcaption>Judge Dredd. Robot Wars. 2000AD comic.</figcaption>
</figure>

<p>I started using ChatGPT and DeepSeek to help me fix bugs and debug error messages, especially when I was new to working with Eleventy. I asked them questions, pasted in code, and then copied the result back into my project. Now, I use Codex to clone my boilerplate files and set up new projects. But whenever I started something new, I had to tell the machine my preferences again.</p>

<p>Machines don’t write code the way I do, so I constantly needed to reorder CSS selectors. Machines use too many class attributes and overly specific selectors, and don’t format CSS and HTML the way I like. When I switched to Codex, I realised there was a way to stop repeating myself by telling it ways to make the machine’s code more like mine.</p>

<h2>The AGENTS.md file</h2>

<p>Codex supports files called <code>AGENTS.md</code>, which is to a machine what a README is to fleshy ones. It can tell an agent what I care about and how I write code. It also defines a set of rules which prevent me from repeating myself at the start of every project.</p>

<p>You can add an <code>AGENTS.md</code> file to any project repo or put one into Codex’s hidden global config folder, which is in the root of your user directory (on a Mac.) Putting preferences there means that every new project starts using them.</p>

<pre><code>~/.codex/AGENTS.md</code></pre>

<p>I included a list of five things I was constantly correcting and needed the machine to stop doing. Here’s my global file:</p>

<pre><code># AGENTS.md## Core rules

- Prefer plain CSS and vanilla JavaScript.
- Prefer inline SVG when control, styling, or reuse matter.
- Use native platform features. Avoid dependencies and libraries.
- Use semantic HTML.

## CSS rules
- Don’t indent CSS declarations.
- Don’t use the `transform` shorthand property. 
- Keep the closing curly brace on the same line as the last property.
- Order all CSS properties alphabetically.
- Prefer shallow selectors and avoid deep selector chains.
- Use individual transform properties (`rotate`, `scale`, `translate`).

## Motion
- Always respect `prefers-reduced-motion`.
- Motion should be subtle, purposeful, and non-disruptive.
- Use CSS for animation and JavaScript for triggers.

## Writing
- Use British English.
- Use sentence case for headings, interface copy, and labels.</code></pre>

<p>Quickly running through that list:</p>

<p>1. By telling Codex I prefer native platform features, I get solutions that rely more on CSS, which is faster and much easier for me to work with.</p>

<p>2. Machines will also happily wrap everything in <code>&lt;div&gt;</code> elements unless I tell it otherwise. I want meaningful structure, so I ask it to prioritise semantic elements like <code>&lt;article&gt;</code>, <code>&lt;header&gt;</code>, <code>&lt;footer&gt;</code>, and <code>&lt;section&gt;</code>. I also work mostly with inline SVG because I need control over animation and styling, so I make that explicit too.</p>

<p>3. I’m especially picky about how I write CSS. Nothing AI did was technically incorrect; it just wasn’t my way of doing things. My AGENTS file tells the machine to write properties in alphabetical order, so I can scan them more quickly. I also want consistent formatting and simple selectors, so code stays easy to read.</p>

<p>4. When I animate, I’m often aiming for ambient motion to add atmosphere. I also need animations that respect someone’s motion preferences, not as an afterthought.</p>

<p>5. Finally, when adding UI copy such as button text and form labels, I want British English spelling in sentence case.</p>

<hr>

<h2>AGENTS.md as a project brief</h2>

<p>A global AGENTS file gets used by every project, but putting one in a project repo makes it act more like a brief. Here’s the boilerplate file I use with Eleventy projects:</p>

<pre><code># AGENTS.md## Overview
- This is an Eleventy project.
- Follow existing patterns in templates, styles, and naming.
- Make the smallest change that solves the problem cleanly.
- Don’t write new code unless absolutely necessary.

## Structure
- `src/` contains all source files.
- `src/_includes/` contains layouts and partials.
- `src/css/` contains global and component styles.
- `src/js/` contains small progressive-enhancement scripts.

## Working approach
- Don’t refactor unrelated code.
- Reuse existing patterns before creating new ones.

## HTML
- Don’t add unnecessary wrapper elements.
- Reuse existing partials where possible.
- Use semantic HTML elements.

## CSS
- Extend existing styles before creating new ones.
- Follow the global AGENTS.md for formatting and style rules.
- Keep selectors shallow and readable.
- Place media queries at the end of stylesheets.
- Use `.alt-*` for variations and `.item-*` for content types when needed.

## JavaScript
- Always use progressive enhancement.
- Don’t use JavaScript if HTML and CSS can solve the problem.
- Keep scripts small and vanilla.
- Scope behaviour to the relevant component or page.

## Eleventy
- Don’t duplicate already defined logic.
- Reuse existing collections, data files, and filters.
- Check if the content fits an existing collection before creating a new one.

## Verification
- Run the local build before finishing.</code></pre>

<hr>

<h2>Wrapping up</h2>

<p>I started using AI coding tools to free up time by avoiding repetitive tasks. AGENTS files define the rules the machine should follow, so I don’t have to also spend time editing its code to suit my style.</p>

<ul>
<li><a href="https://gist.github.com/malarkey/0eb8aba779811d55ca9060ef2e371bb9">Global AGENTS.md</a></li>
<li><a href="https://gist.github.com/malarkey/3834149866c0dcd436ed14c84f411b2b">AGENTS.md project brief</a></li>
</ul>]]></description>
      <pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://stuffandnonsense.co.uk/blog/i-got-tired-of-correcting-machines-so-i-gave-them-five-rules</link>
      <dc:creator>Stuff &amp; Nonsense Ltd. Blog feed</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5185120028</guid>
    </item>
    <item>
      <title><![CDATA[Pretext review]]></title>
      <description><![CDATA[<p>This weekend a project called <a href="https://github.com/chenglou/pretext">pretext</a> started making waves by <a href="https://github.com/chenglou">Cheng Lou</a>. Here’s a gif that has been blowing up social media:</p>
<p><img src="https://blog.damato.design/images/pretext-dragon.gif" alt="Directing a dragon with your cursor through a body of text"></p>
<p>There’s no doubt about how impressive this demo is. The speed in which the text moves out of the way of the dragon is pretty incredible. As expected, many people are saying this is the end of CSS. So, let’s talk about it and address <a href="https://chenglou.me/pretext/">the demos provided that showcase some of the features</a> in the lens of user experience.</p>
<h2 id="accordion">Accordion</h2>
<p>The first demo is of an accordion showcasing how the sections open and close smoothly without CSS hacks or measuring the DOM. This has been true for many years, we’ve wanted to animate from an intrinsic height for a long time. Well, we can do that now with pure CSS, in a few different ways.</p>
<p>The first way was maybe a hack, which was to <a href="https://nemzes.net/posts/animating-height-auto/">animate the grid row</a>. This was discovered 5 years ago. The difficulty with this approach is that it doesn’t work well with the native <code>&lt;details&gt;</code> element.</p>
<p>The second way is to use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/interpolate-size"><code>interpolate-size: allow-keywords</code></a>. This allows you to animate between a <code>&lt;length-percentage&gt;</code> value and an intrinsic size, <code>fit-content</code>, or <code>max-content</code>. This allows you to animate between <code>0</code> and <code>auto</code> for the height.</p>
<p>And finally, we have <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/calc-size"><code>calc-size()</code></a> which is a function I recently used on the new <a href="https://mode.place">Mise en Mode</a> website to get the <code>auto</code> size as a number that can be used in other functions, such as <code>round()</code>. This also calculates the intrinsic size of the content, so it can be used to animate from <code>0</code> to <code>auto</code> as well.</p>
<p>As you might see when reviewing the MDN pages of these new CSS features, they don’t all have baseline availability yet. But do you really need JavaScript in order to animate the accordion? I’d argue you don’t, if you’re willing to put in a little extra effort learning what CSS has, and not ship some extra JavaScript for the sake of this.</p>
<h2 id="bubbles">Bubbles</h2>
<p>This is an interesting one. This is showcasing something called the “shrinkwrap” problem that is now more common than ever in a world of web chats. The problem is that CSS text wrapping isn’t efficient. The bubble that it leaves will have extra space not taken by text. What would be preferred is for the bubble to only be as big as the text needs to be. So without the shrinkwrap, each bubble is the same width. Meanwhile, with shrinkwrap, the bubbles match the size of the text leaving no extra space.</p>
<p><img src="https://blog.damato.design/images/bubble-wasted-pixels.png" alt="Wasted pixels on text bubbles without shrinkwrap"></p>
<p>There is a <a href="https://kizu.dev/shrinkwrap-problem/">CSS solution found by Roman Komarov</a> to help with this problem which uses the new anchor positioning features. It also uses some pseudo element tricks to get the effect to behave. This is a pretty clever solution, but it is also pretty complex. It also doesn’t work in all browsers yet.</p>
<p>The question is, do you want to bring in a JavaScript library just to get your text bubbles to shrinkwrap? Is this going to improve the user experience or is it an attention to detail that is not worth the cost of the extra JavaScript? I would argue the latter. The shrinkwrap problem is a problem, but it’s not a problem that is worth the cost of bringing in a JavaScript library to solve it. It’s a nice to have, but it’s not a must have.</p>
<h2 id="dynamic-layout">Dynamic Layout</h2>
<p>This looks like a magazine or editorial layout which has some images that are effecting the placement of the text. When I first saw this, I immediately thought of the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/shape-outside"><code>shape-outside</code></a> property which allows you to wrap text around a shape. Honestly, I haven’t used this CSS property much but I do know it takes some setup in order for it to work. You’d need to have an alpha of the shape so that text can wrap around it. The object needs to be set as a <code>float</code> in order for the wrapping to occur. Placing that element could be a pain, requiring additional margins to be added, most likely with <code>shape-margin</code>.</p>
<p>What CSS won’t accomplish is the rotation of the images. For example, setting <code>rotate: 45deg</code> on the image doesn’t affect the text wrapping around it. In the pretext demo, rotating <em>does</em> affect the text. Ultimately, I imagine you could maybe animate a very complex polygon of points to follow on an interaction. But the reality here is that if you want this effect, JavaScript is the way to go.</p>
<p>But do you want this effect?</p>
<p>Even without the animation, when text doesn’t consistently start at the same place, it can become tiring to read. This is why we don’t typically center body text. Our eye wants to align to the left (or right in RTL languages) and when it can’t, it can be a bit of a strain. The additional rotation that can occur would make catching the word you were trying to read difficult since all the lines are recalculating their position on every frame. This is a fun effect, but it’s not something that I would add to an article because it’ll distract from the content and make it harder to read.</p>
<h2 id="variable-typographic-ascii">Variable Typographic ASCII</h2>
<p>As you might imagine, this isn’t meant for you to read the text. This is instead a artistic effect that uses glyphs to create a visual piece of art. Since there isn’t anything here that is specifically UX related, I’ll refrain from commenting on it, past hoping that someone puts an <code>aria-hidden="true"</code> on it so that screen readers don’t try to read that unintelligible mess.</p>
<h2 id="editorial-engine">Editorial Engine</h2>
<p>Similar to the <a href="https://blog.damato.design/posts/pretext-review#dynamic-layout">dynamic layout</a>, this is a layout that has some images that are effecting the placement of the text. The difficulty in reading the text is much more apparent here because the balls are constantly moving around making the location of every word change on every frame. As said earlier, not something I’d use for something I’d want someone to read.</p>
<h2 id="justification-comparison">Justification Comparison</h2>
<p>I have to admit this has some practical use. The justification algorithm in CSS leaves a lot to be desired. It doesn’t do a good job of balancing the space between words and letters, which leads to large gaps appearing within the text called rivers. The pretext demos show a better way of balancing the text such that the rivers are reduced, even having tighter algorithms available.</p>
<p>Though, one thing you’ll need to be careful of is the introduction of hyphenation. CSS has a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/hyphens"><code>hyphens</code></a> property that allows you to loosely control when words are hyphenated. If you have a better justification algorithm, you might find that you need to hyphenate more often in order to get the best results. This is something to keep in mind when considering this approach as I did when typesetting the book <a href="https://mode.place">Mise en Mode</a> and ultimately decided to avoid justifying the text because of the potential for rivers and the need for hyphenation. I wanted to avoid both of those things in order to provide the best reading experience possible. I kept the content as the primary focus in a way that was familiar without stressing over the details of justification. Curating every small hyphen is very tedious. I personally rather keep the words intact and not have full justification.</p>
<h2 id="rich-text">Rich Text</h2>
<p>Again, I think this does have some practical use. <a href="https://blog.damato.design/posts/book-authoring-backstage/">I’ve recently written about the difficulty in wrapping inline code accurately in CSS.</a> In this demo, the inline code does wrap in a reasonable way but I’ll also say there’s configurations that make the wrapping a bit too eager.</p>
<p><img src="https://blog.damato.design/images/eager-wrapping.png" alt="Wrapping has occurred too soon breaking the word docs"></p>
<p>It might make some sense to add some <code>&lt;wbr&gt;</code> spaces into the code at the right places. This could be done in a preprocessing step instead of on the client where the <code>pretext</code> project needs to run.</p>
<h2 id="masonry">Masonry</h2>
<p>Ah yes, the layout everyone is waiting for. While <code>display: grid-lanes</code> isn’t the property value we might have been expecting, the industry has been asking for a masonry layout for a long time. The pretext demo shows how to achieve this layout with JavaScript.</p>
<p>While we’ve known that we’ve needed JavaScript to have dynamic masonry layouts <a href="https://masonry.desandro.com/">for a long time</a>, there’s something you need to consider when implementing them; reading order.</p>
<p>When you look at a masonry layout, what is the order of the boxes meant to be read in? Is it top to bottom, left to right? Or is it left to right, top to bottom? Maybe it changes due to one box being longer than another. If you inspect the demo you’ll see that some of the items are inserted in places that aren’t in any clear order. It is most likely using a dense packing algorithm to efficiently use the available space, but this isn’t necessarily how the items are meant to be consumed.</p>
<p>The new <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/reading-flow"><code>reading-flow</code></a> property in CSS is meant to help suggest how these items are meant to be read. I’ll say with the amount of consideration the <a href="https://www.w3.org/TR/css-grid-3/">CSSWG has put into <code>display: grid-lanes</code></a>, I’d trust that they’ve thought more about how masonry layouts should be managed more than “whatever fits best”.</p>
<h2 id="what-is-pretext-for">What is pretext for?</h2>
<p>It’s for people who want more control over how text flows; either for artistic purposes or for typesetting designers who crave the tools traditional print design has had for a long time. Just remember <strong>the web is not print</strong>. Text has never animated on a magazine page, nor has that magazine page needed to download additional code in order for the layout to behave. These are different mediums even if we’ve made analogs to them over the years.</p>
<p>I’m certainly not saying to avoid using the library, I think it’s fun and does have some practical use cases. But you should first consider your users and whether the features of this library are worth the cost of the extra JavaScript and the potential decrease in user experience. If you do decide to use it, make sure to use it in a way that enhances the content and doesn’t distract from it.</p>
<p>This certainly isn’t the end of CSS, it’s showing what’s next.</p>]]></description>
      <pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://blog.damato.design/posts/pretext-review</link>
      <dc:creator>blog.damato.design</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5175001181</guid>
    </item>
    <item>
      <title><![CDATA[WordPress Everywhere]]></title>
      <description><![CDATA[
<p class="wp-block-paragraph">As we <a href="https://wordpress.org/news/2026/03/announcing-my-wordpress/">announced</a> and <a href="https://techcrunch.com/2026/03/11/wordpress-debuts-a-private-workspace-that-runs-in-your-browser-via-a-new-service-my-wordpress-net/">TechCrunch covered</a>, <a href="https://my.wordpress.net/">my.wordpress.net</a> has soft-launched.</p>



<p class="wp-block-paragraph">What this means is you need to fundamentally shift how you think about WordPress. </p>



<p class="wp-block-paragraph">From the beginning, WordPress has always been open source, giving you freedom, liberty, autonomy, and digital sovereignty. <a href="https://automattic.com/creed/">Open source is the most powerful idea of our generation</a>.</p>



<p class="wp-block-paragraph">For the past few decades, WordPress was software you got from a cloud provider or web host, such as <a href="https://wordpress.com/">WordPress.com</a>, <a href="https://www.bluehost.com/">Bluehost</a>, <a href="https://www.hostinger.com/">Hostinger</a>, or <a href="https://pressable.com/">Pressable</a> (the currently <a href="https://wordpress.org/hosting/">recommended WordPress hosts</a>). You could self-host it on a Raspberry Pi or home server, but few people did.</p>



<p class="wp-block-paragraph">The experience of <a href="https://wordpress.org/download/">downloading WordPress</a>, as my Mom did, is that it unzips a bunch of PHP and various code files onto your desktop. Very confusing!</p>



<p class="wp-block-paragraph">But now, thanks to incredible advances in <span style="box-sizing: border-box; margin: 0px; padding: 0px;"><a href="https://webassembly.org/" target="_blank">WebAssembly (WASM)</a>, we can spin up a web server, a database (SQLite or MariaDB), and a full WordPress installation</span> inside your browser in about 30 seconds. Instantly. No server needed. <a href="https://youtu.be/VeigCZuxnfY?t=2914">I introduced Playground at State of the Word in 2022</a>.</p>



<p class="wp-block-paragraph">You can even use it to cross-publish apps to the web, desktop, and iOS, like <a href="https://wptavern.com/blocknotes-app-runs-wordpress-natively-on-ios-now-in-public-beta">Blocknotes did in 2023</a>. You <a href="https://blocknotes.org/">can get the latest Blocknotes at Blocknotes.org</a>. One codebase, multiple platforms.</p>



<p class="wp-block-paragraph">These <a href="https://wordpress.org/playground/">WordPress Playground </a>containers are fully composable and atomic. You can track and roll back any change. Undo for everything. Stop thinking of WordPress as just on a web host and worrying about maintenance and management, and more as a self-contained unit of open source goodness, a fun little package where you own and control the code and data and can run it however you like.</p>



<p class="wp-block-paragraph">How perfect is that for AI to work with? <a href="https://wordpress.org/playground/">Playground</a> makes WordPress local, fast, and trivial to spin up multiple instances, test code changes, and save them.</p>



<p class="wp-block-paragraph">Next up, we’re going to add peer-to-peer sync, version control integration, and cloud publishing so other people can access it.</p>



<p class="wp-block-paragraph">I believe this will take us <a href="https://w3techs.com/technologies/history_overview/content_management/all/y">from millions of WordPresses</a> in the world to billions. Hosting isn’t going away; in fact, I think demand for cloud syncing will increase drastically as we radically open up what people can build on top of WordPress.</p>



<p class="wp-block-paragraph">In an AI age where it’s trivial to spin up software from scratch, consumers will have to give much more thought to brands they trust to be in it for the long term. We’ve been relentlessly iterating on WordPress since 2003. I plan to work on it the rest of my life, and there’s a broad community of hundreds of thousands, if not millions, of people who make their living on top of WordPress. </p>



<p class="wp-block-paragraph">On WordPress.com we offer <a href="https://wordpress.com/100-year/">100-year plans</a> and <a href="https://wordpress.com/100-year-domain/">100-year domains</a>, and I believe we’re one of the few companies where that’s credible. It’s led by <a href="https://rosefutures.com/">Zander Rose</a>, who ran the <a href="https://longnow.org/">Long Now Foundation</a> (one of my favorite non-profits) from 1997 to 2023, a quarter century.</p>



<p class="wp-block-paragraph">In core WordPress, we are <em>obsessed</em> with backwards compatibility. You can run plugins and themes written 20 years ago on today’s WordPress. I’ve stumbled on decade-old installs, and the built-in auto-upgrade took everything to the newest version.</p>



<p class="wp-block-paragraph">At <a href="https://automattic.com/">Automattic</a>, for better and worse, <a href="https://killedbygoogle.com/">unlike Google</a>, we almost never shut things down. We obsess about <a href="https://blog.archive.org/2026/02/04/inside-the-new-wayback-machine-plugin-for-wordpress/">maintaining or redirecting permalinks</a>. We make it easy not just to get your data in, but take it out too. We build businesses that lower churn not by locking you in (<a href="https://support.wix.com/en/article/exporting-or-embedding-your-wix-site-elsewhere">Wix famously has no export</a>) but by making it easy for you to leave. <a href="https://www.youtube.com/watch?v=LSGl3d4KOMk">If you love somebody, set them free</a>.</p>



<p class="wp-block-paragraph">In the next few years, there will be a <a href="https://en.wikipedia.org/wiki/Cambrian_explosion">Cambrian explosion</a> of software and services. You’re going to have a lot of choices about where to put your most precious data and software. You should demand open source and bet on those who are clearly in it for the long-term.</p>



<p class="wp-block-paragraph">Today, everyone gets a phone number and email when they grow up. That will expand in the future, everyone will have <a href="https://wordpress.com/domains/">a domain</a> and a WordPress. A part of the internet that you own.</p>



<p class="wp-block-paragraph">Technology is best when it brings people together. Technology is best when it puts you in control, gives you ownership, digital autonomy, freedom, and liberty. That’s open source. It’s so exciting to see how AI is supercharging open source.</p>



<p class="wp-block-paragraph"><a href="http://make.wordpress.org/">Join the WordPress community</a>. It’s fun! We have cookies that don’t track you. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;"></p>
]]></description>
      <pubDate>Thu, 12 Mar 2026 02:07:19 +0000</pubDate>
      <link>https://ma.tt/2026/03/wordpress-everywhere/</link>
      <dc:creator>Matt Mullenweg</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5150999567</guid>
    </item>
    <item>
      <title><![CDATA[The geolocation element is odd]]></title>
      <description><![CDATA[
                                <p>Imagine the following scene: you're on a website, and there's a button. When you click the button, it shows some text, but the website's font size is really small. So, you change the default font size in your browser from 16 to 24 pixels. You go back to the website and click the button again, but it doesn't work now. That's not just very unfortunate, but it's also absolutely unexpected, confusing, and irritating.<br>
With that picture in mind. Let's talk about some things that I find odd about the geolocation element.</p><h2 id="first-impression">First impression</h2>
<p>When I first read about the geolocation element, my initial reaction was surprise. I was surprised to find a dedicated element for obtaining the user's location. I thought that could've also been a relatively simple web component instead.<br>
On the <a href="https://developer.chrome.com/">Chrome for Developers</a> blog, the authors explain that there are three main reasons for the existence of the element.</p>
<ul>
<li><strong>Clear intent and timing</strong> - By clicking a button, users show clear intent. That's in contrast to how it works on many websites, where, out of nowhere, they will automatically ask you to share your location. Users often dismiss or decline these requests, which can be problematic because it's not easy to change that decision later.</li>
<li><strong>Simplified recovery</strong> - If a user or browser has previously blocked access to a location, the element enables them to change their settings.</li>
<li><strong>Automatic refresh</strong> - If a permission has already been granted, users can click the button to update it.</li>
</ul>
<p>I wasn't 100% convinced, but the recovery and refresh features sold it to me. Still, I didn't think I would use this element much because I don't need to know the user's location very often. That's, of course, my problem.</p>
<p>When I learned everything I wanted to know, I wrote a simple <a href="https://www.matuzo.at/blog/2026/geolocation-element">introductory post about the element</a> on my website. Five weeks later, I'm writing another post because I've learned some more surprising and strange things.</p>
<h2 id="styling-constraints">Styling constraints</h2>
<p>You can style the geolocation element, but there are styling constraints to ensure user trust and prevent deceptive design patterns. I understand why these restrictions exist, but what confuses me is how they're implemented. They come in three flavours.</p>
<ul>
<li>Enforced user agent styles</li>
<li>Enforced user agent bounds</li>
<li>Element deactivation</li>
</ul>
<h3 id="enforced-user-agent-styles">Enforced user agent styles</h3>
<p>There are certain rules you can't overwrite because they are marked as <code>!important</code> in the <a href="https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/html/resources/capability_element.css#L15">user agent style sheet</a>. For example, you can't change the <code>box-sizing</code> property.</p>
<pre><code class="language-css">permission, geolocation, install, usermedia {
  background-color:#efefef;
  border: solid 1px #767676;
  box-sizing: content-box !important;
  …
  word-spacing: normal;
  vertical-align: middle !important;
 }</code></pre>
<p>That's not new. We already know this from other elements, like some form elements.</p>
<h3 id="enforced-user-agent-bounds">Enforced user agent bounds</h3>
<p>Other rules are not important, but they have style bounds. For example, you can define a <code>border-width</code>, but the value must be between <code>0</code> and <code>0.6em</code>. </p>
<p>Basically, anything that makes your button not look like a button is either prohibited or restricted. I understand that, and you also have enough properties at your disposal to change the design of your button to match your theme, but where it gets really strange is the third measure.</p>
<h3 id="element-deactivation">Element deactivation</h3>
<p>I can live with enforced user agent styles and bounds, but if you define certain rules or do certain things, the element will be deactivated, and it won't work anymore.</p>
<p>The element just stops working without letting the user know when:</p>
<ul>
<li>The contrast ratio is below 3:1</li>
<li>The font size is smaller than the computed value of <code>small</code> or larger than the computed value of <code>xxx-large</code>. By default, with a base font size of 16px, that's 12px and 48px.</li>
<li>If more than 5px of the element is not inside the viewport. For example, if you move the element using a negative margin or transform, or if you only scroll the page a bit too much out of the viewport. (The 5px is based on my tests; it could also be a relative value I'm not aware of)</li>
<li>If more than 5px of the element is covered by another element, e.g., a sticky header.</li>
</ul>
<div class="highlight demo-parent">
<h3 id="demo">Demo</h3>
<p>
Try it yourself by increasing the font size using the range slider and then clicking the button. (You may have to press the button twice when you switch from a too large to a smaller font size. I have no idea why.)
</p>
<p>
  <label for="fz">Font-size</label>
  <input type="range" id="fz" min="19" max="70" step="1" value="19">
</p>
<div>
<geolocation id="btn2">
  <strong>Your browser doesn't support &lt;geolocation&gt;. Try Chrome 144+</strong>
</geolocation><br><br>
<div role="status">Font size: <span class="fontsize">19px</span></div>
<div role="status">Your location: <span class="location">press the button</span></div>
</div>
</div>
<script>
document.querySelector('#btn2').addEventListener('location', e => {
   let geolocation = ('HTMLGeolocationElement' in window);
  if (geolocation) {
    document.querySelector('.location').textContent = `${e.target.position.coords.latitude} / ${e.target.position.coords.longitude}`
  }
})
document.querySelector('#fz').addEventListener('input', e => {
  document.querySelector('#btn2').style.fontSize = `${e.target.value}px`
  document.querySelector('.location').textContent = 'press the button again'
  document.querySelector('.fontsize').textContent = `${e.target.value}px`
  if (e.target.value > 48) {
    document.querySelector('.demo-parent').style.background = 'oklch(0.63 0.26 29.23 / 0.23)'
  } else {
    document.querySelector('.demo-parent').style.removeProperty('background');
  }
});
</script>
<p>Coming back to the intro of the post. If the font size of the geolocation is set to <code>2rem</code> in the author style sheet and the user sets their browser's default font size to 25px, they won't be able to use the element just because they need a larger font size. The worst thing about this is that there is no visual feedback and no console error. You'll find an error message inside the Issues panel, but how many of you actually use the Issues panel, and how many of your users do?</p>
<p>I just can't understand how anyone could think that this was a good idea.</p>
<p>That's by far the worst thing of all the things I find strange about the element, but there's more.</p>
<h2 id="javascript-dependency">JavaScript dependency</h2>
<p>There aren't many HTML elements that fully rely on JavaScript to do anything meaningful (e.g., canvas). Geolocation is one of them. If I want to use the user's location, I have to attach an EventListener in JavaScript. I can't just process the data inside a form and pass it to a script via GET or POST. That's very unfortunate because if there's one thing I love about all these new features in HTML that replace custom JavaScript solutions (e.g., dialog with invoker commands, popover, details), it's that I don't need JavaScript at all.</p>
<p>PS: JavaScript dependency in HTML is actually an interesting topic. I'm working on a blog post about it.</p>
<h3 id="content-restrictions">Content restrictions</h3>
<p>The element comes with an icon and a text label. You can hide the icon and select a locale for the text via the <code>lang</code> attribute, but that's it. You cannot change the text because the browser determines it. It's also not possible to show only an icon, as many websites do in their custom solutions.</p>
<pre><code class="language-css">geolocation::permission-icon {
  display: none;
}</code></pre>
<pre><code class="language-html">&lt;geolocation lang="de"&gt;
  &lt;strong&gt;Ihr Browser unterstützt das &amp;lt;geolocation&amp;gt; Element nicht.&lt;/strong&gt;
&lt;/geolocation&gt;</code></pre>
<script>
const updateMap = e => {
  let coords;
  if (!('HTMLGeolocationElement' in window)) {
    coords = e.coords;
  } else {
    coords = e.target.position.coords
  }
  e.target.nextElementSibling.innerHTML =  `${e.target.position.coords.latitude} / ${e.target.position.coords.longitude}`
}
</script>
<div>
<style>
#btn3::permission-icon {
  display: none;
}
</style>
<geolocation lang="de" id="btn3">
  <strong>Ihr Browser unterstützt das &lt;geolocation&gt; Element nicht.</strong>
</geolocation>
<div role="status"></div>
</div>
<script>
const geo = document.querySelector('#btn3');
geo.addEventListener('location', updateMap)
</script>
<h3 id="dark-mode">Dark Mode</h3>
<p>At this point I'm unsure if this is a feature or just a bug, but the geolocation element doesn't support a dark color scheme.</p>
<pre><code class="language-html">&lt;div style="color-scheme:dark"&gt;
  &lt;geolocation&gt;&lt;/geolocation&gt;
  &lt;button&gt;I'm a regular button&lt;/button&gt;
&lt;/div&gt;</code></pre>
<div class="highlight" style="color-scheme:dark; background-color: #353232; color: #fff">
<h3 id="demo">Demo</h3>
  <geolocation></geolocation>
  <button>I'm a regular button</button>
</div>
<h2 id="conclusion">Conclusion</h2>
<p>Don't get me wrong, the element does the thing it's supposed to do well, but I can imagine that the styling and content restrictions will be a blocker for many people. Even if they can accept the limitations, they may not want to accept the possible issues that come with this horrible element deactivation feature. The least browsers should do is tell and show users that the button is deactivated and why. What I would find even better is to just define bounds for the font size instead of deactivating the element. For contrast, they could use the closest-matching combination for the text and background colors that has a ratio of at least 3:1.</p>
<p>I don't enjoy writing blog posts like this, but the element is in its current state not very useful. I've read the explainer and discussions on GitHub, I know that a lot of thoughts and work went into this, but the implementation feels a bit rushed.</p>
<h2 id="additional-resources">Additional resources</h2>
<ul>
<li><a href="https://issues.chromium.org/issues/491743372">geolocation doesn't respect dark mode/color scheme</a></li>
<li><a href="https://issues.chromium.org/issues/490290421">Deactivation of the geolocation element is unpredictable and confusing</a></li>
<li><a href="https://issues.chromium.org/issues/491711372">geolocation style constraints interfere with accessibility</a></li>
</ul><p>My blog doesn't support comments yet, but you can reply via <a href="mailto:blog@matuzo.at?subject=Comment+on+%E2%80%9CThe+geolocation+element+is+odd%E2%80%9D">blog@matuzo.at</a>.</p>        ]]></description>
      <pubDate>Wed, 11 Mar 2026 11:30:00 +0000</pubDate>
      <link>https://www.matuzo.at/blog/2026/geolocation-is-odd</link>
      <dc:creator>Manuel Matuzović - Blog</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5150513979</guid>
    </item>
    <item>
      <title><![CDATA[Still here]]></title>
      <description><![CDATA[
<p class="wp-block-paragraph">In January, I read Geoff Graham’s article, <a href="https://geoffgraham.me/changing-roles/">Changing Roles</a>. It dawned on him that somehow at sometime, he stopped being a web designer. It was not something he necessarily intended to happen. He took advantage of different opportunities and evolved into a new role. Web educator is the best way to describe his role these days.</p>



<p class="wp-block-paragraph">As I read Geoff’s article, it gave me a moment to pause. <strong>I am still here</strong>. I am still a front-end developer (even though my title is UX Developer). I am still here. And I don’t say that like a grump older person who has been in the role for a long time with a sense that I am stuck or that I am not thrilled about it.</p>



<p class="wp-block-paragraph">No, when I say “I am still here” it is very much an enthusiastic and very grateful tone. I feel incredibly blessed that “I am still here.” Why?</p>



<p class="wp-block-paragraph">Two years ago, I lost my job unexpected. And I spent the next twelve months looking for a new job. There were a lot of days during that unemployment that I thought my time building and crafting websites were over. There were days that I had to fight hard against discouragement to keep going. It took me quite a while just to start getting some interviews. And then I would get my hopes up only to be disappointed. Even a year ago today, I had some possibilities but still had this fear that my time in the Web industry might be over. Fighting for hope was a daily battle.</p>



<p class="wp-block-paragraph">I had no idea what I would do instead. I had invested twenty five years in this field. And I still enjoyed working on every project that came my way. I love crafting with code. And that was the hardest part of being unemployed. I was not motivated to do side projects because in a lot of ways, it was too painful. I missed the energy that doing creative work each day had given to me while I was employed. </p>



<p class="wp-block-paragraph">I entertained the idea of pivoting to something else or trying to retool. I bought <a href="https://www.joshwcomeau.com/">Josh Comeau</a>‘s <a href="https://www.joyofreact.com/">Joy of React</a> course and started learning React. But I didn’t necessarily see a way forward in that lane as all the job postings that I saw were looking for someone with years of experience. And I struggled to think about being a React developer because of some fundamental philosophical/professional disagreements with frameworks as an approach to building the web. It seemed to go against <a href="https://gomakethings.com/what-is-the-grain-of-the-web/">the grain of the Web</a> not using the most up-to-date HMTL elements like dialog. I also had accessibility and progressive enhancement concerns.</p>



<p class="wp-block-paragraph">But I am still here. The timing of things came together and <em><a href="https://reviveourhearts.com">Revive Our Hearts</a></em> ministry needed someone with my skills. It was the <strong>right fit</strong> at the <strong>right time</strong>. It brought me back full circle. I got my start in web development while working for Campus Crusade for Christ (now Cru). Once again, I was going to be able to invest my skills in something I was very passionate about.</p>



<p class="wp-block-paragraph">There have been so many days, especially over the last six months, that I am just so grateful and appreciative of the fact that I still am here–that I still get to do the thing that I love doing and have built up so many skills and knowledge about. I am humbled when I see how many people are losing their jobs in the industry and still looking for new opportunities. I don’t take it for granted that I still get to do what I do. It is a privilege and a blessing.</p>



<p class="wp-block-paragraph">I don’t know how much longer it will last but I want to enjoy every minute of it. I thought about several scenes from the movie, <a href="https://www.imdb.com/title/tt0120363/">Toy Story 2</a>. Woody, a vintage toy cowboy, considers going to a museum as he begins to feel that his days as Andy’s beloved toy are numbered. But then later in the movie he comes to his senses.</p>



<p class="wp-block-paragraph">“I can’t prevent Andy from growing up but I wouldn’t miss it for the world.”</p>



<p class="wp-block-paragraph">After Buzz Lightyear, Woody’s space toy friend, asks Woody if he was still worried about Andy growing up. “Nah, it will be fun while it lasts.”</p>



<p class="wp-block-paragraph">I don’t know what is ahead for our industry. I know there is a lot of concern about how AI will change our jobs or whether there will be less jobs in the future because of AI. I still have my doubts about the “inevitability” of AI. But I am still not sure how to hedge my bets.</p>



<p class="wp-block-paragraph">But for now, “<strong>I am still here</strong>.” And I am grateful and trying to enjoy it as long as it lasts. And trusting God with what is next if it comes to that.</p>



<hr class="wp-block-separator has-alpha-channel-opacity">



<ul class="wp-block-list">
<li><a href="https://jeffbridgforth.com/day-366/">Day 366</a> – I accepted my current role almost a year ago on Day 366 of my unemployment</li>



<li><a href="https://jeffbridgforth.com/lessons-from-a-yearlong-job-search/">Lessons from a yearlong job search</a></li>



<li><a href="https://jeffbridgforth.com/two-powerful-investments/">Two powerful investments to make in your career</a></li>



<li><a href="https://jeffbridgforth.com/get-to-know-me-roh/">Get to know me: <em>Revive Our Hearts</em> Edition</a> – I put this together so that my new teammates could get to know me a bit better. I share a bit more about my journey into this new role.</li>



<li><a href="https://jeffbridgforth.com/six-months/">Six months</a> – Reflections about my first six months in my role with <em>Revive Our Hearts</em></li>



<li><a href="https://jeffbridgforth.com/2025-by-the-numbers/">2025 by the numbers</a> – My year-end review</li>



<li><a href="https://jeffbridgforth.com/a-decade-of-development-2010-to-2019/">A Decade of Development: 2010 to 2019</a></li>



<li><a href="https://jeffbridgforth.com/the-making-of-a-webcraftsman-part-1/">The making of a webcraftsman, part 1</a> – I need to write more chapters in this series. This covers 1999–2003.</li>
</ul>
<p>The post <a href="https://jeffbridgforth.com/still-here/">Still here</a> appeared first on <a href="https://jeffbridgforth.com">Jeff Bridgforth</a>.</p>
]]></description>
      <pubDate>Sun, 08 Mar 2026 22:46:25 +0000</pubDate>
      <link>https://jeffbridgforth.com/still-here/</link>
      <dc:creator>Jeff Bridgforth</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5146898496</guid>
    </item>
    <item>
      <title><![CDATA[Modern Web Weekly #68]]></title>
      <description><![CDATA[<meta charset="utf-8"><title>Modern Web Weekly #68</title><style>
@media (max-width: 1024px) {
  .typography .pullquote-align-left,
  .typography.editor .pullquote-align-left,
  .typography .pullquote-align-right,
  .typography.editor .pullquote-align-right,
  .typography .pullquote-align-wide,
  .typography.editor .pullquote-align-wide,
  .typography .pullquote-align-center,
  .typography.editor .pullquote-align-center {
    float: none;
    margin: 0 auto;
    width: 100%;
    max-width: 100%;
  }
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
  .typography .markup table.image-wrapper img,
  .typography.editor .markup table.image-wrapper img,
  .typography .markup table.kindle-wrapper img,
  .typography.editor .markup table.kindle-wrapper img {
    max-width: 550px;
  }
}
@media (min-width: 1024px) {
  .typography:not(:has(#toc)) .captioned-image-container figure:has(> a.image2-offset-left),
  .typography.editor:not(:has(#toc)) .captioned-image-container figure:has(> a.image2-offset-left) {
    margin-left: var(--image-offset-margin);
  }
  .typography:not(:has(#toc)) .captioned-image-container figure:has(> a.image2-offset-right),
  .typography.editor:not(:has(#toc)) .captioned-image-container figure:has(> a.image2-offset-right) {
    margin-right: var(--image-offset-margin);
  }
}
@media (min-width: 1300px) {
  .typography .captioned-image-container figure:has(> a.image2-offset-left),
  .typography.editor .captioned-image-container figure:has(> a.image2-offset-left) {
    margin-left: var(--image-offset-margin);
  }
  .typography .captioned-image-container figure:has(> a.image2-offset-right),
  .typography.editor .captioned-image-container figure:has(> a.image2-offset-right) {
    margin-right: var(--image-offset-margin);
  }
}
@media (max-width: 1024px) {
  .typography,
  .typography.editor {
    /* Disable offset on mobile/tablet */
  }
  .typography .captioned-image-container figure:has(> a.image2-align-left),
  .typography.editor .captioned-image-container figure:has(> a.image2-align-left),
  .typography .captioned-image-container figure:has(> a.image2-align-right),
  .typography.editor .captioned-image-container figure:has(> a.image2-align-right) {
    float: none;
    margin: 1em auto;
    max-width: 100%;
    width: auto;
    padding: 0;
  }
  .typography .captioned-image-container figure:has(> a.image2-align-left.thefp),
  .typography.editor .captioned-image-container figure:has(> a.image2-align-left.thefp),
  .typography .captioned-image-container figure:has(> a.image2-align-right.thefp),
  .typography.editor .captioned-image-container figure:has(> a.image2-align-right.thefp) {
    margin: 1em auto;
  }
  .typography .captioned-image-container figure:has(> a.image2-offset-left),
  .typography.editor .captioned-image-container figure:has(> a.image2-offset-left),
  .typography .captioned-image-container figure:has(> a.image2-offset-right),
  .typography.editor .captioned-image-container figure:has(> a.image2-offset-right) {
    margin: 1em auto;
  }
  .typography .captioned-image-container figure:has(> a.image2-align-left) .image2-inset,
  .typography.editor .captioned-image-container figure:has(> a.image2-align-left) .image2-inset,
  .typography .captioned-image-container figure:has(> a.image2-align-right) .image2-inset,
  .typography.editor .captioned-image-container figure:has(> a.image2-align-right) .image2-inset {
    display: block;
    justify-content: initial;
  }
}
@media (max-width: 768px) {
  .typography .markup div.sponsorship-campaign-embed,
  .typography.editor .markup div.sponsorship-campaign-embed {
    margin-top: 24px;
    margin-bottom: 24px;
  }
  .typography .markup div.sponsorship-campaign-embed:first-child,
  .typography.editor .markup div.sponsorship-campaign-embed:first-child {
    margin-top: 0px;
  }
}
@media screen and (max-width: 650px) {
  .typography .markup div.youtube-overlay,
  .typography.editor .markup div.youtube-overlay,
  .typography .markup div.vimeo-overlay,
  .typography.editor .markup div.vimeo-overlay {
    display: none !important;
  }
}
@media screen and (max-width: 370px) {
  .typography .markup div.tiktok-wrap,
  .typography.editor .markup div.tiktok-wrap {
    width: calc(95vw - 32px);
    height: calc((95vw - 32px - 2px) / 0.485714);
  }
}
@media screen and (max-width: 650px) {
  .typography .markup div.embedded-publication-wrap .embedded-publication.show-subscribe,
  .typography.editor .markup div.embedded-publication-wrap .embedded-publication.show-subscribe {
    padding: 24px;
  }
}
@media screen and (max-width: 650px) {
  .typography .markup div.subscription-widget-wrap .subscription-widget.show-subscribe,
  .typography.editor .markup div.subscription-widget-wrap .subscription-widget.show-subscribe,
  .typography .markup div.subscription-widget-wrap-editor .subscription-widget.show-subscribe,
  .typography.editor .markup div.subscription-widget-wrap-editor .subscription-widget.show-subscribe,
  .typography .markup div.captioned-button-wrap .subscription-widget.show-subscribe,
  .typography.editor .markup div.captioned-button-wrap .subscription-widget.show-subscribe {
    padding: 0px 24px;
  }
}
@media screen and (max-width: 650px) {
  .typography .markup div.subscription-widget-wrap .subscription-widget.show-subscribe .subscription-widget-subscribe .button,
  .typography.editor .markup div.subscription-widget-wrap .subscription-widget.show-subscribe .subscription-widget-subscribe .button,
  .typography .markup div.subscription-widget-wrap-editor .subscription-widget.show-subscribe .subscription-widget-subscribe .button,
  .typography.editor .markup div.subscription-widget-wrap-editor .subscription-widget.show-subscribe .subscription-widget-subscribe .button,
  .typography .markup div.captioned-button-wrap .subscription-widget.show-subscribe .subscription-widget-subscribe .button,
  .typography.editor .markup div.captioned-button-wrap .subscription-widget.show-subscribe .subscription-widget-subscribe .button {
    padding: 10px 12px;
    min-width: 110px;
  }
}
@media (max-width: 650px) {
  .typography .markup .twitter-embed,
  .typography.editor .markup .twitter-embed,
  .typography .markup .tweet,
  .typography.editor .markup .tweet {
    padding: 12px;
  }
}
@media (max-width: 650px) {
  .typography .markup .twitter-embed .tweet-text,
  .typography.editor .markup .twitter-embed .tweet-text,
  .typography .markup .tweet .tweet-text,
  .typography.editor .markup .tweet .tweet-text {
    font-size: 14px;
    line-height: 20px;
  }
}
@media (max-width: 650px) {
  .typography .markup .twitter-embed .tweet-photos-container.two,
  .typography.editor .markup .twitter-embed .tweet-photos-container.two,
  .typography .markup .tweet .tweet-photos-container.two,
  .typography.editor .markup .tweet .tweet-photos-container.two,
  .typography .markup .twitter-embed .tweet-photos-container.three,
  .typography.editor .markup .twitter-embed .tweet-photos-container.three,
  .typography .markup .tweet .tweet-photos-container.three,
  .typography.editor .markup .tweet .tweet-photos-container.three,
  .typography .markup .twitter-embed .tweet-photos-container.four,
  .typography.editor .markup .twitter-embed .tweet-photos-container.four,
  .typography .markup .tweet .tweet-photos-container.four,
  .typography.editor .markup .tweet .tweet-photos-container.four {
    height: 200px;
  }
}
@media (max-width: 650px) {
  .typography .markup .twitter-embed a.expanded-link .expanded-link-img,
  .typography.editor .markup .twitter-embed a.expanded-link .expanded-link-img,
  .typography .markup .tweet a.expanded-link .expanded-link-img,
  .typography.editor .markup .tweet a.expanded-link .expanded-link-img {
    max-height: 180px;
  }
}
@media (max-width: 650px) {
  .typography .markup .twitter-embed a.expanded-link .expanded-link-description,
  .typography.editor .markup .twitter-embed a.expanded-link .expanded-link-description,
  .typography .markup .tweet a.expanded-link .expanded-link-description,
  .typography.editor .markup .tweet a.expanded-link .expanded-link-description {
    display: none;
  }
}
@media screen and (max-width: 650px) {
  .typography .markup .apple-podcast-container,
  .typography.editor .markup .apple-podcast-container {
    width: unset;
  }
}
@media (max-width: 420px) {
  .typography .markup .install-substack-app-embed img.install-substack-app-embed-img,
  .typography.editor .markup .install-substack-app-embed img.install-substack-app-embed-img {
    margin: 0 auto 16px auto;
  }
}
@media (max-width: 420px) {
  .typography .markup .install-substack-app-embed .install-substack-app-embed-text,
  .typography.editor .markup .install-substack-app-embed .install-substack-app-embed-text {
    margin: 0 0 12px 0;
    max-width: 100%;
    width: auto;
    text-align: center;
  }
}
@media (max-width: 420px) {
  .typography .markup .install-substack-app-embed .install-substack-app-embed-link,
  .typography.editor .markup .install-substack-app-embed .install-substack-app-embed-link {
    display: flex;
    justify-content: center;
  }
}
@media screen and (min-width: 481px) {
  .share-button-container {
    height: 38px;
  }
}
@media screen and (min-width: 481px) {
  .share-button-container a.comment {
    height: 38px;
    line-height: 38px;
    padding-right: 10px;
  }
}
@media screen and (max-width: 480px) {
  .share-button-container .separator {
    display: block;
    margin: 0;
    height: 8px;
    border-left: none;
  }
}
@media screen and (max-width: 480px) {
  .share-button-container a.share.first img {
    padding-left: 0;
  }
}
@media screen and (min-width: 481px) {
  .share-button-container a.mobile {
    display: none !important;
  }
}
@media screen and (min-width: 541px) {
  .settings-add-pub-modal-wrapper .container .add-recommending-pub-modal-container {
    padding: 36px;
    height: 680px;
  }
}
@media screen and (min-width: 541px) {
  .settings-add-pub-modal-wrapper .container .add-recommending-pub-modal-container .footer {
    position: absolute;
    bottom: 36px;
    margin: 0px;
  }
}
@media screen and (max-width: 650px) {
  .header-anchor-parent {
    display: none;
  }
}
@media screen and (max-width: 768px) {
  .post {
    padding: 16px 0 0 0;
  }
}
@media screen and (max-width: 650px) {
  .post .post-header .post-label {
    margin-top: 8px;
  }
}
@media screen and (max-width: 650px) {
  .post .post-header .meta-author-wrap.alternative-meta .meta-right-column .post-meta {
    margin-top: 6px;
  }
}
@media screen and (max-width: 650px) {
  .post .footer-facepile-container {
    height: 64px;
    padding: 0 16px;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    width: 100%;
  }
}
@media screen and (max-width: 650px) {
  .post .post-footer.use-separators {
    justify-content: center;
  }
}
@media screen and (max-width: 650px) {
  .post .post-footer.next-prev {
    height: 64px;
    justify-content: space-between;
    box-sizing: border-box;
  }
}
@media screen and (max-width: 650px) {
  .post-contributor-footer .post-contributor-bio-table {
    display: block;
  }
  .post-contributor-footer .post-contributor-bio-table-row {
    display: flex;
    flex-direction: row;
  }
  .post-contributor-footer .post-contributor-bio-userhead-cell,
  .post-contributor-footer .post-contributor-bio-body-cell {
    display: block;
  }
  .post-contributor-footer .post-contributor-bio-body-cell {
    flex-grow: 1;
  }
  .post-contributor-footer .post-contributor-bio-body-table {
    display: block;
  }
  .post-contributor-footer .post-contributor-bio-body-table-row {
    display: block;
  }
  .post-contributor-footer .post-contributor-bio-copy-cell,
  .post-contributor-footer .post-contributor-bio-controls-cell {
    display: block;
  }
  .post-contributor-footer .post-contributor-bio-copy-cell {
    margin: 0 0 16px 0;
  }
  .post-contributor-footer .post-contributor-bio-controls-cell {
    width: auto;
  }
  .post-contributor-footer .post-contributor-bio-controls {
    margin: auto;
  }
  .post-contributor-footer .post-contributor-bio-controls .button.primary {
    width: 100%;
  }
  .post-contributor-footer .post-contributor-bio-text {
    font-size: 14px;
  }
}
@media screen and (min-width: 768px) {
  .post-silhouette {
    padding: 32px 0;
  }
}
@media screen and (max-width: 650px) {
  .post-silhouette .post-silhouette-title {
    margin-top: 10.44225025px;
    height: 120px;
  }
}
@media screen and (max-width: 650px) {
  .post-silhouette .post-silhouette-meta {
    width: 75%;
  }
}
@media screen and (max-width: 650px) {
  .post-silhouette .post-silhouette-meta.with-byline-image {
    margin: 20px 0;
  }
}
@media screen and (max-width: 650px) {
  .use-theme-bg .post-meta.alternative-meta .post-meta-item,
  .post-meta.alternative-meta .post-meta-item {
    padding-right: 16px;
  }
}
@media screen and (max-width: 370px) {
  .use-theme-bg .post-meta.alternative-meta .post-meta-item,
  .post-meta.alternative-meta .post-meta-item {
    font-size: 14px;
  }
}
@media screen and (max-width: 650px) {
  .use-theme-bg .post-meta.alternative-meta .post-meta-item.guest-author-publication,
  .post-meta.alternative-meta .post-meta-item.guest-author-publication {
    display: none;
  }
}
@media screen and (max-width: 370px) {
  .post-meta .post-meta-item .post-meta-button {
    height: 36px !important;
    /* important to override in-line height style on emails */
  }
  .post-meta .post-meta-item .post-meta-button .meta-button-label {
    display: none;
  }
  .post-meta .post-meta-item .post-meta-button > svg {
    margin-right: 0;
  }
}
@media screen and (max-width: 370px) {
  .post-meta .post-meta-item {
    font-size: 12px;
  }
}
@media screen and (max-width: 650px) {
  .post .floating-subscribe-button {
    bottom: 20px;
    right: 20px;
  }
}
@media (max-width: 1024px) {
  body .pullquote-align-left,
  body .pullquote-align-right,
  body .pullquote-align-wide,
  body .pullquote-align-center {
    float: none;
    margin: 0 auto;
    width: 100%;
    max-width: 100%;
  }
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
  body .markup table.image-wrapper img,
  body .markup table.kindle-wrapper img {
    max-width: 550px;
  }
}
@media (min-width: 1024px) {
  body:not(:has(#toc)) .captioned-image-container figure:has(> a.image2-offset-left) {
    margin-left: var(--image-offset-margin);
  }
  body:not(:has(#toc)) .captioned-image-container figure:has(> a.image2-offset-right) {
    margin-right: var(--image-offset-margin);
  }
}
@media (min-width: 1300px) {
  body .captioned-image-container figure:has(> a.image2-offset-left) {
    margin-left: var(--image-offset-margin);
  }
  body .captioned-image-container figure:has(> a.image2-offset-right) {
    margin-right: var(--image-offset-margin);
  }
}
@media (max-width: 1024px) {
  body {
    /* Disable offset on mobile/tablet */
  }
  body .captioned-image-container figure:has(> a.image2-align-left),
  body .captioned-image-container figure:has(> a.image2-align-right) {
    float: none;
    margin: 1em auto;
    max-width: 100%;
    width: auto;
    padding: 0;
  }
  body .captioned-image-container figure:has(> a.image2-align-left.thefp),
  body .captioned-image-container figure:has(> a.image2-align-right.thefp) {
    margin: 1em auto;
  }
  body .captioned-image-container figure:has(> a.image2-offset-left),
  body .captioned-image-container figure:has(> a.image2-offset-right) {
    margin: 1em auto;
  }
  body .captioned-image-container figure:has(> a.image2-align-left) .image2-inset,
  body .captioned-image-container figure:has(> a.image2-align-right) .image2-inset {
    display: block;
    justify-content: initial;
  }
}
@media (max-width: 768px) {
  body .markup div.sponsorship-campaign-embed {
    margin-top: 24px;
    margin-bottom: 24px;
  }
  body .markup div.sponsorship-campaign-embed:first-child {
    margin-top: 0px;
  }
}
@media screen and (max-width: 650px) {
  body .markup div.youtube-overlay,
  body .markup div.vimeo-overlay {
    display: none !important;
  }
}
@media screen and (max-width: 370px) {
  body .markup div.tiktok-wrap {
    width: calc(95vw - 32px);
    height: calc((95vw - 32px - 2px) / 0.485714);
  }
}
@media screen and (max-width: 650px) {
  body .markup div.embedded-publication-wrap .embedded-publication.show-subscribe {
    padding: 24px;
  }
}
@media screen and (max-width: 650px) {
  body .markup div.subscription-widget-wrap .subscription-widget.show-subscribe,
  body .markup div.subscription-widget-wrap-editor .subscription-widget.show-subscribe,
  body .markup div.captioned-button-wrap .subscription-widget.show-subscribe {
    padding: 0px 24px;
  }
}
@media screen and (max-width: 650px) {
  body .markup div.subscription-widget-wrap .subscription-widget.show-subscribe .subscription-widget-subscribe .button,
  body .markup div.subscription-widget-wrap-editor .subscription-widget.show-subscribe .subscription-widget-subscribe .button,
  body .markup div.captioned-button-wrap .subscription-widget.show-subscribe .subscription-widget-subscribe .button {
    padding: 10px 12px;
    min-width: 110px;
  }
}
@media (max-width: 650px) {
  body .markup .twitter-embed,
  body .markup .tweet {
    padding: 12px;
  }
}
@media (max-width: 650px) {
  body .markup .twitter-embed .tweet-text,
  body .markup .tweet .tweet-text {
    font-size: 14px;
    line-height: 20px;
  }
}
@media (max-width: 650px) {
  body .markup .twitter-embed .tweet-photos-container.two,
  body .markup .tweet .tweet-photos-container.two,
  body .markup .twitter-embed .tweet-photos-container.three,
  body .markup .tweet .tweet-photos-container.three,
  body .markup .twitter-embed .tweet-photos-container.four,
  body .markup .tweet .tweet-photos-container.four {
    height: 200px;
  }
}
@media (max-width: 650px) {
  body .markup .twitter-embed a.expanded-link .expanded-link-img,
  body .markup .tweet a.expanded-link .expanded-link-img {
    max-height: 180px;
  }
}
@media (max-width: 650px) {
  body .markup .twitter-embed a.expanded-link .expanded-link-description,
  body .markup .tweet a.expanded-link .expanded-link-description {
    display: none;
  }
}
@media screen and (max-width: 650px) {
  body .markup .apple-podcast-container {
    width: unset;
  }
}
@media (max-width: 420px) {
  body .markup .install-substack-app-embed img.install-substack-app-embed-img {
    margin: 0 auto 16px auto;
  }
}
@media (max-width: 420px) {
  body .markup .install-substack-app-embed .install-substack-app-embed-text {
    margin: 0 0 12px 0;
    max-width: 100%;
    width: auto;
    text-align: center;
  }
}
@media (max-width: 420px) {
  body .markup .install-substack-app-embed .install-substack-app-embed-link {
    display: flex;
    justify-content: center;
  }
}
@media screen and (min-width: 500px) {
  body .header a.logo {
    width: 42px;
    height: 42px;
    border-radius: 12px;
  }
}
@media screen and (max-width: 420px) {
  body .subscription-receipt table:first-of-type .subscription-amount .subscription-discount {
    width: 72px !important;
  }
}
@media screen and (min-width: 481px) {
  body .share-button-container {
    height: auto;
  }
}
@media screen and (max-width: 480px) {
  body .share-button-container .separator {
    display: block !important;
    margin: 0 !important;
    height: 8px !important;
    border-left: none !important;
  }
}
@media screen and (max-width: 650px) {
  .digest .item .post-meta-item.audience {
    display: none;
  }
}
@media screen and (min-width: 500px) {
  .digest-publication .logo img {
    width: 42px;
    height: 42px;
    border-radius: 8px;
  }
}
@media screen and (max-width: 650px) {
  .comments-page .container .comment-list .collapsed-reply {
    margin-left: calc(10 + 32px - 24px);
  }
}
@media screen and (max-width: 650px) {
  .comment > .comment-list {
    padding-left: 24px;
  }
}
@media screen and (max-width: 650px) {
  .finish-magic-login-modal .modal-content .container {
    padding: 24px 0;
  }
}
@media (max-width: 650px) {
  .reader2-text-b3 {
    line-height: 24px;
  }
}
@media screen and (max-width: 650px) {
  .reader2-text-h4 {
    line-height: 24px;
  }
}
@media screen and (min-width: 541px) {
  .user-profile-modal {
    padding-left: 12px;
    padding-right: 12px;
  }
}
@media screen and (max-width: 650px) {
  .subscribe-widget form.form .sideBySideWrap button.rightButton {
    padding: 10px 12px;
  }
}
@media screen and (min-width: 541px) {
  .pub-icon:hover .logo-hover,
  .feed-item-icon:hover .logo-hover {
    display: block;
  }
}
@media screen and (max-width: 650px) {
  .post-ufi.single-full-width-button .post-ufi-button-wrapper {
    width: 100%;
    padding: 16px;
  }
  .post-ufi.single-full-width-button .post-ufi-button-wrapper:empty {
    display: none;
  }
  .post-ufi.single-full-width-button .post-ufi-button {
    width: 100%;
    justify-content: center;
  }
}
@media screen and (max-width: 768px) {
  .file-embed-wrapper {
    padding: 0;
  }
}
@media screen and (max-width: 768px) {
  .file-embed-wrapper-editor {
    padding: 0;
  }
}
@media screen and (max-width: 768px) {
  .file-embed-wrapper-editor:active {
    padding: 0;
  }
}
@media only screen and (max-width: 650px) {
  .file-embed-button.wide,
  .file-embed-error-button.wide {
    display: none;
  }
}
@media only screen and (min-width: 630px) {
  .file-embed-button.narrow,
  .file-embed-error-button.narrow {
    display: none;
  }
}
@media screen and (min-width: 541px) {
  .audio-player-wrapper .audio-player {
    min-width: 500px;
  }
}
@media screen and (max-width: 650px) {
  .audio-player-wrapper .audio-player .audio-player-progress {
    border-left-width: 16px;
    border-right-width: 16px;
  }
}
@media screen and (max-width: 650px) {
  .audio-player-wrapper .audio-player .audio-player-progress .audio-player-progress-bar .audio-player-progress-bar-popup {
    top: -54px;
  }
}
@media screen and (max-width: 650px) {
  .audio-player-wrapper-fancy .audio-player .audio-player-progress {
    border-left-width: 16px;
    border-right-width: 16px;
  }
}
@media screen and (max-width: 650px) {
  .audio-player-wrapper-fancy .audio-player .audio-player-progress .audio-player-progress-bar .audio-player-progress-bar-popup {
    top: -54px;
  }
}
@media (min-width: 250px) {
  .audio-player-wrapper-fancy .audio-player {
    padding: 32px;
  }
  .audio-player-wrapper-fancy .audio-player .btn-group {
    display: flex;
  }
  .audio-player-wrapper-fancy .audio-player .btn-group .button:last-of-type {
    display: block;
  }
}
@media (min-width: 300px) {
  .audio-player-wrapper-fancy .audio-player .btn-group {
    display: block;
  }
  .audio-player-wrapper-fancy .audio-player .btn-group .button:first-of-type {
    display: block;
  }
}
@media (min-width: 350px) {
  .audio-player-wrapper-fancy .audio-player .audio-player-substack-logo {
    display: block;
  }
  .audio-player-wrapper-fancy .audio-player .audio-player-title {
    margin-top: 16px;
  }
  .audio-player-wrapper-fancy .audio-player .audio-player-hero-image-container {
    padding-top: 15%;
    width: 15%;
    display: block;
  }
  .audio-player-wrapper-fancy .audio-player .btn-group .button:first-of-type {
    display: block;
  }
  .audio-player-wrapper-fancy .audio-player .audio-player-substack-logo {
    display: block;
  }
}
@media (min-width: 350px) {
  .audio-player-wrapper-fancy .audio-player .audio-player-hero-image-container {
    padding-top: 25%;
    width: 25%;
    display: block;
  }
  .audio-player-wrapper-fancy .audio-player .btn-group {
    display: flex;
  }
  .audio-player-wrapper-fancy .audio-player .btn-group .button:first-of-type {
    display: block;
  }
}
@media (min-width: 400px) {
  .audio-player-wrapper-fancy .audio-player .audio-player-hero-image-container {
    padding-top: 40%;
    width: 40%;
  }
}
@media (max-width: 400px) {
  .audio-player-wrapper-fancy .audio-player .btn-group {
    margin-top: 12px;
  }
  .audio-player-wrapper-fancy .audio-player .btn-group .button {
    font-size: 13px;
    padding: 6px 12px;
    height: auto;
    margin-top: 10px;
  }
}
@media (min-width: 600px) {
  .audio-player-wrapper-fancy .audio-player .audio-player-hero-image-container {
    padding-top: 55%;
    width: 55%;
  }
}
@media (max-width: 650px) {
  .poll-editor-modal {
    min-width: calc(100% - 20px);
  }
}
@media (max-width: 750px) {
  .poll-embed .poll-anchor-target .poll-anchor-copy-button {
    left: 8px;
    top: 45px;
  }
}
@media not all and (min-resolution: 0.001dpcm) {
  @supports (-webkit-appearance: none) {
    p a:not(.primary.button),
    .post p:not(.button-wrapper) a:not(.primary.button),
    .footnote a.footnote-anchor:not(.primary.button),
    .thread-head .markup p:not(.button-wrapper) a:not(.primary.button) {
      color: #51b4e9;
      text-decoration: none;
    }
    p a:not(.primary.button):hover,
    .post p:not(.button-wrapper) a:not(.primary.button):hover,
    .footnote a.footnote-anchor:not(.primary.button):hover,
    .thread-head .markup p:not(.button-wrapper) a:not(.primary.button):hover {
      text-decoration: underline;
    }
  }
}</style><img src="https://eotrx.substackcdn.com/open?token=eyJtIjoiPDIwMjYwMzA2MTQ0NDEwLjMuZmVhODYxOWZhMjA3NzZiZkBtZzIuc3Vic3RhY2suY29tPiIsInUiOjMyNDM4OTgyMiwiciI6Im1obmhmQGZlZWRiLmluIiwiZCI6Im1nMi5zdWJzdGFjay5jb20iLCJwIjoxODg0ODc2MDAsInQiOiJuZXdzbGV0dGVyIiwiYSI6ImV2ZXJ5b25lIiwicyI6MTYzNjY4NywiYyI6InBvc3QiLCJmIjp0cnVlLCJwb3NpdGlvbiI6InRvcCIsImlhdCI6MTc3MjgwODI1NiwiZXhwIjoxNzc1NDAwMjU2LCJpc3MiOiJwdWItMCIsInN1YiI6ImVvIn0.YhKWaBXCaEVs-8RChzgB4se-iDLo7xbiiqYk0Rup4OI" alt="" width="1" height="1" border="0" style="height:1px !important;width:1px !important;border-width:0 !important;margin-top:0 !important;margin-bottom:0 !important;margin-right:0 !important;margin-left:0 !important;padding-top:0 !important;padding-bottom:0 !important;padding-right:0 !important;padding-left:0 !important;"><div class="preview" style="display:none;font-size:1px;color:#333333;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">The Modern Web, tested and explained in plain English</div><div class="preview" style="display:none;font-size:1px;color:#333333;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­͏ &nbsp;   ­</div><table class="email-body-container" role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td></td><td class="content" width="550"></td><td></td></tr><tr><td></td><td class="content" width="550" align="left"><div style="font-size: 16px;line-height: 26px;max-width: 550px;width: 100%;margin: 0 auto;overflow-wrap: break-word;"><table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td align="right" style="height:20px;"><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td style="vertical-align:middle;"><span class="pencraft pc-reset reset-IxiVJZ tw-font-body tw-text-ssm tw-text-substack-secondary" style="font-family: SF Pro Text, -apple-system, system-ui, BlinkMacSystemFont, Inter, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;font-size: 13px;color: unset;list-style: none;text-decoration: unset;margin: 0;"><div class="pencraft pc-reset align-right-VJbKw5 size-12-mmZ61m reset-IxiVJZ" style="list-style: none;color: unset;text-align: right;font-size: 12px;line-height: 16px;text-decoration: unset;margin: 0;"><span class="pencraft pc-reset reset-IxiVJZ" translated="" style="list-style: none;color: unset;text-decoration: unset;margin: 0;">Forwarded this email? <a class="pencraft pc-reset decoration-underline-ClTkYc reset-IxiVJZ" href="https://substack.com/redirect/2/eyJlIjoiaHR0cHM6Ly9tb2Rlcm53ZWJ3ZWVrbHkuc3Vic3RhY2suY29tL3N1YnNjcmliZT91dG1fc291cmNlPWVtYWlsJnV0bV9jYW1wYWlnbj1lbWFpbC1zdWJzY3JpYmUmcj01ZDRzc2UmbmV4dD1odHRwcyUzQSUyRiUyRm1vZGVybndlYndlZWtseS5zdWJzdGFjay5jb20lMkZwJTJGbW9kZXJuLXdlYi13ZWVrbHktNjgiLCJwIjoxODg0ODc2MDAsInMiOjE2MzY2ODcsImYiOnRydWUsInUiOjMyNDM4OTgyMiwiaWF0IjoxNzcyODA4MjU1LCJleHAiOjIwODgzODQyNTUsImlzcyI6InB1Yi0wIiwic3ViIjoibGluay1yZWRpcmVjdCJ9.s9C6g5ZUmsxzJBurkhTW4iUwgkknChDMt31vyT5Fo_g?" style="list-style: none;color: unset;text-decoration: unset;margin: 0;-webkit-text-decoration-line: underline;text-decoration-line: underline;">Subscribe here</a> for more</span></div></span></td></tr></tbody></table></td></tr></tbody></table><table class="header graphic-header" role="presentation" style="border-spacing: 0;padding: 16px 0 32px;"><tbody><tr><td align="center" style="text-align: center;padding: 0;"><a href="https://substack.com/redirect/2/eyJlIjoiaHR0cHM6Ly9tb2Rlcm53ZWJ3ZWVrbHkuc3Vic3RhY2suY29tL3AvbW9kZXJuLXdlYi13ZWVrbHktNjg_dXRtX2NhbXBhaWduPWVtYWlsLWhhbGYtcG9zdCZyPTVkNHNzZSZ0b2tlbj1leUoxYzJWeVgybGtJam96TWpRek9EazRNaklzSW5CdmMzUmZhV1FpT2pFNE9EUTROell3TUN3aWFXRjBJam94TnpjeU9EQTRNalUxTENKbGVIQWlPakUzTnpVME1EQXlOVFVzSW1semN5STZJbkIxWWkweE5qTTJOamczSWl3aWMzVmlJam9pY0c5emRDMXlaV0ZqZEdsdmJpSjkuc1c1SWlCVFBzMzNvWlJGaXhOMEtXS1ZrOEswYndEYVJzS1pvMGtHYzRhWSIsInAiOjE4ODQ4NzYwMCwicyI6MTYzNjY4NywiZiI6dHJ1ZSwidSI6MzI0Mzg5ODIyLCJpYXQiOjE3NzI4MDgyNTUsImV4cCI6MjA4ODM4NDI1NSwiaXNzIjoicHViLTAiLCJzdWIiOiJsaW5rLXJlZGlyZWN0In0.XOSPfs-BNk07ITE2EJnrdW0BwL_1WiK8qGh4q2aGi50?"><img class="header-image" role="presentation" width="550" height="110" src="https://substackcdn.com/image/fetch/$s_!3PZS!,w_1100,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F565a348a-71da-4b21-b114-9b63bdf5aaf9_1100x220.png" style="border: none !important;vertical-align: middle;max-width: 550px;display: block;margin: 0 auto;height: auto;width: 100%;"></a></td></tr></tbody></table><div class="post typography" dir="auto" style="--image-offset-margin: -120px;padding: 32px 0 0 0;font-size: 16px;line-height: 26px;"><div class="post-header" role="region" aria-label="Post header" style="font-size: 16px;line-height: 26px;"><h1 class="post-title published title-X77sOw" dir="auto" style="direction: auto;text-align: start;unicode-bidi: isolate;color: rgb(54,55,55);font-family: 'SF Pro Display', -apple-system, system-ui, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';font-weight: 700;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: antialiased;-webkit-appearance: optimizelegibility;-moz-appearance: optimizelegibility;appearance: optimizelegibility;margin: 0;line-height: 36px;font-size: 32px;"><a href="https://substack.com/app-link/post?publication_id=1636687&amp;post_id=188487600&amp;utm_source=post-email-title&amp;utm_campaign=email-post-title&amp;isFreemail=true&amp;r=5d4sse&amp;token=eyJ1c2VyX2lkIjozMjQzODk4MjIsInBvc3RfaWQiOjE4ODQ4NzYwMCwiaWF0IjoxNzcyODA4MjU1LCJleHAiOjE3NzU0MDAyNTUsImlzcyI6InB1Yi0xNjM2Njg3Iiwic3ViIjoicG9zdC1yZWFjdGlvbiJ9.sW5IiBTPs33oZRFixN0KWKVk8K0bwDaRsKZo0kGc4aY" style="color: rgb(54,55,55);text-decoration: none;">Modern Web Weekly #68</a></h1><h3 class="subtitle subtitle-HEEcLo" dir="auto" style="direction: auto;text-align: start;unicode-bidi: isolate;font-family: 'SF Pro Display',-apple-system-headline,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: normal;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: antialiased;-webkit-appearance: optimizelegibility;-moz-appearance: optimizelegibility;appearance: optimizelegibility;margin: 4px 0 0;color: #777777;line-height: 24px;font-size: 18px;margin-top: 12px;">The Modern Web, tested and explained in plain English </h3><table class="post-meta" role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="margin: 1em 0;height: 20px;align-items: center;"><tbody><tr><td><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td style="vertical-align:middle;"><div class="pencraft pc-reset color-primary-zABazT line-height-20-t4M0El font-meta-MWBumP size-11-NuY2Zx weight-medium-fw81nC transform-uppercase-yKDgcq reset-IxiVJZ meta-EgzBVA custom-css-email-post-author" style="list-style: none;font-size: 11px;line-height: 20px;text-decoration: unset;color: rgb(54,55,55);margin: 0;font-family: 'SF Compact',-apple-system,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: 500;text-transform: uppercase;letter-spacing: .2px;"><a class="pencraft pc-reset color-primary-zABazT line-height-20-t4M0El font-meta-MWBumP size-11-NuY2Zx weight-medium-fw81nC transform-uppercase-yKDgcq reset-IxiVJZ meta-EgzBVA" style="list-style: none;color: rgb(54,55,55);margin: 0;font-size: 11px;line-height: 20px;font-family: 'SF Compact',-apple-system,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: 500;text-transform: uppercase;letter-spacing: .2px;text-decoration: none" href="https://substack.com/@dannymoerkerke">Danny Moerkerke</a></div></td></tr></tbody></table></td></tr><tr><td><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td style="vertical-align:middle;"><div class="pencraft pc-reset color-secondary-ls1g8s line-height-20-t4M0El font-meta-MWBumP size-11-NuY2Zx weight-medium-fw81nC transform-uppercase-yKDgcq reset-IxiVJZ meta-EgzBVA" style="list-style: none;font-size: 11px;line-height: 20px;text-decoration: unset;color: rgb(119,119,119);margin: 0;font-family: 'SF Compact',-apple-system,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: 500;text-transform: uppercase;letter-spacing: .2px;"><time datetime="2026-03-06T14:44:10.837Z">Mar 6</time></div></td></tr></tbody></table></td></tr></tbody></table></td></tr></tbody></table><table class="email-ufi-2-top" role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="border-top: 1px solid rgb(0,0,0,.1);border-bottom: 1px solid rgb(0,0,0,.1);min-width: 100%;"><tbody><tr height="16"><td height="16" style="font-size:0px;line-height:0;">&nbsp;</td></tr><tr><td><table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td style="vertical-align:middle;"><table role="presentation" width="38" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td align="center"><a class="email-icon-button" href="https://substack.com/app-link/post?publication_id=1636687&amp;post_id=188487600&amp;utm_source=substack&amp;isFreemail=true&amp;submitLike=true&amp;token=eyJ1c2VyX2lkIjozMjQzODk4MjIsInBvc3RfaWQiOjE4ODQ4NzYwMCwicmVhY3Rpb24iOiLinaQiLCJpYXQiOjE3NzI4MDgyNTUsImV4cCI6MTc3NTQwMDI1NSwiaXNzIjoicHViLTE2MzY2ODciLCJzdWIiOiJyZWFjdGlvbiJ9.vTtpH9AzfxHul3vxYb6YBlsY7giOGX0LnHJrB6WfEuc&amp;utm_medium=email&amp;utm_campaign=email-reaction&amp;r=5d4sse" style="font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';display: inline-block;font-weight: 500;border: 1px solid rgb(0,0,0,.1);border-radius: 9999px;text-transform: uppercase;font-size: 12px;line-height: 1;padding: 9px 0;text-decoration: none;color: rgb(119,119,119);min-width: 38px;box-sizing: border-box;width: 38px"><img class="icon" src="https://substackcdn.com/image/fetch/$s_!PeVs!,w_36,c_scale,f_png,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Ficon%2FLucideHeart%3Fv%3D4%26height%3D36%26fill%3Dnone%26stroke%3D%2523808080%26strokeWidth%3D2" width="18" height="18" style="border: none;vertical-align: middle;max-width: 18px" alt=""></a></td></tr></tbody></table></td><td width="8" style="min-width:8px;"></td><td style="vertical-align:middle;"><table role="presentation" width="38" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td align="center"><a class="email-icon-button" href="https://substack.com/app-link/post?publication_id=1636687&amp;post_id=188487600&amp;utm_source=substack&amp;utm_medium=email&amp;isFreemail=true&amp;comments=true&amp;token=eyJ1c2VyX2lkIjozMjQzODk4MjIsInBvc3RfaWQiOjE4ODQ4NzYwMCwiaWF0IjoxNzcyODA4MjU1LCJleHAiOjE3NzU0MDAyNTUsImlzcyI6InB1Yi0xNjM2Njg3Iiwic3ViIjoicG9zdC1yZWFjdGlvbiJ9.sW5IiBTPs33oZRFixN0KWKVk8K0bwDaRsKZo0kGc4aY&amp;r=5d4sse&amp;utm_campaign=email-half-magic-comments&amp;action=post-comment&amp;utm_source=substack&amp;utm_medium=email" style="font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';display: inline-block;font-weight: 500;border: 1px solid rgb(0,0,0,.1);border-radius: 9999px;text-transform: uppercase;font-size: 12px;line-height: 1;padding: 9px 0;text-decoration: none;color: rgb(119,119,119);min-width: 38px;box-sizing: border-box;width: 38px"><img class="icon" src="https://substackcdn.com/image/fetch/$s_!x1tS!,w_36,c_scale,f_png,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Ficon%2FLucideComments%3Fv%3D4%26height%3D36%26fill%3Dnone%26stroke%3D%2523808080%26strokeWidth%3D2" width="18" height="18" style="border: none;vertical-align: middle;max-width: 18px" alt=""></a></td></tr></tbody></table></td><td width="8" style="min-width:8px;"></td><td style="vertical-align:middle;"><table role="presentation" width="38" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td align="center"><a class="email-icon-button" href="https://substack.com/app-link/post?publication_id=1636687&amp;post_id=188487600&amp;utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;utm_campaign=email-share&amp;action=share&amp;triggerShare=true&amp;isFreemail=true&amp;r=5d4sse&amp;token=eyJ1c2VyX2lkIjozMjQzODk4MjIsInBvc3RfaWQiOjE4ODQ4NzYwMCwiaWF0IjoxNzcyODA4MjU1LCJleHAiOjE3NzU0MDAyNTUsImlzcyI6InB1Yi0xNjM2Njg3Iiwic3ViIjoicG9zdC1yZWFjdGlvbiJ9.sW5IiBTPs33oZRFixN0KWKVk8K0bwDaRsKZo0kGc4aY" style="font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';display: inline-block;font-weight: 500;border: 1px solid rgb(0,0,0,.1);border-radius: 9999px;text-transform: uppercase;font-size: 12px;line-height: 1;padding: 9px 0;text-decoration: none;color: rgb(119,119,119);min-width: 38px;box-sizing: border-box;width: 38px"><img class="icon" src="https://substackcdn.com/image/fetch/$s_!_L14!,w_36,c_scale,f_png,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Ficon%2FLucideShare2%3Fv%3D4%26height%3D36%26fill%3Dnone%26stroke%3D%2523808080%26strokeWidth%3D2" width="18" height="18" style="border: none;vertical-align: middle;max-width: 18px" alt=""></a></td></tr></tbody></table></td><td width="8" style="min-width:8px;"></td><td style="vertical-align:middle;"><table role="presentation" width="38" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td align="center"><a class="email-icon-button" href="https://substack.com/redirect/2/eyJlIjoiaHR0cHM6Ly9vcGVuLnN1YnN0YWNrLmNvbS9wdWIvbW9kZXJud2Vid2Vla2x5L3AvbW9kZXJuLXdlYi13ZWVrbHktNjg_dXRtX3NvdXJjZT1zdWJzdGFjayZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jYW1wYWlnbj1lbWFpbC1yZXN0YWNrLWNvbW1lbnQmYWN0aW9uPXJlc3RhY2stY29tbWVudCZyPTVkNHNzZSZ0b2tlbj1leUoxYzJWeVgybGtJam96TWpRek9EazRNaklzSW5CdmMzUmZhV1FpT2pFNE9EUTROell3TUN3aWFXRjBJam94TnpjeU9EQTRNalUxTENKbGVIQWlPakUzTnpVME1EQXlOVFVzSW1semN5STZJbkIxWWkweE5qTTJOamczSWl3aWMzVmlJam9pY0c5emRDMXlaV0ZqZEdsdmJpSjkuc1c1SWlCVFBzMzNvWlJGaXhOMEtXS1ZrOEswYndEYVJzS1pvMGtHYzRhWSIsInAiOjE4ODQ4NzYwMCwicyI6MTYzNjY4NywiZiI6dHJ1ZSwidSI6MzI0Mzg5ODIyLCJpYXQiOjE3NzI4MDgyNTUsImV4cCI6MjA4ODM4NDI1NSwiaXNzIjoicHViLTAiLCJzdWIiOiJsaW5rLXJlZGlyZWN0In0.TzSa57QGL_uEOBLHfaua5y18EH_JTYvWiwfJuPQIlrk?&amp;utm_source=substack&amp;utm_medium=email" style="font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';display: inline-block;font-weight: 500;border: 1px solid rgb(0,0,0,.1);border-radius: 9999px;text-transform: uppercase;font-size: 12px;line-height: 1;padding: 9px 0;text-decoration: none;color: rgb(119,119,119);min-width: 38px;box-sizing: border-box;width: 38px"><img class="icon" src="https://substackcdn.com/image/fetch/$s_!5EGt!,w_36,c_scale,f_png,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Ficon%2FNoteForwardIcon%3Fv%3D4%26height%3D36%26fill%3Dnone%26stroke%3D%2523808080%26strokeWidth%3D2" width="18" height="18" style="border: none;vertical-align: middle;max-width: 18px" alt=""></a></td></tr></tbody></table></td></tr></tbody></table></td><td align="right"><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td style="vertical-align:middle;"><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td align="center"><a class="email-button-outline" href="https://open.substack.com/pub/modernwebweekly/p/modern-web-weekly-68?utm_source=email&amp;redirect=app-store&amp;utm_campaign=email-read-in-app" style="font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';display: inline-block;font-weight: 500;border: 1px solid rgb(0,0,0,.1);border-radius: 9999px;text-transform: uppercase;font-size: 12px;line-height: 12px;padding: 9px 14px;text-decoration: none;color: rgb(119,119,119);"><div class="email-button-spacer" style="font-size: 16px;line-height: 26px;display: inline-block;vertical-align: middle;max-width: 0;min-height: 18px;"></div><span class="email-button-text" style="vertical-align: middle;margin-right: 4px">READ IN APP</span><img class="icon text-icon" src="https://substackcdn.com/image/fetch/$s_!ET-_!,w_36,c_scale,f_png,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Ficon%2FLucideArrowUpRight%3Fv%3D4%26height%3D36%26fill%3Dnone%26stroke%3D%2523808080%26strokeWidth%3D2" width="18" height="18" style="min-width: 18px;min-height: 18px;border: none;vertical-align: middle;margin-right: 0;margin-left: 0;max-width: 18px" alt=""></a></td></tr></tbody></table></td></tr></tbody></table></td></tr></tbody></table></td></tr><tr height="16"><td height="16" style="font-size:0px;line-height:0;">&nbsp;</td></tr></tbody></table></div></div><div class="post typography" dir="auto" style="--image-offset-margin: -120px;padding: 32px 0 0 0;font-size: 16px;line-height: 26px;"><div class="body markup" dir="auto" style="text-align: initial;font-size: 16px;line-height: 26px;width: 100%;word-break: break-word;margin-bottom: 16px;font-family: 'SF Pro Display', -apple-system, system-ui, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';font-weight: 400;"><div class="captioned-image-container-static" style="font-size: 16px;line-height: 26px;margin: 32px auto;margin-top: 0;"><figure style="width: 100%;margin: 0 auto;"><table class="image-wrapper" width="100%" border="0" cellspacing="0" cellpadding="0" data-component-name="Image2ToDOMStatic" style="mso-padding-alt: 1em 0 1.6em;"><tbody><tr><td style="text-align: center;"></td><td class="content" align="left" width="528" style="text-align: center;"><a class="image-link" target="_blank" href="https://substack.com/redirect/12a63abe-569b-46c7-8b19-7ff96fb103bc?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="position: relative;flex-direction: column;align-items: center;padding: 0;width: auto;height: auto;border: none;text-decoration: none;display: block;margin: 0;margin-top: 0;margin-bottom: 0;"><img data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7f3f4b6d-2757-44ea-bd8f-f025e7b59524_528x385.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:385,&quot;width&quot;:528,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8756,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://modernwebweekly.substack.com/i/188487600?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3f4b6d-2757-44ea-bd8f-f025e7b59524_528x385.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" alt="" width="528" height="385" src="https://substackcdn.com/image/fetch/$s_!PpF5!,w_528,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3f4b6d-2757-44ea-bd8f-f025e7b59524_528x385.heic" style="border: none !important;vertical-align: middle;display: block;-ms-interpolation-mode: bicubic;height: auto;margin-bottom: 0;width: auto !important;max-width: 100% !important;margin: 0 auto;"></a></td><td style="text-align: center;"></td></tr></tbody></table></figure></div><h2 class="header-anchor-post" style="position: relative;font-family: 'SF Pro Display',-apple-system-headline,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: bold;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: antialiased;-webkit-appearance: optimizelegibility;-moz-appearance: optimizelegibility;appearance: optimizelegibility;margin: 1em 0 0.625em 0;color: rgb(54,55,55);line-height: 1.16em;font-size: 1.625em;">Customisable &lt;select&gt; now in Safari Tech Preview 238</h2><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>After Chrome, Safari has now also implemented the fully customisable </span><code>&lt;select&gt;</code><span> element in Tech Preview 238. This means we can now fully style this element, which was barely possible before.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>To opt-in to the new customizable </span><code>&lt;select&gt;</code><span> add this to your CSS:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>select,
::picker(select) {
  appearance: base-select;
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>This will add the new </span><code>::picker(select)</code><span> pseudo-element that represents the popover containing the </span><code>&lt;option&gt;</code><span> elements.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>With default styling, the </span><code>&lt;select&gt;</code><span> with </span><code>appearance: base-select</code><span> doesn’t look very different from the regular version, but things get interesting when we customize it further.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>We can add a </span><code>&lt;button&gt;</code><span> that will open the select and inside it, we can place a </span><code>&lt;selectedcontent&gt;</code><span> element, which will contain the currently selected </span><code>&lt;option&gt;</code><span>. Inside this </span><code>&lt;button&gt;</code><span> we can place a custom indicator to indicate the open and closed state of the </span><code>&lt;select&gt;</code><span>. The default arrow indicator can be hidden with:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>::picker-icon {
  display: none;
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>The </span><code>&lt;select&gt;</code><span> could now look something like this:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>&lt;select&gt;
  
  &lt;!-- button to open and close the select --&gt;
  &lt;button&gt;
    
    &lt;!-- element where the selected option will be placed --&gt;
    &lt;selectedcontent&gt;&lt;/selectedcontent&gt;

    &lt;!-- custom arrow indicator --&gt;
    &lt;span class="arrow"&gt;&lt;/span&gt;
  &lt;/button&gt;

  &lt;!-- options --&gt;
  &lt;option value="1"&gt;
    Option 1
  &lt;/option&gt;
  &lt;option value="2"&gt;
    Option 2
  &lt;/option&gt;
  &lt;option value="3"&gt;
    Option 3
  &lt;/option&gt;
  &lt;option value="4"&gt;
    Option 4
  &lt;/option&gt;
&lt;/select&gt;</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Keep in mind that only the contents of the selected </span><code>&lt;option&gt;</code><span> will be placed inside </span><code>&lt;selectedcontent&gt;</code><span> and not the </span><code>&lt;option&gt;</code><span> element itself. This is important to know when you want to style the contents of </span><code>&lt;selectedcontent&gt;</code><span>.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>In the screenshot below, the second </span><code>&lt;option&gt;</code><span> is selected:</span></p><div class="captioned-image-container-static" style="font-size: 16px;line-height: 26px;margin: 32px auto;"><figure style="width: 100%;margin: 0 auto;"><table class="image-wrapper" width="100%" border="0" cellspacing="0" cellpadding="0" data-component-name="Image2ToDOMStatic" style="mso-padding-alt: 1em 0 1.6em;"><tbody><tr><td style="text-align: center;"></td><td class="content" align="left" width="826" style="text-align: center;"><a class="image-link" target="_blank" href="https://substack.com/redirect/70010f9d-8020-4eb9-8408-9cc3a5b52023?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="position: relative;flex-direction: column;align-items: center;padding: 0;width: auto;height: auto;border: none;text-decoration: none;display: block;margin: 0;"><img class="wide-image" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f39991bb-866e-4ac7-b21c-28187178f394_826x406.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:406,&quot;width&quot;:826,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" alt="" title="" width="550" height="270.33898305084745" src="https://substackcdn.com/image/fetch/$s_!pwbm!,w_1100,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39991bb-866e-4ac7-b21c-28187178f394_826x406.png" style="border: none !important;vertical-align: middle;display: block;-ms-interpolation-mode: bicubic;height: auto;margin-bottom: 0;width: auto !important;max-width: 100% !important;margin: 0 auto;"></a></td><td style="text-align: center;"></td></tr></tbody></table><figcaption class="image-caption" style="box-sizing: content-box;color: rgb(119,119,119);font-size: 14px;line-height: 20px;font-weight: 400;letter-spacing: -.15px;margin-top: 8px;width: 70%;padding-left: 15%;padding-right: 15%;text-align: center;"><code>&lt;select&gt;</code><span> with contents of selected </span><code>&lt;option&gt;</code><span> in </span><code>&lt;selectedcontent&gt;</code></figcaption></figure></div><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>By default, a checkmark is shown in front of the selected </span><code>&lt;option&gt;</code><span>:</span></p><div class="captioned-image-container-static" style="font-size: 16px;line-height: 26px;margin: 32px auto;"><figure style="width: 100%;margin: 0 auto;"><table class="image-wrapper" width="100%" border="0" cellspacing="0" cellpadding="0" data-component-name="Image2ToDOMStatic" style="mso-padding-alt: 1em 0 1.6em;"><tbody><tr><td style="text-align: center;"></td><td class="content" align="left" width="404" style="text-align: center;"><a class="image-link" target="_blank" href="https://substack.com/redirect/f5ed9124-98c4-45d0-b2e0-55ae167a08ef?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="position: relative;flex-direction: column;align-items: center;padding: 0;width: auto;height: auto;border: none;text-decoration: none;display: block;margin: 0;"><img data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9d047266-7dbe-4cb8-9bc8-f1ef8cf21a4a_404x478.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:478,&quot;width&quot;:404,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" alt="" title="" width="404" height="478" src="https://substackcdn.com/image/fetch/$s_!IAES!,w_404,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d047266-7dbe-4cb8-9bc8-f1ef8cf21a4a_404x478.png" style="border: none !important;vertical-align: middle;display: block;-ms-interpolation-mode: bicubic;height: auto;margin-bottom: 0;width: auto !important;max-width: 100% !important;margin: 0 auto;"></a></td><td style="text-align: center;"></td></tr></tbody></table><figcaption class="image-caption" style="box-sizing: content-box;color: rgb(119,119,119);font-size: 14px;line-height: 20px;font-weight: 400;letter-spacing: -.15px;margin-top: 8px;width: 70%;padding-left: 15%;padding-right: 15%;text-align: center;"><span>Checkmark in front of the selected </span><code>&lt;option&gt;</code></figcaption></figure></div><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">If needed, you can hide this checkmark with:</p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>option::checkmark {
  display: none;
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><strong>Adding HTML elements to </strong><code>&lt;option&gt;</code><br><span>In addition to text, you can now also add other HTML elements to </span><code>&lt;option&gt;</code><span>. Here’s an example of a currency picker using flag images that I adapted from an example by Una Kravetz:</span></p><div class="captioned-image-container-static" style="font-size: 16px;line-height: 26px;margin: 32px auto;"><figure style="width: 100%;margin: 0 auto;"><table class="image-wrapper" width="100%" border="0" cellspacing="0" cellpadding="0" data-component-name="Image2ToDOMStatic" style="mso-padding-alt: 1em 0 1.6em;"><tbody><tr><td style="text-align: center;"></td><td class="content" align="left" width="250" style="text-align: center;"><a class="image-link" target="_blank" href="https://substack.com/redirect/f70ec8e4-04e6-4b94-83f6-0eb85929adf8?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="position: relative;flex-direction: column;align-items: center;padding: 0;width: auto;height: auto;border: none;text-decoration: none;display: block;margin: 0;"><img data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7f77230f-3830-44e9-a9e4-7613aae94a95_250x337.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:337,&quot;width&quot;:250,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" alt="" title="" width="250" height="337" src="https://substackcdn.com/image/fetch/$s_!5n69!,w_250,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f77230f-3830-44e9-a9e4-7613aae94a95_250x337.png" style="border: none !important;vertical-align: middle;display: block;-ms-interpolation-mode: bicubic;height: auto;margin-bottom: 0;width: auto !important;max-width: 100% !important;margin: 0 auto;"></a></td><td style="text-align: center;"></td></tr></tbody></table><figcaption class="image-caption" style="box-sizing: content-box;color: rgb(119,119,119);font-size: 14px;line-height: 20px;font-weight: 400;letter-spacing: -.15px;margin-top: 8px;width: 70%;padding-left: 15%;padding-right: 15%;text-align: center;"><code>&lt;select&gt;</code><span> as a currency picker with flag images</span></figcaption></figure></div><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>You can now simply accomplish this by placing the needed HTML elements inside </span><code>&lt;option&gt;</code><span> and style them with CSS:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>&lt;option value="eur"&gt;
  &lt;img src="europe.png"&gt;
  &lt;div class="currency"&gt;
    &lt;div class="currency-short"&gt;EUR&lt;/div&gt;
    &lt;div class="currency-long"&gt;Euro&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="symbol"&gt;€&lt;/div&gt;
&lt;/option&gt;</code></pre><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>option {
  .currency {
    flex-grow: 1;
  }
  
  .currency-long {
    color: #595959;
  }
  
  .symbol {
    justify-self: end;
  }
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><strong>Standing on the shoulders of giants</strong><br><span>Internally, the customizable </span><code>&lt;select&gt;</code><span> uses Anchor Positioning to anchor the </span><code>::picker(select)</code><span> pseudo-element to it and the Popover API to position it in the top layer, above all other elements on the page.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">This is important to know if you want to animate the opening and closing of the picker, which is now possible as well!</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Let’s implement a nice sliding transition where the picker’s height is animated and the </span><code>&lt;option&gt;</code><span> elements inside it slide down. To do this, we can place a </span><code>&lt;div&gt;</code><span> around the </span><code>&lt;option&gt;</code><span> elements and then give this </span><code>&lt;div&gt;</code><span> the following CSS:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>display: flex;
flex-direction: column;
justify-content: flex-end;</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>which aligns the </span><code>&lt;option&gt;</code><span> elements to the bottom so they’ll slide down when the picker is opened by expanding its height.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>We can do this by simply wrapping the </span><code>&lt;option&gt;</code><span> elements inside a </span><code>&lt;div&gt;</code><span> because any content that is placed inside the </span><code>&lt;select&gt;</code><span> outside the </span><code>&lt;button&gt;</code><span> will be automatically placed inside </span><code>::picker(select)</code><span>:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>&lt;div class="options"&gt;
  &lt;option value="1"&gt;
    Option 1
  &lt;/option&gt;
  &lt;option value="2"&gt;
    Option 2
  &lt;/option&gt;
&lt;/div&gt;</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Even though the </span><code>&lt;div&gt;</code><span> will be placed inside </span><code>::picker(select)</code><span>, it needs to be styled with </span><code>select .options</code><span> and not </span><code>select::picker(select) .options</code><span> so it will look like this:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>select .options {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>We will then define the styles for </span><code>::picker(select)</code><span> by setting its </span><code>height</code><span> to </span><code>0</code><span> and defining the transitions:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>::picker(select) {
  height: 0;
  min-height: 0;
  transition-property: height, overlay, display;
  transition-duration: .3s;
  transition-behavior: allow-discrete;
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>In addition to </span><code>height</code><span>, we also need to set </span><code>overlay</code><span> and </span><code>display</code><span> as transition properties because the picker will not have </span><code>display</code><span> by default and it will be moved to the top layer when shown. For that reason, we need to transition these properties as well, otherwise the transition will be instant.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>To indicate we want to allow transitioning of </span><code>overlay</code><span> and </span><code>display</code><span>, which are discrete properties, we set </span><code>transition-behavior</code><span> to </span><code>allow-discrete</code><span>. The property </span><code>min-height: 0</code><span> is needed for a smooth transition.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Since the picker doesn’t have </span><code>display</code><span> when it’s not shown, we need to define a </span><code>@starting-style</code><span> rule for it because only elements that have </span><code>display</code><span> can be animated. This rule defines the property and value we want to transition </span><em>from</em><span>, in our case </span><code>height: 0</code><span>:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>@starting-style {
  ::picker(select) {
    height: 0;
    min-height: 0;
  }
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Finally, we need to define the property and value we want to transition to, in our case the </span><code>height</code><span> of the picker when the </span><code>&lt;select&gt;</code><span> is open:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>select:open::picker(select) {
  height: auto;
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>To enable transitioning to a keyword like </span><code>auto</code><span>, we need to set this CSS as well:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>:root {
  interpolate-size: allow-keywords; 
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>And with that, we now have a smooth sliding </span><code>&lt;select&gt;</code><span>! 💪</span></p><div class="captioned-image-container-static" style="font-size: 16px;line-height: 26px;margin: 32px auto;"><figure style="width: 100%;margin: 0 auto;"><table class="image-wrapper" width="100%" border="0" cellspacing="0" cellpadding="0" data-component-name="Image2ToDOMStatic" style="mso-padding-alt: 1em 0 1.6em;"><tbody><tr><td style="text-align: center;"></td><td class="content" align="left" width="506" style="text-align: center;"><a class="image-link" target="_blank" href="https://substack.com/redirect/ef967378-9638-4610-a17e-8eb06fa6c4e9?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="position: relative;flex-direction: column;align-items: center;padding: 0;width: auto;height: auto;border: none;text-decoration: none;display: block;margin: 0;"><img data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eda9d082-6484-454b-ab6f-64d917df40fc_506x654.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:654,&quot;width&quot;:506,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" alt="" title="" width="506" height="654" src="https://substackcdn.com/image/fetch/$s_!0nvr!,w_506,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feda9d082-6484-454b-ab6f-64d917df40fc_506x654.gif" style="border: none !important;vertical-align: middle;display: block;-ms-interpolation-mode: bicubic;height: auto;margin-bottom: 0;width: auto !important;max-width: 100% !important;margin: 0 auto;"></a></td><td style="text-align: center;"></td></tr></tbody></table><figcaption class="image-caption" style="box-sizing: content-box;color: rgb(119,119,119);font-size: 14px;line-height: 20px;font-weight: 400;letter-spacing: -.15px;margin-top: 8px;width: 70%;padding-left: 15%;padding-right: 15%;text-align: center;"><code>&lt;select&gt;</code><span> with animated open and close transitions</span></figcaption></figure></div><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>I created a </span><a href="https://substack.com/redirect/57a36a34-6102-4c1b-87bd-923ab7c5ae39?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="color: #51b4e9;text-decoration: none;">demo on codepen.io</a><span>, but unfortunately, Codepen doesn’t work for some reason (bug) in Safari Tech Preview 238. If you want to view it there, you can copy the HTML and CSS to a local HTML page and run it from there for now 🤷‍♂️</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>I also had to apply a small Safari-only fix to this demo with the sliding opening and closing. When the </span><code>&lt;select&gt;</code><span> is opened, the options are correctly displayed below the button, but when closing, the options slide up to the top of the button, covering it:</span></p><div class="video-player-with-background is-static video-player-wrapper" role="region" aria-label="Video player" style="flex-grow: 1;font-size: 16px;line-height: 26px;margin-top: 0.5em;"><a class="video-player video-player-with-background" href="https://substack.com/redirect/13aa3a77-a771-44a3-a176-ef6684aba5b0?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" style="position: relative;display: block;width: 100%;cursor: pointer;background-color: rgb(0,0,0);height: auto;border-radius: 8px;padding-bottom: 0"><img class="video-player-preview" width="550" src="https://substackcdn.com/image/fetch/$s_!tBHw!,w_1100,h_734,c_pad,f_auto,q_auto:good,fl_progressive:steep/l_play_button_usfui2,w_144,e_colorize:0/https%3A%2F%2Fsubstack-video.s3.amazonaws.com%2Fvideo_upload%2Fpost%2F188487600%2Fe41ee914-f568-4294-95b0-e695f1f4b1e8%2Ftranscoded-00001.png" data-component-name="VideoPlayer-PreviewImg" data-testid="video-player-static-preview-image" style="border: none !important;vertical-align: middle;display: block;-ms-interpolation-mode: bicubic;max-width: 100%;height: auto;margin: 0 auto;width: 100%;max-height: 100%;background: black;cursor: pointer;position: relative;border-radius: 8px;"></a></div><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>By adding </span><code>margin-top</code><span> to the picker, this is solved:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code> @supports (font: -apple-system-body) {
    &amp;:not(:open)::picker(select) {
      margin-top: 38px;
    }
  }</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>The </span><code>@supports</code><span> block makes sure this is only applied in Safari.</span></p><div style="font-size: 16px;line-height: 26px;"><hr style="margin: 32px 0;padding: 0;height: 1px;background: rgb(0,0,0,.1);border: none;"></div><h2 class="header-anchor-post" style="position: relative;font-family: 'SF Pro Display',-apple-system-headline,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: bold;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: antialiased;-webkit-appearance: optimizelegibility;-moz-appearance: optimizelegibility;appearance: optimizelegibility;margin: 1em 0 0.625em 0;color: rgb(54,55,55);line-height: 1.16em;font-size: 1.625em;"><span>Accessible arrow navigation without JavaScript with </span><code>focusgroup</code></h2><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>In Chrome 146 with Experimental Web Features enabled (or an origin trial), you can now try the </span><code>focusgroup</code><span> attribute. This is a declarative way to add keyboard arrow-key navigation to widgets like toolbars, tablists, menus, listboxes, etc., without the need for JavaScript.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Before, to implement keyboard navigation with the arrow keys for these widgets, you would need to write a lot of boilerplate code. </span><code>focusgroup</code><span> aims to solve all this.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">The attribute takes a behavior token and three optional modifier tokens: </p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>focusgroup="[behavior] [modifier1] [modifier2] [modifier3]"</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">The behavior token defines the widget pattern and can take the values:</p><ul style="margin-top: 0;padding: 0;"><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;"><code>toolbar</code></p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;"><code>tablist</code></p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;"><code>radiogroup</code></p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;"><code>listbox</code></p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;"><code>menu</code></p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;"><code>menubar</code></p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;"><code>none</code></p></li></ul><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>The first modifier defines the navigation direction and can take the values </span><code>block</code><span> (vertical) or </span><code>inline</code><span> (horizontal). So a widget with the modifier </span><code>inline</code><span> can be navigated with the left and right arrow keys, and a widget with the modifier </span><code>block</code><span> can be navigated with the up and down arrow keys.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>The second modifier defines the wrapping behavior and can take the values </span><code>wrap</code><span> (allow navigation to wrap around) or </span><code>nowrap</code><span> (prevent navigation from wrapping). When set to </span><code>wrap</code><span>, the navigation will wrap around to the next item in the group, so when the last item is reached, the focus will move to the first item in the group and vice versa. When set to </span><code>nowrap</code><span>, the navigation will stop at the last item, and the focus will remain on the last item.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>The third modifier determines whether the last-focused item in a group regains focus on re-entry with Tab. Use the </span><code>nomemory</code><span> modifier to disable it. This is mainly used in combination with the </span><code>focusgroupstart</code><span> attribute (see below).</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Any modifier can be omitted if you only want to specify certain ones. By default, toolbar, tablist, and menubar have the modifier </span><code>inline</code><span>, menu has the modifier </span><code>block</code><span>, and tablist, menu, and menubar have the modifier </span><code>wrap</code><span>.</span></p><h4 class="header-anchor-post" style="position: relative;font-family: 'SF Pro Display',-apple-system-headline,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: bold;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: antialiased;-webkit-appearance: optimizelegibility;-moz-appearance: optimizelegibility;appearance: optimizelegibility;margin: 1em 0 0.625em 0;color: rgb(54,55,55);line-height: 1.16em;font-size: 1.125em;">focusgroupstart and nomemory</h4><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>When a widget with </span><code>focusgroup</code><span> is entered with TAB, the first focusable element will get focus, or the element with the </span><code>focusgroupstart</code><span> attribute. By default, this only applies when the widget </span><em>is entered for the first time</em><span>. </span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>When you focus another element in this group with the arrow keys, then TAB to another group, and then go back to this group with shift+TAB, the last focused element will get focus again, </span><strong>not</strong><span> the element with </span><code>focusgroupstart</code><span>.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>You can disable this memory behavior with the nomemory modifier. With that modifier, the element with </span><code>focusgroupstart</code><span> will always get focus when the widget is entered with TAB or shift+TAB, even if another element was last focused when the widget was exited.</span></p><h4 class="header-anchor-post" style="position: relative;font-family: 'SF Pro Display',-apple-system-headline,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: bold;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: antialiased;-webkit-appearance: optimizelegibility;-moz-appearance: optimizelegibility;appearance: optimizelegibility;margin: 1em 0 0.625em 0;color: rgb(54,55,55);line-height: 1.16em;font-size: 1.125em;">Nested groups</h4><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>You can also nest elements with </span><code>focusgroup,</code><span> where each group has its own scope. A nested </span><code>focusgroup</code><span> automatically opts out of its ancestor’s arrow navigation, and you can use TAB and shift+TAB to move between the groups.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Here’s an example of a menubar with horizontal navigation through the </span><code>menubar</code><span> behavior, where each button can open a submenu, which has vertical navigation through the </span><code>menu</code><span> behavior.:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>&lt;!-- menubar with horizontal navigation --&gt;
&lt;ul focusgroup="menubar"&gt;
  &lt;li&gt;
    &lt;button type="button" popovertarget="aboutpop"&gt;About&lt;/button&gt;
    
    &lt;!-- submenu with vertical navigation --&gt;
    &lt;ul focusgroup="menu" autofocus id="aboutpop" popover&gt;
      &lt;li&gt;&lt;a tabindex="0"&gt;Overview&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a tabindex="0"&gt;Administration&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;

  &lt;/li&gt;
  &lt;li&gt;
    &lt;button type="button" popovertarget="admitpop"&gt;Admissions&lt;/button&gt;
    
    &lt;!-- submenu with vertical navigation --&gt;
    &lt;ul focusgroup="menu" autofocus id="admitpop" popover&gt;
      &lt;li&gt;&lt;a tabindex="0"&gt;Apply&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;
        &lt;button type="button" popovertarget="tuitpop"&gt;Tuition&lt;/button&gt;
        
        &lt;!-- submenu with vertical navigation --&gt;
        &lt;ul focusgroup="menu" autofocus id="tuitpop" popover&gt;
          &lt;li&gt;&lt;a tabindex="0"&gt;Undergraduate&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a tabindex="0"&gt;Graduate&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;

      &lt;/li&gt;
      &lt;li&gt;&lt;a tabindex="0"&gt;Visit&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;    
    </code></pre><h4 class="header-anchor-post" style="position: relative;font-family: 'SF Pro Display',-apple-system-headline,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: bold;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: antialiased;-webkit-appearance: optimizelegibility;-moz-appearance: optimizelegibility;appearance: optimizelegibility;margin: 1em 0 0.625em 0;color: rgb(54,55,55);line-height: 1.16em;font-size: 1.125em;"><span>Excluding elements with </span><code>focusgroup=”none”</code></h4><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>You can exclude elements in a focusgroup by setting </span><code>focusgroup=”none”</code><span> on them. This will make an element unreachable by arrow navigation, but you can still use TAB and shift+TAB to access it. </span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">In the example below, the second button is not reachable with the arrow keys:</p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>
&lt;div focusgroup="toolbar wrap"&gt;
  &lt;button&gt;A&lt;/button&gt;
  &lt;button focusgroup="none"&gt;(Not arrow reachable)&lt;/button&gt;
  &lt;button&gt;B&lt;/button&gt;
&lt;/div&gt;    
    </code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Check the </span><a href="https://substack.com/redirect/a5945a9b-62a6-4763-ab18-5380c3387a7f?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="color: #51b4e9;text-decoration: none;">demo on What PWA Can Do Today</a><span> in Chrome 146+ and definitely read the </span><a href="https://substack.com/redirect/8a0a1b63-f5d6-4e28-b731-33e62bcf9864?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="color: #51b4e9;text-decoration: none;">article</a><span> on the Chrome Developers Blog for more details.</span></p><div style="font-size: 16px;line-height: 26px;"><hr style="margin: 32px 0;padding: 0;height: 1px;background: rgb(0,0,0,.1);border: none;"></div><h2 class="header-anchor-post" style="position: relative;font-family: 'SF Pro Display',-apple-system-headline,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: bold;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: antialiased;-webkit-appearance: optimizelegibility;-moz-appearance: optimizelegibility;appearance: optimizelegibility;margin: 1em 0 0.625em 0;color: rgb(54,55,55);line-height: 1.16em;font-size: 1.625em;">PWA Audit: on your way to a great PWA</h2><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">Do you already have a PWA, but are you running into issues with performance, security, or functionality? Or are you not sure how to make your PWA better?</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">I can help you by running an audit of your PWA</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>I will evaluate it on more than 35 criteria and provide you with </span><strong>clear </strong><span>and </span><strong>actionable </strong><span>instructions on how to improve it. No generic stuff that you can get anywhere, but an in-depth quality checkup to get you on your way to a great PWA.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">Some of the criteria I will evaluate it on are:</p><ul style="margin-top: 0;padding: 0;"><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;">Installability</p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;">Cross-device and cross-platform compatibility</p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;">Offline support</p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;">Usability</p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;">Effective use of modern web APIs</p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;">Performance</p></li><li style="margin: 8px 0 0 32px;mso-special-format: bullet;"><p style="color: rgb(54,55,55);line-height: 26px;margin-bottom: 0;box-sizing: border-box;padding-left: 4px;font-size: 16px;margin: 0;">Security</p></li></ul><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">Your investment in the improvement of your PWA through the audit is €499, excluding VAT (where applicable).</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>If you want to request an audit or first would like to know more, you can </span><br><a href="https://substack.com/redirect/b8605a0b-e818-4dc2-8cbe-e1648d17a4b0?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="color: #51b4e9;text-decoration: none;">fill out the form</a><span> or </span><a href="https://substack.com/redirect/4a700725-4aac-45f9-8ba7-4854fa57bd73?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="color: #51b4e9;text-decoration: none;">book a call with me</a><span>.</span></p><div class="captioned-image-container-static" style="font-size: 16px;line-height: 26px;margin: 32px auto;"><figure style="width: 100%;margin: 0 auto;"><table class="image-wrapper" width="100%" border="0" cellspacing="0" cellpadding="0" data-component-name="Image2ToDOMStatic" style="mso-padding-alt: 1em 0 1.6em;"><tbody><tr><td style="text-align: center;"></td><td class="content" align="left" width="733" style="text-align: center;"><a class="image-link" target="_blank" href="https://substack.com/redirect/4a700725-4aac-45f9-8ba7-4854fa57bd73?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="position: relative;flex-direction: column;align-items: center;padding: 0;width: auto;height: auto;border: none;text-decoration: none;display: block;margin: 0;"><img class="wide-image" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1437bbbc-0e9d-466f-82bd-a29c12026252_733x150.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:150,&quot;width&quot;:733,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9107,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:&quot;https://cal.com/dannymoerkerke/30min&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://modernwebweekly.substack.com/i/158031003?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1437bbbc-0e9d-466f-82bd-a29c12026252_733x150.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" alt="" title="" width="550" height="112.55115961800819" src="https://substackcdn.com/image/fetch/$s_!5SyH!,w_1100,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1437bbbc-0e9d-466f-82bd-a29c12026252_733x150.heic" style="border: none !important;vertical-align: middle;display: block;-ms-interpolation-mode: bicubic;height: auto;margin-bottom: 0;width: auto !important;max-width: 100% !important;margin: 0 auto;"></a></td><td style="text-align: center;"></td></tr></tbody></table></figure></div><div style="font-size: 16px;line-height: 26px;"><hr style="margin: 32px 0;padding: 0;height: 1px;background: rgb(0,0,0,.1);border: none;"></div><h2 class="header-anchor-post" style="position: relative;font-family: 'SF Pro Display',-apple-system-headline,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';font-weight: bold;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: antialiased;-webkit-appearance: optimizelegibility;-moz-appearance: optimizelegibility;appearance: optimizelegibility;margin: 1em 0 0.625em 0;color: rgb(54,55,55);line-height: 1.16em;font-size: 1.625em;"><span>Better title attributes with </span><code>interestfor</code></h2><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">Chromium-based browsers now support Interest Invokers, which you can compare to Invoker Commands. But instead of clicking, they invoke an action when the user hovers over an element. Think of popovers, but then they are opened when a button is hovered instead of clicked.</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>For example, you can let a button open a popover on hover with the </span><code>interestfor</code><span> attribute which holds the </span><code>id</code><span> of the popover as its value:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>&lt;button type="button" interestfor="title-popover"&gt;Hover me&lt;/button&gt;

&lt;div id="title-popover" popover="hint"&gt;Here I am!&lt;/div&gt;</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>The </span><code>interestfor</code><span> attribute also implicitly anchors the popover to the button, so you can use anchor positioning to position the popover:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>#title-popover {
  position-area: bottom span-right;
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Of course, you can use the native </span><code>title</code><span> attribute to show a title on hover, but this way, you can style the title and configure the time it takes before the popover is shown and hidden with CSS:</span></p><pre style="background: rgb(255,255,255);padding: 20px;margin-top: 0;margin-bottom: 20px;outline: 1px solid rgb(0,0,0,.1);outline-offset: -1px;border-radius: 12px;box-sizing: border-box;white-space: pre-wrap;font-family: monospace;font-size: 13px;line-height: 24px;font-weight: 500;color: rgb(54,55,55);"><code>[interestfor] {
  /* delay before interest is shown */
  interest-delay-start: 200ms;

  /* delay before interest is hidden */
  interest-delay-end: 200ms;
}</code></pre><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>The API also exposed events when the element with </span><code>interestfor</code><span> is hovered and unhovered; respectively, “interest” and “loseinterest”. Just like with the Invoker Commands API, the event is dispatched on the target of the invoker and not the invoker (button) itself, so you only need to set an event on the popover and not an event on each button.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>In the demo I created on codepen, I use the “interest” event to read the </span><code>data-title</code><span> attribute of the hovered button to show its value (the title of the button) in the popover.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"><span>Check the demo </span><a href="https://substack.com/redirect/56693f5f-422f-4bdb-9128-76206e63c951?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" rel="" style="color: #51b4e9;text-decoration: none;">here</a><span>.</span></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;"></p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">check sw update in spa with navigation api</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">title with interestfor</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">geolocation element, usermedia element</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">focusgroup</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">datatransfer</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">share_target </p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">https://github.com/danielsakhapov/declarative-scroll-commands-for-html-explainer </p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;">clipboardchange</p><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;margin-bottom: 0;">transform in anchor positioning https://codepen.io/bramus/pen/pvyrmWv</p></div></div><div class="container-border" style="margin: 32px 0 0;width: 100%;box-sizing: border-box;border-top: 1px solid #e6e6e6;font-size: 16px;line-height: 26px;"></div><div class="typography" style="--image-offset-margin: -120px;font-size: 16px;line-height: 26px;"><div class="postscript" style="margin: 32px 0;font-size: 16px;line-height: 26px;"><div class="body markup" dir="auto" style="text-align: initial;font-size: 16px;line-height: 26px;width: 100%;word-break: break-word;margin-bottom: 16px;"><p style="margin: 0 0 20px 0;color: rgb(54,55,55);line-height: 26px;font-size: 16px;margin-top: 0;"><em>You’re currently a free subscriber to Modern Web Weekly. For get access to exclusive members-only content and the full archive,&nbsp;upgrade your subscription.</em></p><div class="subscribe-widget is-signed-up" data-component-name="SubscribeWidget" style="margin: 0 0 1em;direction: ltr;font-size: 16px;line-height: 26px;margin-bottom: 0;"><div class="pencraft pc-reset button-wrapper" style="text-decoration: unset;list-style: none;font-size: 16px;line-height: 26px;text-align: center;cursor: pointer;border-radius: 4px;"><a class="button subscribe-btn primary" href="https://substack.com/redirect/2/eyJlIjoiaHR0cHM6Ly9tb2Rlcm53ZWJ3ZWVrbHkuc3Vic3RhY2suY29tL3N1YnNjcmliZT91dG1fc291cmNlPXBvc3QmdXRtX2NhbXBhaWduPWVtYWlsLWNoZWNrb3V0Jm5leHQ9aHR0cHMlM0ElMkYlMkZtb2Rlcm53ZWJ3ZWVrbHkuc3Vic3RhY2suY29tJTJGcCUyRm1vZGVybi13ZWItd2Vla2x5LTY4JnI9NWQ0c3NlJnRva2VuPWV5SjFjMlZ5WDJsa0lqb3pNalF6T0RrNE1qSXNJbWxoZENJNk1UYzNNamd3T0RJMU5Td2laWGh3SWpveE56YzFOREF3TWpVMUxDSnBjM01pT2lKd2RXSXRNVFl6TmpZNE55SXNJbk4xWWlJNkltTm9aV05yYjNWMEluMC5vZXFzMU9MMGpWUHlCQXhrc3FfeXVGSnlqajNkUmxpTDhjUnlEZnlJcDVjIiwicCI6MTg4NDg3NjAwLCJzIjoxNjM2Njg3LCJmIjp0cnVlLCJ1IjozMjQzODk4MjIsImlhdCI6MTc3MjgwODI1NSwiZXhwIjoyMDg4Mzg0MjU1LCJpc3MiOiJwdWItMCIsInN1YiI6ImxpbmstcmVkaXJlY3QifQ.gomB3MhxFkuYBziGqXERKzcdKA9VvNTMACLUNcs-5EE?&amp;utm_medium=email&amp;utm_source=subscribe-widget&amp;utm_content=188487600" style="font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';display: inline-block;box-sizing: border-box;cursor: pointer;border: none;border-radius: 8px;font-size: 14px;line-height: 20px;font-weight: 600;text-align: center;opacity: 1;outline: none;white-space: nowrap;text-decoration: none !important;background-color: #51b4e9;color: #ffffff !important;margin: 0 auto;padding: 12px 20px;height: auto;"><span style="color: #ffffff;text-decoration: none;">Upgrade to paid</span></a></div></div></div></div></div><table class="email-ufi-2-bottom" role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="border-top: 1px solid rgb(0,0,0,.1);border-bottom: 1px solid rgb(0,0,0,.1);min-width: 100%;"><tbody><tr height="16"><td height="16" style="font-size:0px;line-height:0;">&nbsp;</td></tr><tr><td><table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0" style="margin:0 auto;"><tbody><tr><td style="vertical-align:middle;"><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td align="center"><a class="email-button-outline" href="https://substack.com/app-link/post?publication_id=1636687&amp;post_id=188487600&amp;utm_source=substack&amp;isFreemail=true&amp;submitLike=true&amp;token=eyJ1c2VyX2lkIjozMjQzODk4MjIsInBvc3RfaWQiOjE4ODQ4NzYwMCwicmVhY3Rpb24iOiLinaQiLCJpYXQiOjE3NzI4MDgyNTUsImV4cCI6MTc3NTQwMDI1NSwiaXNzIjoicHViLTE2MzY2ODciLCJzdWIiOiJyZWFjdGlvbiJ9.vTtpH9AzfxHul3vxYb6YBlsY7giOGX0LnHJrB6WfEuc&amp;utm_medium=email&amp;utm_campaign=email-reaction&amp;r=5d4sse" style="font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';display: inline-block;font-weight: 500;border: 1px solid rgb(0,0,0,.1);border-radius: 9999px;text-transform: uppercase;font-size: 12px;line-height: 12px;padding: 9px 14px;text-decoration: none;color: rgb(119,119,119);"><img class="icon" src="https://substackcdn.com/image/fetch/$s_!PeVs!,w_36,c_scale,f_png,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Ficon%2FLucideHeart%3Fv%3D4%26height%3D36%26fill%3Dnone%26stroke%3D%2523808080%26strokeWidth%3D2" width="18" height="18" style="margin-right: 8px;min-width: 18px;min-height: 18px;border: none;vertical-align: middle;max-width: 18px" alt=""><span class="email-button-text" style="vertical-align: middle;">Like</span></a></td></tr></tbody></table></td><td width="8" style="min-width:8px;"></td><td style="vertical-align:middle;"><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td align="center"><a class="email-button-outline" href="https://substack.com/app-link/post?publication_id=1636687&amp;post_id=188487600&amp;utm_source=substack&amp;utm_medium=email&amp;isFreemail=true&amp;comments=true&amp;token=eyJ1c2VyX2lkIjozMjQzODk4MjIsInBvc3RfaWQiOjE4ODQ4NzYwMCwiaWF0IjoxNzcyODA4MjU1LCJleHAiOjE3NzU0MDAyNTUsImlzcyI6InB1Yi0xNjM2Njg3Iiwic3ViIjoicG9zdC1yZWFjdGlvbiJ9.sW5IiBTPs33oZRFixN0KWKVk8K0bwDaRsKZo0kGc4aY&amp;r=5d4sse&amp;utm_campaign=email-half-magic-comments&amp;action=post-comment&amp;utm_source=substack&amp;utm_medium=email" style="font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';display: inline-block;font-weight: 500;border: 1px solid rgb(0,0,0,.1);border-radius: 9999px;text-transform: uppercase;font-size: 12px;line-height: 12px;padding: 9px 14px;text-decoration: none;color: rgb(119,119,119);"><img class="icon" src="https://substackcdn.com/image/fetch/$s_!x1tS!,w_36,c_scale,f_png,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Ficon%2FLucideComments%3Fv%3D4%26height%3D36%26fill%3Dnone%26stroke%3D%2523808080%26strokeWidth%3D2" width="18" height="18" style="margin-right: 8px;min-width: 18px;min-height: 18px;border: none;vertical-align: middle;max-width: 18px" alt=""><span class="email-button-text" style="vertical-align: middle;">Comment</span></a></td></tr></tbody></table></td><td width="8" style="min-width:8px;"></td><td style="vertical-align:middle;"><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td align="center"><a class="email-button-outline" href="https://substack.com/redirect/2/eyJlIjoiaHR0cHM6Ly9vcGVuLnN1YnN0YWNrLmNvbS9wdWIvbW9kZXJud2Vid2Vla2x5L3AvbW9kZXJuLXdlYi13ZWVrbHktNjg_dXRtX3NvdXJjZT1zdWJzdGFjayZ1dG1fbWVkaXVtPWVtYWlsJnV0bV9jYW1wYWlnbj1lbWFpbC1yZXN0YWNrLWNvbW1lbnQmYWN0aW9uPXJlc3RhY2stY29tbWVudCZyPTVkNHNzZSZ0b2tlbj1leUoxYzJWeVgybGtJam96TWpRek9EazRNaklzSW5CdmMzUmZhV1FpT2pFNE9EUTROell3TUN3aWFXRjBJam94TnpjeU9EQTRNalUxTENKbGVIQWlPakUzTnpVME1EQXlOVFVzSW1semN5STZJbkIxWWkweE5qTTJOamczSWl3aWMzVmlJam9pY0c5emRDMXlaV0ZqZEdsdmJpSjkuc1c1SWlCVFBzMzNvWlJGaXhOMEtXS1ZrOEswYndEYVJzS1pvMGtHYzRhWSIsInAiOjE4ODQ4NzYwMCwicyI6MTYzNjY4NywiZiI6dHJ1ZSwidSI6MzI0Mzg5ODIyLCJpYXQiOjE3NzI4MDgyNTUsImV4cCI6MjA4ODM4NDI1NSwiaXNzIjoicHViLTAiLCJzdWIiOiJsaW5rLXJlZGlyZWN0In0.TzSa57QGL_uEOBLHfaua5y18EH_JTYvWiwfJuPQIlrk?&amp;utm_source=substack&amp;utm_medium=email" style="font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';display: inline-block;font-weight: 500;border: 1px solid rgb(0,0,0,.1);border-radius: 9999px;text-transform: uppercase;font-size: 12px;line-height: 12px;padding: 9px 14px;text-decoration: none;color: rgb(119,119,119);"><img class="icon" src="https://substackcdn.com/image/fetch/$s_!5EGt!,w_36,c_scale,f_png,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Ficon%2FNoteForwardIcon%3Fv%3D4%26height%3D36%26fill%3Dnone%26stroke%3D%2523808080%26strokeWidth%3D2" width="18" height="18" style="margin-right: 8px;min-width: 18px;min-height: 18px;border: none;vertical-align: middle;max-width: 18px" alt=""><span class="email-button-text" style="vertical-align: middle;">Restack</span></a></td></tr></tbody></table></td></tr></tbody></table></td><td align="right"><table role="presentation" width="auto" border="0" cellspacing="0" cellpadding="0"><tbody><tr></tr></tbody></table></td></tr></tbody></table></td></tr><tr height="16"><td height="16" style="font-size:0px;line-height:0;">&nbsp;</td></tr></tbody></table><div class="footer footer-ZM59BM" style="color: rgb(119,119,119);text-align: center;font-size: 16px;line-height: 26px;padding: 24px0;"><div style="font-size: 16px;line-height: 26px;padding-bottom: 24px"><p class="pencraft pc-reset color-secondary-ls1g8s size-12-mmZ61m reset-IxiVJZ small meta-B2bqa5" style="list-style: none;font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';padding-bottom: 0;font-size: 12px;line-height: 16px;margin: 0;color: rgb(119,119,119);text-decoration: unset;">© 2026 <span>Danny Moerkerke</span><br>548 Market Street PMB 72296, San Francisco, CA 94104 <br><a href="https://substack.com/redirect/2/eyJlIjoiaHR0cHM6Ly9tb2Rlcm53ZWJ3ZWVrbHkuc3Vic3RhY2suY29tL2FjdGlvbi9kaXNhYmxlX2VtYWlsP3Rva2VuPWV5SjFjMlZ5WDJsa0lqb3pNalF6T0RrNE1qSXNJbkJ2YzNSZmFXUWlPakU0T0RRNE56WXdNQ3dpYVdGMElqb3hOemN5T0RBNE1qVTFMQ0psZUhBaU9qRTRNRFF6TkRReU5UVXNJbWx6Y3lJNkluQjFZaTB4TmpNMk5qZzNJaXdpYzNWaUlqb2laR2x6WVdKc1pWOWxiV0ZwYkNKOS5jdEppSUFUU2tmUEp4N3JmSXFHUzR3eTQ1ME1BeV9SN0JYOTM4OTJFX0tvIiwicCI6MTg4NDg3NjAwLCJzIjoxNjM2Njg3LCJmIjp0cnVlLCJ1IjozMjQzODk4MjIsImlhdCI6MTc3MjgwODI1NSwiZXhwIjoyMDg4Mzg0MjU1LCJpc3MiOiJwdWItMCIsInN1YiI6ImxpbmstcmVkaXJlY3QifQ.gDimNjTh2tQSWbjeeM2jVZ-veawlo8NzP7oKaMO6liY?" style="color: #51b4e9;text-decoration: none;"><span style="color: rgb(119,119,119);text-decoration: underline;">Unsubscribe</span></a></p></div><p class="footerSection-EHR0jG small powered-by-substack" style="padding: 0 24px;font-size: 12px;line-height: 20px;margin: 0;color: rgb(119,119,119);font-family: system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';padding-bottom: 0;margin-top: 0;"><a href="https://substack.com/redirect/aa6d30c5-0a98-470b-bbb7-d31080565c39?j=eyJ1IjoiNWQ0c3NlIn0.bfcDdxgbGNFT8euIiLGt3BNcnmDdb5euKVgOcZ2aEZM" style="color: #51b4e9;text-decoration: none;display: inline-block;margin: 0 4px;"><img src="https://substackcdn.com/image/fetch/$s_!IzGP!,w_262,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Femail%2Fgeneric-app-button%402x.png" srcset="https://substackcdn.com/image/fetch/$s_!DIki!,w_131,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Femail%2Fgeneric-app-button.png, https://substackcdn.com/image/fetch/$s_!IzGP!,w_262,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Femail%2Fgeneric-app-button%402x.png 2x, https://substackcdn.com/image/fetch/$s_!QWua!,w_393,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Femail%2Fgeneric-app-button%403x.png 3x" width="131" alt="Get the app" height="40" style="max-width: 550px;border: none !important;vertical-align: middle;"></a><a href="https://substack.com/redirect/2/eyJlIjoiaHR0cHM6Ly9zdWJzdGFjay5jb20vc2lnbnVwP3V0bV9zb3VyY2U9c3Vic3RhY2smdXRtX21lZGl1bT1lbWFpbCZ1dG1fY29udGVudD1mb290ZXImdXRtX2NhbXBhaWduPWF1dG9maWxsZWQtZm9vdGVyJmZyZWVTaWdudXBFbWFpbD1taG5oZkBmZWVkYi5pbiZyPTVkNHNzZSIsInAiOjE4ODQ4NzYwMCwicyI6MTYzNjY4NywiZiI6dHJ1ZSwidSI6MzI0Mzg5ODIyLCJpYXQiOjE3NzI4MDgyNTUsImV4cCI6MjA4ODM4NDI1NSwiaXNzIjoicHViLTAiLCJzdWIiOiJsaW5rLXJlZGlyZWN0In0.NNjLdpjXIfVGz92KOpyiM8FiU-BuEJnLR78Om7E4aYM?" style="color: #51b4e9;text-decoration: none;display: inline-block;margin: 0 4px;"><img src="https://substackcdn.com/image/fetch/$s_!LkrL!,w_270,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Femail%2Fpublish-button%402x.png" srcset="https://substackcdn.com/image/fetch/$s_!wgfj!,w_135,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Femail%2Fpublish-button.png, https://substackcdn.com/image/fetch/$s_!LkrL!,w_270,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Femail%2Fpublish-button%402x.png 2x, https://substackcdn.com/image/fetch/$s_!KjtY!,w_405,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Femail%2Fpublish-button%403x.png 3x" width="135" alt="Start writing" height="40" style="max-width: 550px;border: none !important;vertical-align: middle;"></a></p></div></div></td><td></td></tr></tbody></table><img src="https://eotrx.substackcdn.com/open?token=eyJtIjoiPDIwMjYwMzA2MTQ0NDEwLjMuZmVhODYxOWZhMjA3NzZiZkBtZzIuc3Vic3RhY2suY29tPiIsInUiOjMyNDM4OTgyMiwiciI6Im1obmhmQGZlZWRiLmluIiwiZCI6Im1nMi5zdWJzdGFjay5jb20iLCJwIjoxODg0ODc2MDAsInQiOiJuZXdzbGV0dGVyIiwiYSI6ImV2ZXJ5b25lIiwicyI6MTYzNjY4NywiYyI6InBvc3QiLCJmIjp0cnVlLCJwb3NpdGlvbiI6ImJvdHRvbSIsImlhdCI6MTc3MjgwODI1NiwiZXhwIjoxNzc1NDAwMjU2LCJpc3MiOiJwdWItMCIsInN1YiI6ImVvIn0.Ogb1i8oFlcA93AVIkU5DxCPm6lnfvR07roXh7-NuG90" alt="" width="1" height="1" border="0" style="height:1px !important;width:1px !important;border-width:0 !important;margin-top:0 !important;margin-bottom:0 !important;margin-right:0 !important;margin-left:0 !important;padding-top:0 !important;padding-bottom:0 !important;padding-right:0 !important;padding-left:0 !important;"><img width="1" height="1" alt="" src="https://email.mg2.substack.com/o/eJxEkEvK8yAARVfzOwxqjJqBawk-blpp1OKjJbv_SVv4pufC4XC97biVeppnaZ0EIwLTiyYwTCmuqebLSpBsPLYbMqrtCJvtf6sUeiF34y2XNKyaiXn1OziT1DvpAYXF8YWRaDjlks5UMiEEo9M87bBasnW3nCol3f5P0HTjUxuudesfky-JxLbtFZ8A0-sAuTI3O0JE9jB4oZ4l_3AMhmkttJKUfkk_nzAZ73agd1TyHG7zJaWRYz83ZOsOhJ94uCN622PJH5GcpdSKVJPu-X617UBwU8ykDRdKsjGbVAJqfsO9gcdxkv59cTTUyzFzMetVc05ehv8PAAD__wPhd4Y">
]]></description>
      <pubDate>Fri, 06 Mar 2026 14:44:22 +0000</pubDate>
      <link>https://newsletters.feedbinusercontent.com/8b8/8b807d45467e15232e2f185d5c6b0feb7769e708.html</link>
      <dc:creator>Modern Web Weekly</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5143984835</guid>
    </item>
    <item>
      <title><![CDATA[Here’s how to instruct a LLM to reference the ARIA Authoring Practices Guide]]></title>
      <description><![CDATA[<p>Say you’re working with a LLM and training it to write good frontend code. <a href="https://annaecook.com/writing/2026/2/2/accessible-design-is-digital-infrastructure">Good frontend code is accessible code</a>, so of course you want to instruct the LLM to produce it.</p>
<p>However, <a href="https://webaim.org/projects/million/#errors">the bulk of frontend code on the web is inaccessible</a> to some degree. Also know here that LLMs are trained on the majority of the world’s frontend code. Similarly, many contemporary LLM-friendly UI libraries claim to be accessible, but may have varying degrees of support when manually evaluated.</p>
<p><a href="https://microsoft.github.io/a11y-llm-eval-report/">The deck is a bit stacked</a>, folks.</p>
<p>Given that, you need to <strong>get creative about how to handle the LLM’s training</strong>. It might seem like a good idea to instruct an LLM—reference, skill, agent, sub-agent, what have you—to reference <a href="https://www.w3.org/WAI/ARIA/apg/">the ARIA Authoring Practices Guide</a> (<abbr>APG</abbr>), because it <a href="https://www.w3.org/WAI/ARIA/apg/patterns/">lists a lot of common UI patterns</a>, right?</p>
<p>Well, sorta.</p>
<h2 id="the-reality-of-things">The reality of things</h2>
<p><strong>The APG was created to demonstrate ARIA’s capabilities</strong>. Because of this, it <strong>disproportionately favors ARIA in its code examples</strong>. This is even in spite of <a href="https://www.w3.org/TR/using-aria/#rule1">ARIA’s first rule</a>.</p>
<p>The APG was also <strong>not</strong> created to serve as a pattern library, design system, or single source of truth for the “right way” to make something. Unfortunately, a lot of people treat it this way.</p>
<p>Furthermore, the code examples themselves on the APG have an—ahem—range of effectiveness. Some code examples are fine. Some of them kind of work in an awkward and confusing way. Some of them don’t work at all.</p>
<p>Recall here that the original reason for APG code is to <strong>be a showpiece for how ARIA could hypothetically be used</strong>. Because of this, it is not the APG’s concern that <a href="https://www.a11yproject.com/posts/aria-has-perfect-support/">ARIA does not have perfect support</a>.</p>
<p>Also recall that LLMs love ingesting and utilizing structured content. And it’s an understatement to say that <strong>code is structured content</strong>.</p>
<p>While we’re on the topic of code: The APG’s examples are written in a way that may be incompatible with your organization’s way of doing things. Think frameworks, naming conventions, linting rules, and other areas of friction.</p>
<p>The friction caused by this incompatibility may disrupt the <a href="https://www.ibm.com/think/topics/context-window">LLM’s context window</a> in some way. This is not a good thing.</p>
<h2 id="instrumentality">Instrumentality</h2>
<p>If you instruct a LLM to retrieve <strong>all</strong> of the content of the APG, <strong>this bias will be encoded and perpetuated</strong> at the scale of the number of people who use it.</p>
<p>You should also consider the second-stage effects of other LLMs referencing the code your LLM generates. Meanwhile, I will be considering quitting tech to start a combination punk rock venue/mushroom farm.</p>
<h3 id="apg%3A-the-good-parts">APG: The good parts</h3>
<p>From the perspective of both a LLM <strong>and</strong> a human, the good parts of the APG are:</p>
<ul>
<li><strong>The pattern names</strong>. Having a standardized way to refer to a discrete piece of UI that is larger than any one company is highly beneficial.</li>
<li><strong>The content contained in the “About This Pattern” section</strong>. This is what the discrete piece of UI does, and how it goes about doing it.</li>
<li><strong>The content contained in the “Keyboard Interaction” section</strong>. This is how people who use assistive technology will expect things to work.</li>
</ul>
<p>Everything else <a href="https://adrianroselli.com/2023/04/no-apgs-support-charts-are-not-can-i-use-for-aria.html">is suspect</a>.</p>
<h3 id="but-what-about-the-%E2%80%9Cwai-aria-roles%2C-states%2C-and-properties%E2%80%9D-section%3F">But what about the “WAI-ARIA Roles, States, and Properties” section?</h3>
<p>LLMs are not good at nuance.</p>
<p>The WAI-ARIA Roles, States, and Properties section of each APG pattern should be treated as <strong>human-facing informative content</strong>. This is to better avoid a situation where a LLM will “read” that something should have a role, then <a href="https://css-tricks.com/i-learned-the-first-rule-of-aria-the-hard-way/">try to literally apply a <code>role</code> attribute</a>.</p>
<p>You want to encourage use of <a href="https://primer.style/accessibility/design-guidance/semantic-html-and-aria/#native-semantics">the role implicitly conferred by a native, semantic HTML element</a>. Using the <code>button</code> element for buttons, the anchor element for links—you know, that sad old sorry story.</p>
<p>You will also probably need instructions on how to cajole the LLM to barf out the semantic HTML that the majority of the industry has largely been allergic to using for decades. <strong><a href="https://gerireid.com/blog/ai-is-accidently-making-documentation-accessible/">Organization-specific content</a> is likely key for this effort</strong>—especially if tethered to, and reinforced by a design system.</p>
<h2 id="shut-up-and-give-me-the-code%2C-nerd">Shut up and give me the code, nerd</h2>
<p>Here’s what I’ve found to be helpful:</p>
<pre class="language-txt"><code class="language-txt">## [ARIA Authoring Practices Guide (APG) patterns](https://www.w3.org/WAI/ARIA/apg/patterns/)<br>Partially trusted source. Use selectively.<br><br>**CAN reference:**<br>- Content in "About This Interaction" sections<br>- Content in "Keyboard Interaction" sections<br><br>**CANNOT reference:**<br>- Code examples from APG<br>- CodePen links (URLs starting with `https://codepen.io/`)</code></pre>
<p>This might change over time, but right now it seems to help to push a LLM towards only slurping up the salient bits.</p>
<h2 id="there-is-a-world-of-difference-between-a-disclosure-and-a-tree-view">There is a world of difference between a disclosure and a tree view</h2>
<p>The more complicated a component is, the higher the chance it will need tweaks to its underlying structure to interface with assistive technology as expected.</p>
<p>Content like <a href="https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/">disclosures</a> are both simple and well-established. This means they have good support. As a spicy twist here, the APG’s recommendation of <code>aria-controls</code> fails to mention that JAWS is the only screen reader to support the declaration, thus reinforcing the larger point of this post.</p>
<p>Content like tree views are far more complex. They <a href="https://github.blog/engineering/user-experience/considerations-for-making-a-tree-view-component-accessible/#fix-interoperability-issues">need intervention to work as expected</a> across a spread of popular assistive technologies.</p>
<p>This range of differences is even more true as these entities are brought out of the realm of the platonic and are integrated into the real-world ecosystems that are your websites and web apps—with all their constituent functionality and content needs.</p>
<h2 id="this-kind-of-insinuates-that-you%E2%80%99re-indirectly-advocating-for-the-removal-of-code-examples-from-the-apg">This kind of insinuates that you’re indirectly advocating for the removal of code examples from the APG</h2>
<p>I know that effort and care went into the creation of the APG’s code examples. I also am acutely aware of how draining it can be to manage a large and popular open source project, especially one that does not pay.</p>
<p>However, I also get the sense that the APG’s code examples are not actively maintained. I think the code examples’ inclusion are poised to cause more harm than good. This is born of considering network effects combined with an LLM-first approach seeming to be how the industry is settling on building things.</p>
<h2 id="i%E2%80%99m-not-working-on-a-llm%2C-but-our-organization-is">I’m not working on a LLM, but our organization is</h2>
<p>It might be worth asking how your organization’s LLM(s) are approaching producing accessible frontend code. If referencing the APG, it also might be worth investigating how the LLM has specifically been instructed about going about it.</p>
<p>I am willing to bet said instructions are nothing more than a URL included in a list of other related resources. This is likely due to most people’s misunderstanding of the APG’s original intent—to say nothing of <a href="https://adrianroselli.com/2025/10/openai-aria-and-seo-making-the-web-worse.html">OpenAI putting their thumb on the scale</a> with an amount of influence matched only by their ignorance.</p>
<p>Consider approaching asking with a sense of careful curiosity. Systems may not react well to pointing out that their magic code generating toy has flaws, even if you believe yourself to be <a href="https://www.phrases.org.uk/meanings/on-the-side-of-the-angels.html">on the side of the angels</a>.</p>
<h2 id="how-do-i-teach-my-llm-about-aria-if-the-apg-ain%E2%80%99t-it%3F">How do I teach my LLM about ARIA if the APG ain’t it?</h2>
<p>You can point it at <a href="https://www.w3.org/TR/wai-aria/">the WAI-ARIA specification</a> for starters. You might also want to think about how you do so, since it is a large document.</p>
<p>Another thing to consider is <strong>granularly explaining the relationship between ARIA usage and human-facing value</strong> in your documentation. These more discrete, per-component examples will probably also help guard against unwanted hallucinations.</p>
<p>As an aside: We live in a liminal space where LLM-optimized instructional content still <a href="https://ericwbailey.website/published/evolved-antennas-llm-generated-code-and-a-potential-antifuture/">approximately resembles human-friendly language</a>. I wish an appeal to human-facing value was enough to incentivize organizations to invest in good documentation, but I’ll take what I can get.</p>
<h2 id="so%2C-what-about-aria-that-isn%E2%80%99t-supported-by-assistive-technology%3F">So, what about ARIA that isn’t supported by assistive technology?</h2>
<p>Right now I am pinning all my hopes on <a href="https://lolaslab.co/blog/2025/accessibility-compat-data/">the Accessibility Compat Data Project</a> (<abbr>ACD</abbr>). Until that dream becomes a reality—and even after it—I advise <a href="https://ericwbailey.website/published/evaluating-fables-pay-per-project-offering/">testing your digital experiences with actual daily assistive technology users</a>.</p>
<p>And hey, maybe someone reading this post will <a href="https://opencollective.com/lolas-lab">help fund ACD’s vital efforts</a> the same way <a href="https://www.opensourceforu.com/2026/01/tailwind-css-collapses-under-ai-usage-rescued-by-google-vercel-lovable/">Google was so quick to throw money at Tailwind</a>.</p>
<h2 id="this-is-all-too-much%2C-i-just-don%E2%80%99t-want-my-boss-to-get-mad-at-me">This is all too much, I just don’t want my boss to get mad at me</h2>
<p><a href="https://commission.europa.eu/strategy-and-policy/policies/justice-and-fundamental-rights/disability/european-accessibility-act-eaa_en">The European Accessibility Act</a> is a thing now. Many US-based companies also have important clients with EU-based data sovereignty.</p>
<p>Speaking in broad generalities, there are two things engineers are good at: making things easier for themselves, and creating a big stink when it’s not convenient to do so. Maybe it’s time to start making some noise on issue trackers.</p>
<h2 id="while-i-have-you%2C-how-should-i-stay-on-top-of-non-deterministic-tools-potentially-breaking-the-vital-underlying-semantics-of-an-experience-every-time-they-generate-or-regenerate-code%3F">While I have you, how should I stay on top of non-deterministic tools potentially breaking the vital underlying semantics of an experience every time they generate or regenerate code?</h2>
<p>Man, I don’t know.</p>
<h2 id="won%E2%80%99t-components-created-via-structured-data-nip-the-non-deterministic-problem-in-the-bud%3F">Won’t components created via structured data nip the non-deterministic problem in the bud?</h2>
<p>Projects like <a href="https://json-render.dev/">json-render</a> are interesting. But also know the problems intrinsic to what I’m outlining exist on levels deeper than most are willing to invest in addressing.</p>
<h2 id="and-say%2C-what-about-performance-and-security%3F">And say, what about performance and security?</h2>
<p>Accessibility and security operate similarly, at least in terms of organizational dynamics.</p>
<p><a href="https://blurbify.net/why-do-llms-pick-react-js/">LLMs favor React</a>, and <a href="https://www.theregister.com/2025/12/15/react2shell_flaw_china_iran/">React can be hacked now</a>. So, this is a fun new battle between the forces of convenience and trust. And speaking of convenience: <a href="https://infrequently.org/2025/11/performance-inequality-gap-2026/">Performance is a lost battle</a>, as far as <a href="https://sgom.es/posts/2026-02-13-js-heavy-approaches-are-not-compatible-with-long-term-performance-goals/">I’m concerned</a>.</p>
<h2 id="don%E2%80%99t-you-think-all-of-this-is-a-moot-point-in-a-future-where-everything-is-agentically-operated%3F">Don’t you think all of this is a moot point in a future where everything is agentically-operated?</h2>
<p>That promised future—using a carnival ticket-based economy to push proprietary black box technology—seems to be <a href="https://bsky.app/profile/spavel.bsky.social/post/3mesvq6zue22c">perpetually right around the corner</a>. Let’s also not forget that most of these companies are selling their vision of the future at a loss.</p>
<p>In the meantime we have actual websites and web apps that need to be worked on.</p>
<h2 id="you-seem-kind-of-glum%2C-what-gives%3F">You seem kind of glum, what gives?</h2>
<p>I am becoming increasingly more convinced the future of accessibility on the web will be both:</p>
<ol>
<li>An unwinnable Cold War arms race of reactive <a href="https://en.wikipedia.org/wiki/Monkey_patch">monkey patching</a> to try and solve the <a href="https://notes.jim-nielsen.com/#2026-02-15T2316">all-gas-no-brakes</a>, ever-increasing nondeterministic code changes problem with even more non-deterministic code, all the while contending with <a href="https://www.computerworld.com/article/4059383/openai-admits-ai-hallucinations-are-mathematically-inevitable-not-just-engineering-flaws.html">certain intractable realities</a>.</li>
<li><a href="https://mantisandco.com/resources/guides/future-of-accessibility/ericwbailey">A larger performative dance</a> of <a href="https://buttondown.com/access-ability/archive/dont-fall-into-the-accessibility-gradescore-trap/">metrics-obsession</a> that inserts an artificial layer between nominally satisfying legal and contractual obligations and the more important work of ensuring an experience is actually usable.</li>
</ol>
<h2 id="are-you-spiraling%3F">Are you spiraling?</h2>
<p>Probably!</p>
<h2 id="harm-reduction-in-the-face-of-supposed-inevitability">Harm reduction in the face of supposed inevitability</h2>
<p>Pessimism of the intelligence, optimism of the will.</p>
<p>You’ll still be shepherding and deploying probabilistic munging. Hopefully now the shepherding and deploying are at least a tiny bit more directed.</p>
]]></description>
      <pubDate>Mon, 16 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://ericwbailey.website/published/heres-how-to-instruct-a-llm-to-reference-the-aria-authoring-practices-guide/</link>
      <dc:creator>Eric Bailey</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5120680763</guid>
    </item>
    <item>
      <title><![CDATA[A page is more than just a container for words]]></title>
      <description><![CDATA[
<p>The latest SEO fad is the idea that websites need a machine-only version. Strip out the layout, remove the “noise”, and hand LLMs a simplified view of your content.</p>

<p>The pitch is always framed as pragmatic. Modern websites are bloated. LLMs don’t need the design. They just want the content. So let’s strip things back, give machines what they want, and meet them where they are.</p>

<p>I understand the instinct. Anyone who’s spent time looking at real websites in production knows how much accidental complexity we’ve normalised. When it takes megabytes of JavaScript and a small prayer to render a paragraph of text, it’s tempting to conclude that the page itself is the problem.</p>

<p>But that conclusion is backwards.</p>

<p>What’s being optimised for here isn’t understanding, it’s extraction. And those are not the same thing.</p>

<p>A page is not just a container for words. It’s an editorial artefact. It has hierarchy, emphasis, framing and intent baked into it. What comes first matters. What’s prominent matters. What’s tucked away in a sidebar or a footnote matters. These things are not decorative flourishes for humans. They are signals about meaning.</p>

<p>When you flatten a page into markdown, you don’t just remove clutter. You remove judgment, and you remove context.</p>

<p>The other problem, which people tend to skate past, is <em>trust</em>. The moment you publish a machine-only representation of a page, you’ve created a second candidate version of reality. It doesn’t matter if you promise it’s generated from the same source or swear that it’s “the same content”. From the outside, a system now sees two representations and has to decide which one actually reflects the page.</p>

<p>And once that choice exists, ‘optimisation’ inevitably follows. Not necessarily in the cartoonish sense of outright deception, but in the far more common sense of tidying, polishing and shaping. Awkward caveats get softened. Commercial messages get clearer. The version intended for machines becomes a little cleaner, a little more persuasive, a little more flattering than the one a human actually sees.</p>

<p>So the consuming system has a problem. It can trust the machine-facing version and accept that it will be gamed. It can verify it against the human-facing version, which means extra fetches, extra parsing and extra logic to reconcile differences. Or it can ignore it and just parse the page itself.</p>

<p>At any kind of scale, that third option wins. Fetching one artefact and extracting what’s needed from it is dramatically cheaper than arbitrating between multiple representations forever, even if a machine temporarily leans on a cheaper shortcut for convenience.</p>

<p>The moment that shortcut becomes something a system has to trust, validate, or keep in sync, it stops being cheap. This is why alternate realities on the web have such a poor track record. Not because they’re philosophically offensive, but because they’re economically unsustainable.</p>

<p>There’s also a deeper misunderstanding running through all of this. People point out, correctly, that many LLM systems today don’t render pages in anything like the way a browser does. They fetch the document, they don’t load all the CSS, they don’t execute much JavaScript. From that, it’s tempting to conclude that layout and presentation don’t matter.</p>

<p>But that’s really a statement about today’s limitations, not about long-term relevance.</p>

<p>Google didn’t always render pages either. It learned to, because if you want to model relevance the way humans experience it, you can’t treat all text as equal just because it exists in the markup. Placement, prominence and context change how information is interpreted. They always have.</p>

<p>If these systems are trying to approximate human understanding, then rendering isn’t optional. It’s inevitable. And when that happens, all the signals you stripped away in the name of “LLM friendliness” suddenly matter again.</p>

<p>Which leads to the most uncomfortable part of this whole discussion.</p>

<p>A lot of the enthusiasm for markdown mirrors and machine-only feeds is really a proxy argument about bad websites. Sites where the DOM is incoherent. Sites where content only exists after several client-side miracles. Sites where design systems actively obscure hierarchy instead of clarifying it.</p>

<p>In those cases, stripping everything back does make the content easier to deal with. But that doesn’t mean the right solution is to maintain a shadow version of reality for machines. It means the page itself is failing at its job.</p>

<p>The boring fix still works. Semantic HTML. Clear structure. Sensible hierarchy. Progressive enhancement. Content that exists when the page loads. Layout that reflects editorial intent instead of fighting it.</p>

<p>Do that, and you don’t need a second version of your site. You get one version that works for users, for search engines, and for any system that wants to understand what the page is actually saying.</p>

<p>Ideas like <code>llms.txt</code> sit in the same category. Well-intentioned, occasionally useful as hints, but fundamentally unable to escape the same gravity once they’re relied upon. Separate surface area, separate incentives, same trust and verification problems. Treat them as convenience, not as a strategy.</p>

<p>There isn’t going to be one version of the web for humans and another for machines. The systems that matter will always converge on the human-facing reality, because it’s the cheapest thing to fetch, the easiest thing to validate, and the richest source of meaning.</p>

<p>If you want to be understood by whatever comes next, don’t build shadows and side channels.</p>

<p>A page is not just a container for words. Make your site the best version of your site for whoever, or whatever, is accessing it.</p>
<p>The post <a href="https://www.jonoalderson.com/conjecture/more-than-words/">A page is more than just a container for words</a> appeared first on <a href="https://www.jonoalderson.com">Jono Alderson</a>.</p>
]]></description>
      <pubDate>Tue, 03 Feb 2026 14:43:59 +0000</pubDate>
      <link>https://www.jonoalderson.com/conjecture/more-than-words/</link>
      <dc:creator>Jono Alderson</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5130202232</guid>
    </item>
    <item>
      <title><![CDATA[CSS Scope &amp; Mixins]]></title>
      <description><![CDATA[<img src="https://www.oddbird.net/assets/images/winging-it/winging-it-28-1600w.jpeg" alt=""><p>At the end of 2025,
Firefox added the CSS <code>@scope</code> rule –
making the new feature available
across all major browsers!
Since <a href="https://chriscoyier.net">Chris Coyier</a> has done
a fair amount of writing
and speaking on the topic,
we wanted to talk with him
about what that means.
Chris has also been writing
about the future of CSS mixins,
a new specification
Miriam is working on.
We answer questions,
get feedback,
and chat about where CSS is heading.</p>
<div data-callout="note"><div>
<p>Check out our <a href="https://www.youtube.com/playlist?list=PL4jAKUtAhpHk8uR1kdwGooFffrHfTly4g">CSS Mixins Playlist</a> today.</p>
</div></div>
<figure class="embed">
      <div class="gallery"></div></figure>
<p class="main-action"><a href="http://www.youtube.com/channel/UCUkHxN78y9On9YH1zd-aTGw?sub_confirmation=1">Subscribe to Channel »</a></p>
<div class="anchor-link-wrapper">
<h2 id="what-we-cover%3A" tabindex="-1">What We Cover:</h2>
</div>
<ul>
<li>What <code>@scope</code> is and when it is useful</li>
<li>How CSS functions differ from mixins</li>
<li>Current browser support for mixins</li>
<li>When to use a mixin</li>
<li>Will <code>@content</code> work for CSS mixins, similar to Sass?</li>
<li><code>@content</code> vs <code>@contents</code></li>
<li>Fun with functions</li>
</ul>
<div class="anchor-link-wrapper">
<h2 id="links%3A" tabindex="-1">Links:</h2>
</div>
<ul>
<li><a href="https://frontendmasters.com/blog/scope-in-css/">Scope in CSS</a></li>
<li><a href="https://frontendmasters.com/blog/thoughts-on-native-css-mixins/">Thoughts on Native CSS Mixins</a></li>
<li><a href="https://codepen.io/miriamsuzanne/pen/ExrdXrZ">Embedded scopes pen</a></li>
<li><a href="https://codepen.io/miriamsuzanne/pen/azONKwv">Gradient text mixin pen</a></li>
<li><a href="https://codepen.io/miriamsuzanne/pen/GgJZmBd">Flag functions pen</a></li>
</ul>
]]></description>
      <pubDate>Thu, 22 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://www.oddbird.net/2026/01/22/winging-it-28/</link>
      <dc:creator>OddBird</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5122904098</guid>
    </item>
    <item>
      <title><![CDATA[An in-depth guide to customising lists with CSS]]></title>
      <description><![CDATA[<p>This first rule of styling lists is that they should be treated with the same reverence you would show any other text. If a list is inserted within a passage of text, treat it as a continuation and integral part of that text.</p>
<p>For bulleted or <dfn>unordered</dfn> lists, use padding to indent each list item the equivalent distance of a line height. This will allow the bullet to sit neatly in a square of white-space.</p>
<pre><code>ul {
	padding-inline-start: 1lh;
}
</code></pre>
<p>Numbered or <dfn>ordered</dfn> lists which reach into double figures and beyond will require more space. Allocate this space in multiples and fractions of the line height.</p>
<h2>The basics: choosing different kinds of lists</h2>
<h3>Unordered lists</h3>
<p>Unordered lists are given filled <strong>discs</strong> for bullets by default. As lists are further nested, browsers apply <strong>circles</strong>. These are followed by filled <strong>squares</strong> for lists nested three or more deep.</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/WbxmjZm">Lists: example 0 - default nested lists</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>You can change which bullets are used through the <code>list-style-type</code> property with values of <code>disc</code>, <code>circle</code> and <code>square</code> respectively. For example, to make all unordered list items filled squares use:</p>
<pre><code>ul li {
	list-style-type: square;
}
</code></pre>
<p>If you want to use a different symbol for a bullet you can do so by specifying it in quote marks. You’re not limited to individual characters – as well as regular glyphs, you can use words and emoji.</p>
<p>The <code>-type</code> suffix of <code>list-style-type</code> implies that the bullets are typographical. You can also use images for your list bullets. Link to your image using the <code>list-style-image</code> property.</p>
<p>You can position the bullet in-line with your text by using <code>list-style-position: inside</code>. Use <code>outside</code> to set it back again.</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/VYjroYK">Lists: example 1 - basic list-style-type</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<pre><code>ul li.point {
	list-style-type: "👉️ ";
}

ul li.tool {
	list-style-image: url(wrench.svg);
}

ul.notes li {
	list-style-type: "Note: ";
	list-style-position: inside;
}
</code></pre>
<h3>Ordered lists</h3>
<p>The default style for numbered or <dfn>ordered</dfn> lists is a decimal progression, with each number followed by a full stop. You can change the numbering system to alphabetical or Roman numerals by applying the <code>lower-latin</code>, <code>upper-latin</code>, <code>lower-roman</code>, <code>upper-roman</code> or <code>decimal</code> values of <code>list-style-type</code>. You can also select from many non-Latin numbering systems such as Greek, Devanagari, Persian and Katakana – you’ll need to use these even when the document or element has the appropriate language set.</p>
<h3>Changing font and colour</h3>
<p>Numbering and bullets will inherit the colour and font of your list text. While this will provide your reader with a consistent experience, you may wish to style them differently to reduce their visual impact, or otherwise distinguish them from the text.</p>
<p>The bullets and numbering automatically displayed by browsers are known as <dfn>markers</dfn> in CSS. The good news is that these are created as <code>::marker</code> pseudo-elements which means you can style them directly. The bad news is that we are very limited in which styles we can apply. All you can change is the colour (not the background) and the font via any property beginning with <code>font-</code> such as family, weight and size. You can also animate these properties and specify directionality of the text.</p>
<p>Despite these limitations, being able to style the colour and font of the markers accounts for a common use case that required workarounds and extra markup in the past. For example, applying these styles directly to the markers…</p>
<pre><code>ol li::marker {
	color: gray;
	font-family: sans-serif;
	font-size: 0.8em;
}
</code></pre>
<p>…will reduce the visual impact of your numbering by making it grey, sans-serif and slightly smaller.</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/ogLoKxL">Lists: example 2 - styling using ::marker</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>By default, list markers use tabular numerals, meaning the numbers all line up nicely. If you wanted a different effect you would need to add <code>font-variant-numeric: proportional-nums</code> to your marker styling.</p>
<p>You might also be tempted to make your numeric markers much larger than your list text. This can be a pleasing effect, however simply changing the <code>font-size</code> of your marker to <code>3em</code> probably won’t achieve the result you’re hoping for:</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/vEKWoXm">Lists: example 3 - changing font size using ::marker</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>The gotcha is that browsers align the baseline of markers with that of the first line of the list item text. Therefore a large list item number will always stick up above the list item text. And because we can’t apply any positioning or layout properties directly to the marker there’s not much you can do about it, although there are alternatives which we’ll come to later.</p>
<p></p>
<h2>Generating your own marker content</h2>
<p>You can customise the generated content of the marker as it also takes the <code>content</code> property, like this:</p>
<pre><code>ul li::marker {
	/* make all the bullets pointing hand emoji */
  content: "👉️ ";
}

ol li::marker {
	/* follow each number by a parenthesis instead of a full stop */
  content: counter(list-item) ") ";
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/NPrwQbB">Lists: example 4 - generated content in ::marker</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>Generating content for markers is supported by Chromium and Firefox, but not by WebKit (Safari), with no support in sight at the time of writing. Safari falls back to regular bullets or numbering which may, or may not, be acceptable to you depending on whether you consider your styling a progressive enhancement rather than crucial to the meaning of your content.</p>
<p>So what is <code>::marker</code> good for? It’s definitely the way to go if you want to change just the colour or font of your list markers. Anything more and you’ll need a different technique. Before we get on to those we need to talk about symbols and counters.</p>
<p>So far we have been defining what the bullets in an entire list should look like, that is to say we’re using the same symbols for all list items at the same level of nesting. However it is also possible to define a sequence of symbols for your bullets. There are a couple of ways to do this. The first is with the <code>symbols()</code> function, which you can use as a value of the <code>list-style</code> property.  For example, you could specify the classic sequence of symbols for footnotes:</p>
<pre><code>ol {
	list-style: symbols("*" "†" "‡" "§");
}
</code></pre>
<p><img src="https://piccalil.b-cdn.net/images/blog/css-symbols-list.jpg" alt="The symbols, assigned in the above code block, rendered in Firefox" title="At the time of writing, this only works in Firefox. You can still <a href='https://codepen.io/clagnut/pen/dPXZxRv'>see the demo here though</a>"></p>
<p>In this case the symbols automatically double up when the end of your specified sequence is reached. You can add the <code>cyclic</code> keyword to prevent this. The default behaviour is <code>symbolic</code>.</p>
<pre><code>ul {
	list-style: symbols(cyclic "🌑" "🌓" "🌕" "🌗");
}
</code></pre>
<p><img src="https://piccalil.b-cdn.net/images/blog/css-symbols-list-2.jpg" alt="The symbols, assigned in the above code block, rendered in Firefox" title="At the time of writing, this only works in Firefox. You can still <a href='https://codepen.io/clagnut/pen/qENVePa'>see the demo here though</a>"></p>
<p>You can also specify that your symbols sequence is <code>numeric</code> like decimals …7 8 9 10 11… or <code>alphabetic</code> like …a b c aa ab ac ba…, or use <code>fixed</code> to revert to default numbering once your custom symbols run out – useful if you’re specifying a sequence like ① ②…⑧ ⑨.</p>
<p><em>However,</em> the <code>symbols()</code> function is <strong>only supported in Firefox</strong>, with no interest from <a href="https://bugs.webkit.org/show_bug.cgi?id=299922">Webkit</a> or <a href="https://issues.chromium.org/484510686">Chromium</a> at the time of writing. In theory you can also specify a sequence of images, but this is not supported in any browser.</p>
<p>The <a href="https://drafts.csswg.org/css-counter-styles/#symbols-function">CSS spec</a> says <q>The <code>symbols()</code> function allows a counter style to be defined inline in a property value, for when a style is used only once in a stylesheet and defining a full <code>@counter-style</code> rule would be overkill.</q>. And this brings us to some good news.</p>
<h2>Defining your own numbering sequence</h2>
<p>The <code>@counter-style</code> rule enables you to further specify exactly what symbols or text appear in your list markers, and in what order or combination. You can use it to replicate all the <code>symbols()</code>  and <code>::marker</code> content functionality we’ve seen so far, and cross-browser is support is good – it became <a href="https://web-platform-dx.github.io/web-features-explorer/features/counter-style/">Baseline Newly Available</a> in September 2023.</p>
<p>This is how to use <code>@counter-style</code> to achieve the <code>symbols()</code> examples we looked at earlier. This will work across all modern browsers today:</p>
<pre><code>@counter-style --footnotes {
  system: symbolic;
  symbols: '*' † ‡ §;
  suffix: " ";
}

@counter-style --moons {
  system: cyclic;
  symbols: 🌑 🌓 🌕 🌗;
  suffix: " ";
}
</code></pre>
<p>Which you apply to your lists using the <code>list-style</code> property:</p>
<pre><code>ol.footnotes { 	list-style: --footnotes; }
ul.moons { list-style: --moons; }
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/bNeYXOz">Lists: example 7 - @counter-style demo</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>Similarly one of the marker examples attempted to use a 👉️ for all its bullets. You can achieve that effect cross-browser like this:</p>
<pre><code>@counter-style --point {
  system: cyclic;
  symbols: 👉️;
  suffix: " ";
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/WbxXVPW">Lists: example 8 - @counter-style example with pointing hand bullets</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>These counter style examples consist of four parts. The first is a name for the style, such as <code>--point</code>. You can call the counter styles pretty much what you want. You don’t need to prefix them with <code>--</code> although I find it good practise to do so in order to be consistent with naming custom properties. The other parts of the rule are:</p>
<ul>
<li><code>system</code> which sets the algorithm used for how the counter works. This takes the same keywords as the <code>symbols()</code> function described earlier, plus <code>additive</code> and <code>extends</code> which we’ll come to later.</li>
<li><code>symbols</code> is a space separated list of numbers or bullets (in theory this can also take images but there’s no support at the time of writing). Some characters need to be quoted such as <code>*</code>  – to be safe you could put all the characters inside quotes, but generally you don’t need to.</li>
<li><code>suffix</code> is the character or characters inserted after the counter (a simple space in our examples so far).</li>
</ul>
<p>For completeness, counter style rules also take the following descriptors, some of which are more niche than others:</p>
<ul>
<li><code>prefix</code> is the character or characters inserted before the counter</li>
<li><code>negative</code> sets the characters before and after a negative counter value. For example, specifying <code>negative: "(" ")"</code> will wrap negative counters in parentheses, which is sometimes used in financial contexts, like “(2) (1) 0 1 2 3…”. It’s fair to say there are very few use cases for negatively numbered ordered lists, however this is still a good opportunity to specify a proper minus symbol instead of a hyphen, just in case: <code>negative: "−"</code>.</li>
<li><code>pad</code> gives you a fixed-width style of numbering by repeating a character before the counter such that all counters are the same width. In reality this means zero-padded decimal numbering. So if you know that your ordered list goes up to less than a thousand, you can specify <code>pad: 3 "0"</code> to ensure that all counters are (at least) 3 digits wide. This will cause 1 to be shown as "001", 20 as "020", 300 as "300", 4000 as "4000", and -5 as "-05" (still three characters wide).</li>
<li><code>range</code> enables you apply counter styling to specific ranges in the ordered list. For example adding <code>range: 3 5, 20 infinite</code> will only apply styles to list items with counters 3,4 ,5 and anything 20 and above. All other list items will get the default style of numbering.</li>
<li><code>speak-as</code> describes how assistive technologies should synthesise the spoken form of a counter. Our earlier example had <code>symbols: 🌑 🌓 🌕 🌗</code>. To prevent the browser introducing each list item with something like ‘first quarter moon’, you could set <code>speak-as: bullets</code> so they are introduced like a normal unordered list. See <a href="https://drafts.csswg.org/css-counter-styles-3/#counter-style-speak-as">the spec</a> for more options. Only Safari supports this at the moment but it’s a good candidate for progressive enhancement.</li>
</ul>
<p>For more examples and creative uses of <code>@counter-style</code> see <a href="https://css-tricks.com/some-things-you-might-not-know-about-custom-counter-styles/">Geoff Graham’s CSS Tricks article</a>.</p>
<p>Thinking back to our other marker example, we followed the number by a closing parenthesis instead of a full stop. This is a perfect use case for the <code>extends</code> system. Instead of starting from scratch and working out how to define decimal counting in a counter style, we can ‘extend’ the existing decimal style. You might like to think of <code>extends</code> as meaning ‘modifies’. Here’s how:</p>
<pre><code>@counter-style --decimalparen {
  system: extends decimal;
  suffix: ") ";
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/ByzJQYX">Lists: example 9 - @counter-style demo of 'extends' system</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>You can’t include a <code>symbols</code> descriptor when extending a counter style as that would imply  you’re defining a brand new style. You can extend any counter system from the standard list-types as well as any named custom counter styles.</p>
<p>The space character in the suffixed <code>") "</code>  exposes a slight inconsistency between browsers. If you take the space away you’ll see that Chrome and Firefox position the marker flush against the list item text. However Safari always introduces a gap, which you can’t remove. Most of the time this difference is subtle and inconsequential, but once you start heavily customising your markers it can be more obvious. This is particularly true if you’re increasing the font size of your markers, but as we saw earlier that brought its own problem with the marker being aligned to the first line of text.</p>
<p></p>
<h2>Creating your own marker box</h2>
<p>The only solution to all these inconveniences is to remove the <code>::marker</code> pseudo element and create your own in such a way that you can style it to your specific purposes.</p>
<p>Firstly remove the marker pseudo element by applying <code>list-style:none</code> to your list:</p>
<pre><code>ol {
	list-style: none;
}
</code></pre>
<p>Having done that, Safari will no longer announce your list to assistive software. To rectify this, tell browsers this really is a list by applying an ARIA role:</p>
<pre><code>&lt;ol role="list"&gt;
	&lt;li&gt;First item.
	  &lt;ol role="list"&gt;
			&lt;li&gt;Nested item.&lt;/li&gt;
		&lt;/ol&gt;
	&lt;/li&gt;
	&lt;li&gt;Second item.&lt;/li&gt;
&lt;/ol&gt;
</code></pre>
<p>Now create your own placeholder for the counter by creating <code>::before</code> pseudo elements and moving them into the margin. Then use generated content to display the counter using the <code>counter()</code> function.</p>
<pre><code>ol[role='list'] li::before {
	display:inline-block;
	width:4ch;
	padding-inline-end:1ch;
	margin-inline-start:-5ch;
	text-align:right;
	font-variant-numeric: tabular-nums; /* make the numbers line up nicely */

	content: counter(list-item) ".";
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/KwMZNYa">Lists: example 10 - generating custom list marker elements using li::before</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>The <code>counter()</code> function takes a counter name, which you can specify. In this case we’ve used  <code>list-item</code> which is a special name reserved as an <dfn>implicit counter</dfn> for the list being styled. It outputs the position of the list item.</p>
<p>You can also output the position of all the list numbers in nested sequence, separated by a character or string. To do this use the <code>counters()</code> function:</p>
<pre><code>ol[role='list'] li::before {
	content: counters(list-item,".") ":";
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/MYerbMv">Lists: example 11 - demo of counters() function for nested numbering</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>If you want your list to start at 1 and increment by 1 each time, you don’t need to do anything else provided you use <code>list-item</code> as your counter name. Otherwise you can reset or create a new counter, set its starting point (the default is 0) and increment by a defined amount for each list item:</p>
<pre><code>ol[role='list'].mylist {
	list-style: none;
	counter-reset: --myList 3;
}

ol[role='list'].mylist li {
	counter-increment: --myList 2;
}

ol[role='list'].mylist li::before {
	content: counter(--myList) ". ";
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/QwEaQqw">Lists: example 12 - counter reset and increment</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>You can use <code>counter-increment</code> with any selector meaning you can just as easily number headings in sequence:</p>
<pre><code>h2 {
	counter-increment: --h2;
}

h2::before {
	content: counter(--h2) ". ";
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/VYjyQrB">Lists: example 13 - automatically numbering headings</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>If all you want to do is set the start number for a list or adjust an item’s value, you don’t need to use CSS, you can do it in HTML using <code>start</code> and <code>value</code>. If you want to have the list items count down instead of up you’ll need to specify that in HTML with a <code>reverse</code> attribute:</p>
<pre><code>&lt;ol start="42"&gt;
  &lt;li&gt;First item starts at 42&lt;/li&gt;
  &lt;li&gt;This one is then 43&lt;/li&gt;
  &lt;li&gt;And we end on 44&lt;/li&gt;
&lt;/ol&gt;
</code></pre>
<pre><code>&lt;ol&gt;
  &lt;li&gt;This is number 1&lt;/li&gt;
  &lt;li value="42"&gt;This is number 42&lt;/li&gt;
  &lt;li&gt;It follows that this is 43&lt;/li&gt;
&lt;/ol&gt;
</code></pre>
<pre><code>&lt;ol reversed start="44"&gt;
  &lt;li&gt;Counting down from 44&lt;/li&gt;
  &lt;li&gt;Now we're at 43&lt;/li&gt;
  &lt;li&gt;The final answer.&lt;/li&gt;
&lt;/ol&gt;
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/ByzJYVX">Lists: example 14 - HTML start, value and reversed</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<h2>Tying it all together</h2>
<p>Finally let’s use some of what we’ve learned to create this list:</p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/ZYOOReL">Lists: example 15 - fancy list markers</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>Here’s the full CSS which we’ll walk through afterwards:</p>
<pre><code>ul[role='list'] {
  list-style: none;
  max-inline-size:23em;
  padding-inline-start:2.5em;
  line-height:1.25em;
}

ul[role='list'] li {
  margin-block-end:1lh;
  min-block-size: 2lh;
  position:relative;
  text-box: trim-start cap alphabetic;
}

@counter-style --fleurons {
  system: cyclic;
  symbols: ❦ ✾ ✤ ❈ ✺ ❥;
  suffix: "";
  speak-as: bullets;
}

ul[role='list'] li::before {
  position:absolute;
  inset-block-start: 0;
  inset-inline-start: -1.25em;
  inline-size:1em;
  text-box: trim-start cap alphabetic;
  text-align:end;
  font-size:2lh;
  line-height:1;
  font-weight:bold;
  color: hsl(3, 70%, 55%);
  content: counter(list-item, --fleurons);
}
</code></pre>
<h3>Breakdown</h3>
<p>Looking at each rule individually:</p>
<pre><code>ul[role='list'] {
  list-style: none;
  max-inline-size:23em;
  padding-inline-start:2.5em;
  line-height:1.25em;
}
</code></pre>
<p>We start by removing the default bullets, remembering to tell assistive software the list is still a list. We then set enough padding next to the list to accommodate our fancy new bullets.</p>
<pre><code>ul[role='list'] li {
  margin-block-end:1lh;
  min-block-size: 2lh;
  position:relative;
  text-box: trim-start cap alphabetic;
}
</code></pre>
<p>Next we address the list items themselves, spacing them apart using the list’s line-height to stick to a vertical rhythm. We give each item a minimum height to fit a bullet symbol, and set to relative positioning in preparation for our custom list markers. Finally we trim the text to the top of the capital letters to help us align our bullets consistently.</p>
<pre><code>@counter-style --fleurons {
  system: cyclic;
  symbols: ❦ ✾ ✤ ❈ ✺ ❥;
  suffix: "";
  speak-as: bullets;
}
</code></pre>
<p>We now define a sequence of fancy Unicode symbols to use as bullets. These will cycle round if we end up with more than six list items. We make sure they are announced  as ‘bullet’ rather than ‘rotated heavy black heart’, etc.</p>
<pre><code>ul[role='list'] li::before {
  position:absolute;
  inset-block-start: 0;
  inset-inline-start: -1.25em;
  inline-size:1em;
  text-box: trim-start cap alphabetic;
  text-align:end;
  font-size:2lh;
  line-height:1;
  font-weight:bold;
  color: hsl(3, 70%, 55%);
  content: counter(list-item, --fleurons);
}
</code></pre>
<p>Finally we create pseudo elements before each list item and absolutely position them off to the side, leaving a 0.25em gap. We want the bullets to nicely span two lines of text, so we make the font size <code>2lh</code>, remove any half-leading by setting the line height to <code>1</code>,  and trim the text box to the top of the capitals so it aligns with our list text. Finally we make the bullet bold and red, and write it to the page using generated content by way of the <code>list-item</code> implicit counter and our <code>--fleurons</code> counter style.</p>

<p>There could be a case for using anchor positioning here, however good old fashioned absolute positioning has 100% support and provides a very simple solution to the task at hand.</p>

<h2>In summary</h2>
<p>Phew. Modern CSS enables you to do so much with lists it’s sometimes hard to know where to start, especially as browser support is not complete. Here’s a summary of which properties you should turn to for different use cases.</p>

































<table><thead><tr><th>CSS</th><th>Use case</th></tr></thead><tbody><tr><td><code>list-style</code></td><td>Changing the basic bullet styles or numbering system. Using a Unicode symbol, emoji or text in place of a bullet. Using images for bullets.</td></tr><tr><td><code>li::marker</code></td><td>Colouring the numbering or bullets differently to the list text. Changing the <code>font-</code> properties of the numbering (but not its size unless the difference is subtle).</td></tr><tr><td><code>symbols()</code></td><td>Only supported by Firefox, use <code>@counter-style</code> instead.</td></tr><tr><td><code>@counter-style</code></td><td>For defining your own sequence of bullet symbols (not images) or a completely customised numbering system.</td></tr><tr><td><code>extends</code></td><td>Used within <code>@counter-style</code> to modify existing numbering systems, for example to change or remove the default "." suffix.</td></tr><tr><td><code>li::before</code></td><td>For complete control over marker positioning, especially if your bullets or numbering are much larger than the list text.</td></tr></tbody></table>
<h2>Sources and further reading</h2>
<p>I am indebted to the following articles, in no particular order:</p>
<ul>
<li><a href="https://kizu.dev/list-item-counter/">https://kizu.dev/list-item-counter/</a></li>
<li><a href="https://moderncss.dev/totally-custom-list-styles/">https://moderncss.dev/totally-custom-list-styles/</a></li>
<li><a href="https://hadrysmateusz.com/blog/css-list-styling">https://hadrysmateusz.com/blog/css-list-styling</a></li>
<li><a href="https://web.dev/articles/css-marker-pseudo-element">https://web.dev/articles/css-marker-pseudo-element</a></li>
<li><a href="https://css-tricks.com/some-things-you-might-not-know-about-custom-counter-styles/">https://css-tricks.com/some-things-you-might-not-know-about-custom-counter-styles/</a></li>
<li><a href="https://www.smashingmagazine.com/2019/07/css-lists-markers-counters/">https://www.smashingmagazine.com/2019/07/css-lists-markers-counters/</a></li>
</ul>
<p>Specifications:</p>
<ul>
<li><a href="https://drafts.csswg.org/css-counter-styles/">https://drafts.csswg.org/css-counter-styles/</a></li>
<li><a href="https://drafts.csswg.org/css-lists/">https://drafts.csswg.org/css-lists/</a></li>
<li><a href="https://html.spec.whatwg.org/multipage/grouping-content.html#the-ol-element">https://html.spec.whatwg.org/multipage/grouping-content.html#the-ol-element</a></li>
</ul>
        
        ]]></description>
      <pubDate>Thu, 19 Feb 2026 11:55:00 +0000</pubDate>
      <link>https://piccalil.li/blog/an-in-depth-guide-to-customising-lists-with-css/?ref=main-rss-feed</link>
      <dc:creator>Piccalilli - Everything</dc:creator>
      <guid isPermaLink="false">https://feedbin.me/entries/5123739182</guid>
    </item>
  </channel>
</rss>
