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

<channel>
	<title>Bram.us</title>
	<atom:link href="https://www.bram.us/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.bram.us</link>
	<description>A rather geeky/technical weblog, est. 2001, by Bramus</description>
	<lastBuildDate>Mon, 14 Sep 2026 22:03:39 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>
	<item>
		<title>Introducing &#x3C;&#x200B;rich-input&#x3E;, a GitHub-like search/filter text input to embed on your site</title>
		<link>https://www.bram.us/2026/09/14/introducing-rich-input-a-github-like-search-filter-text-input-to-embed-on-your-site/</link>
					<comments>https://www.bram.us/2026/09/14/introducing-rich-input-a-github-like-search-filter-text-input-to-embed-on-your-site/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Mon, 14 Sep 2026 21:38:37 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[custom elements]]></category>
		<category><![CDATA[forms]]></category>
		<category><![CDATA[opaquerange]]></category>
		<category><![CDATA[search]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36750</guid>

					<description><![CDATA[<p>While working on a side-project to better navigate and search through my record collection, I found myself in need of a rich search input: one that accepts free-form text alongside structured filters like <code>artist:&#8220;Aphex Twin&#8221;</code> or <code>label:&#8220;Warp Records&#8221;</code> — You know, like the one you see on GitHub when searching/filtering issues. </p><p>When I quickly realized a standalone custom element for this didn’t exist yet, I nerd sniped myself into <del>building</del> <ins>curating</ins> one.</p>]]></description>
										<content:encoded><![CDATA[<figure>
  <a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/rich-input-screenshot.png"><img fetchpriority="high" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/rich-input-screenshot.png" alt="" width="560" height="145" class="alignnone size-medium wp-image-36752" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/09/rich-input-screenshot.png 1914w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/rich-input-screenshot-560x145.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/rich-input-screenshot-1120x289.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/rich-input-screenshot-768x198.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/rich-input-screenshot-1536x396.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/rich-input-screenshot-1568x405.png 1568w" sizes="(max-width: 560px) 100vw, 560px" /></a><figcaption>Screenshot of <code>&lt;rich-input&gt;</code> in action</figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="intro">
<p>While working on a side-project to better navigate and search through my record collection, I found myself in need of a rich search input: one that accepts free-form text alongside structured filters like <code>artist:&#8220;Aphex Twin&#8221;</code> or <code>label:&#8220;Warp Records&#8221;</code> — You know, like the one you see on GitHub when searching/filtering issues. </p>
<p>When I quickly realized a standalone custom element for this didn’t exist yet, I nerd sniped myself into <del>building</del> <ins>curating</ins> one.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-need" name="the-need">#</a> The need for a rich search input</h3>
<p>If you have ever used search fields on GitHub, Gmail, or modern music databases, you know the pattern: you can type regular free-text words, but you can also narrow things down using structured <code>key:value</code> pairs. For my record collection side-project, I wanted exactly that kind of search engine-grade filter intelligence.</p>
<figure>
<a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/github-rich-search-filter.png"><img decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/github-rich-search-filter.png" alt="" width="560" height="174" class="alignnone size-medium wp-image-36775" /></a><figcaption>Screenshot of GitHub’s rich search/filter field showing the User popover you get after having typed <code>author:</code></figcaption></figure>
<p>I wanted to be able to type queries such as <code>ambient artist:"Aphex Twin" label:"Warp Records"; year:2024</code>, complete with autocompletion for both the filter keywords and their allowed values. On top of that, I wanted <mark>in-input syntax highlighting for the structured filter values</mark>, without relying on heavy frontend frameworks or breaking standard form behaviors.</p>
<p>Looking around a bit, it seemed like an easy-to-use, standalone custom element providing this out of the box simply did not exist yet. So I nerd sniped myself into creating one: <a href="https://rich-input.netlify.app/"><strong><code>&lt;rich-input&gt;</code></strong></a>.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#what-is-rich-input" name="what-is-rich-input">#</a> What is <code>&lt;rich-input&gt;</code>?</h3>
<p><code>&lt;rich-input&gt;</code> is a standalone Web Component that gives you smart, keyword-aware search fields. You configure it completely declaratively by nesting standard HTML <code>&lt;datalist&gt;</code> elements inside it:</p>
<pre><code class="language-html" style="tab-size: 2">&lt;script type="module" src="https://cdn.jsdelivr.net/npm/rich-input/index.js"&gt;&lt;/script&gt;

&lt;rich-input placeholder="Search music catalog..."&gt;
  &lt;!-- String filter with rich autocomplete options --&gt;
  &lt;datalist id="label" label="Record Label"&gt;
    &lt;option value="Warp Records"&gt;
      &lt;img src="assets/warp-records.png" height="50" width="50" alt="Warp Records"&gt;
      Warp Records
    &lt;/option&gt;
    &lt;option value="Ninja Tune"&gt;
      &lt;img src="assets/ninja-tune.jpg" height="50" width="50" alt="Ninja Tune"&gt;
      Ninja Tune
    &lt;/option&gt;
  &lt;/datalist&gt;

  &lt;!-- Numeric filter --&gt;
  &lt;datalist id="year" label="Release Year" data-type="number"&gt;
    &lt;option value="2026"&gt;&lt;/option&gt;
    &lt;option value="2025"&gt;&lt;/option&gt;
    &lt;option value="2024"&gt;&lt;/option&gt;
  &lt;/datalist&gt;
&lt;/rich-input&gt;</code></pre>
<p>With just that markup in place, the component gives you:</p>
<ul>
<li><strong>Dual Autocompletion:</strong> It automatically suggests filter keywords when you start typing a word (e.g. typing <code>l</code> suggests <code>label:</code>, typing <code>y</code> suggests <code>year:</code>) as well as the values for that keyword (typing <code>label:&#8220;W</code> suggests <code>Warp Records</code>).</li>
<li><strong>Full-String Suggestion Filtering:</strong> The suggestion dropdown filters against the full token string rather than a naive slice at the caret position. Whether your caret is at <code>label:&#8220;W|arp Records&#8221;</code> or right between the colon and quotes at <code>label:|&#8220;Warp Records&#8221;</code>, the dropdown cleanly matches the actual value.</li>
<li><strong>Rich Option Markup:</strong> You can embed <code>&lt;img&gt;</code> logos or avatars inside your <code>&lt;option&gt;</code> tags. The component extracts them into the autocomplete popover and exposes <code>::part(suggestion-image)</code> so you can style them (e.g. as circular avatars).</li>
<li><strong>Validation &amp; Squiggly Underlines:</strong> If you enter an unrecognized keyword or a value that is not part of the configured <code>&lt;datalist&gt;</code>, the component marks it as invalid with a wavy red underline via <code>::highlight(rich-input-invalid)</code> once your cursor moves away or the input loses focus.</li>
<li><strong>Native Form Participation:</strong> It uses <code>formAssociated = true</code>, so it seamlessly submits via standard <code>&lt;form&gt;</code> elements and works with <code>FormData</code>.</li>
</ul>
<p>Try it out here:</p>
<p><iframe height="800" style="width: 100%;" scrolling="no" title="&lt;rich-input&gt; demo" src="https://codepen.io/editor/bramus/embed/01a0a185-7c1d-7487-9082-528055f64685/447736d063b21a1f38511101f9bf4bc9?default-tab=result" frameborder="no" loading="lazy" allowtransparency="true">See the Pen <a href="https://codepen.io/editor/bramus/pen/01a0a185-7c1d-7487-9082-528055f64685/447736d063b21a1f38511101f9bf4bc9"><code>&lt;rich-input&gt;</code> demo</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>) on <a href="https://codepen.io">CodePen</a>.</iframe></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#how-it-works-opaquerange" name="how-it-works-opaquerange">#</a> Powering it with the OpaqueRange API</h3>
<p>I knew this project would be quite easy to build thanks to the new <strong>OpaqueRange API</strong> (<a href="https://olliewilliams.xyz/blog/opaquerange/">as recently covered by Ollie Williams</a>), available in Chrome 152+.</p>
<p>Historically, doing any kind of rich interaction inside a native <code>&lt;input&gt;</code> — such as measuring exact caret coordinates to position a popover, or applying styling to substrings within the input — was a notorious exercise in pain. You had to resort to brittle &#8220;mirror div&#8221; hacks: an off-screen or invisible <code>&lt;div&gt;</code> styled with the exact same font, padding, border, and scroll offsets, replicating the input’s text to measure coordinates.</p>
<p>With <code>OpaqueRange</code> native text inputs gain first-class range capabilities. You can call <code>input.createValueRange(start, end)</code> to get an <code>OpaqueRange</code> representing any text slice inside the control. This unlocks two massive superpowers:</p>
<ol>
<li><strong>Anchoring popovers:</strong> You can call <code>range.getBoundingClientRect()</code> directly on an <code>OpaqueRange</code> to get the exact viewport coordinates of the active token or caret, allowing the autocomplete popover to anchor precisely to the start of the token.</li>
<li><strong>Custom Highlights:</strong> You can register the ranges with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API">CSS Custom Highlight API</a> via <code>CSS.highlights.set(keyword, new Highlight(range))</code>. That means you can style each keyword’s value directly in your CSS stylesheet!</li>
</ol>
<pre><code class="language-css" style="tab-size: 2">/* Style record label values */
::highlight(label) {
  background-color: oklch(0.92 0.08 240);
  color: oklch(0.28 0.14 240);
  text-decoration: 2px underline solid oklch(0.5 0.15 240 / 0.5);
}

/* Style release years */
::highlight(year) {
  background-color: oklch(0.93 0.1 85);
  color: oklch(0.35 0.14 85);
}

/* Style invalid tokens with a wavy red squiggly */
::highlight(rich-input-invalid) {
  text-decoration: underline wavy #ef4444;
  text-decoration-skip-ink: none;
}</code></pre>
<p>Because these are native highlights, text selection, copy-pasting, undo/redo stacks, and caret navigation continue to work 100% natively without any layout shifts or DOM synchronization glitches.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#cross-browser-fallback" name="cross-browser-fallback">#</a> The Cross-Browser Fallback</h3>
<p>Because <code>OpaqueRange</code> is still brand new and currently only supported in Chromium 152+, the component also comes with a built-in fallback strategy. When <code>OpaqueRange</code> is not supported, the component internally swaps the native <code>&lt;input&gt;</code> inside its Shadow DOM for an adapted, single-line <code>[contenteditable]</code> element and resorts to regular CSS Custom Highlights to do the highlighting — a technique I pioneered in <a href="https://brm.us/syntax-highlighting#highlighting-contenteditable">https://brm.us/syntax-highlighting</a>.</p



<p><mark>This broadens browser support of the component to to Chromium 105+, Safari 17.2+, and Firefox 140+.</mark></p>
<p>For some reason the Custom Highlights do not cross into the Shadow DOM in Firefox and Safari. To fix that, the component also copies over document’s custom highlights from <code>document.stylesheets</code> into the Shadow DOM. Also accepted is a <code>&lt;style&gt;</code> element nested directly inside the <code>&lt;rich-text&gt;</code>.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#built-with-antigravity" name="built-with-antigravity">#</a> AI-Assisted with Google Antigravity</h3>
<p>Just like my previous two custom element projects (<a href="https://www.bram.us/2026/09/02/html-in-canvas-pageflip/"><code>&lt;hic-pageflip&gt;</code></a> and <a href="https://www.bram.us/2026/09/10/introducing-mermaid-element-a-custom-element-to-display-mermaid-diagrams/"><code>&lt;mermaid-element&gt;</code></a>), this component was built with AI assistance using <strong>Google Antigravity</strong>.</p>
<p>I used stock Antigravity powered by Gemini, configured with <a href="https://developer.chrome.com/docs/modern-web-guidance">the Modern Web Guidance skill</a> and the <a href="https://developer.chrome.com/blog/chrome-devtools-mcp">Chrome DevTools MCP server</a>.</p>
<p>In <a href="https://github.com/bramus/rich-input/blob/main/prompt.md">my prompt</a>, also explicitly pointed Antigravity to my two previous repositories, instructing it to replicate their project layout, demo structure, and styling. Finally, I also included links to resources about <code>OpaqueRange</code>, including <a href="https://olliewilliams.xyz/blog/opaquerange/">Ollie’s post</a>. You can check <a href="https://github.com/bramus/rich-input/blob/main/prompt.md">my initial prompt</a> here:</p>
<pre><code class="language-md">Create me a rich search component `&lt;rich-search&gt;` that can automplete values for keyword-bases searches.

The component is and acts like a regular `&lt;input type=text&gt;` so you can type in just text. But when typing in specific keywords it should add suggestions for those keywords. Those keyword-searches are in the form  of `keyword:value`

As an example, some keywords and values one could entere for a music application are:
- genre:"&lt;string&gt;"
- style:"&lt;string&gt;"
- year:&lt;number&gt;
- label:"&lt;string&gt;"
- artist:"&lt;string&gt;"
- mix:"&lt;string&gt;"
- mixid:&lt;number&gt;
- playlist:"&lt;string&gt;"
- playlistid:&lt;number&gt;

An example search string could be something like `label:"We Play House Recordings" year:2026 playlist:"WPH Classics"`.

The search field should be able to autocomplete:
- The keywords. E.g. when I type in `m` at the start of a new word, it should suggest `mix:` and `mixid:` which I can choose from a list of suggested options.
- The values for those keywords. E.g. when I start typing `label:"K|` (with | being the current cursor position), then it should suggest the labels “Kranky” and “Keinemusik”.

The configuration of all this happens through `&lt;datalist&gt;` elements that are placed inside the `&lt;rich-search&gt;` element. E.g. this could be the list that suggest values for `label`:

```html
&lt;datalist id=label label="Record Label"&gt;
  &lt;option value="Defected"&gt;&lt;/option&gt;
  &lt;option value="Keinemusik"&gt;&lt;/option&gt;
  &lt;option value="Kranky"&gt;&lt;/option&gt;
  &lt;option value="Ninja Tune"&gt;&lt;/option&gt;
  &lt;option value="We Play House Recordings"&gt;&lt;/option&gt;
  &lt;option value="XL Recordings"&gt;&lt;/option&gt;
&lt;/datalist&gt;
```

Technically, implement this using the OpaqueRange API. Check these resources for info:
- https://chromestatus.com/feature/6297362687066112
- https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/OpaqueRange/explainer.md
- https://olliewilliams.xyz/blog/opaquerange/

Styling of the values using the Custom Highlights API should be possible (e.g. `::highlight(label)` can be used to style the value set in label:"We Play House Recordings"). Styling of the input itself should be done using `::part()`.

Follow the project structure as seen in the projects https://github.com/bramus/hic-pageflip and https://github.com/bramus/mermaid-element. Also use the same demo structure and style.</code></pre>
<p>Antigravity handled this prompt very well. Like, it came up with adding a little leading icon, added a “clear input” button, added keyboard navigation, added the <code>getParsedQuery()</code> API, etc. all without me asking 🙂</p>
<p><em>(Later on I <a href="https://github.com/bramus/rich-input/commit/87dcdb8157def64d4590fbbbe5a990e26a237a3e">renamed</a> it from <code>&lt;rich-search&gt;</code> to <code>&lt;rich-input&gt;</code>, added <a href="https://github.com/bramus/rich-input/commit/e34339fbf3cfc8f2b5258d4d41eed57f34e9722e">support for images</a>, added the fallbacks, etc.)</em></p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<p>If you take a look at the <a href="https://github.com/bramus/rich-input/commits/main/">commit history on GitHub</a>, you’ll notice that most commit messages include the exact prompt I used. One commit I really liked is <a href="https://github.com/bramus/rich-input/commit/c5011e7389b199263fb44ef5f7ced87a50229fed">the one where I asked it to generate a visual</a> similar to <a href="https://una.im/select-updates/#new-elements-and-pseudos">the one Una made for Customizable Select</a>.</p>
<p>The result <em>(after two extra nudges that got squashed into <a href="https://github.com/bramus/rich-input/commit/c5011e7389b199263fb44ef5f7ced87a50229fed">the commit</a>)</em> is this nice visual:</p>
<figure class="resizable">
  <img decoding="async" class="style-svg" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/rich-input-parts.svg" /><figcaption>The visual Antigravity generated.</figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#get-the-package" name="get-the-package">#</a> Get the package</h3>
<p>You can use <code>&lt;rich-input&gt;</code> right now or drop it into your own projects:</p>
<p>To install it via npm:</p>
<pre class="language-bash"><code class="language-bash" style="tab-size: 2">npm install rich-input</code></pre>
<p>Once imported, the custom element is ready to use (the package registers the component all by itself)</p>
<pre class="language-js"><code class="language-js" style="tab-size: 2">import rich-input;</code></pre>
<p>Alternatively, you can load it directly in HTML from a CDN such as jsDelivr:</p>
<pre class="language-html"><code class="language-html" style="tab-size: 2">&lt;script type="module" src="https://cdn.jsdelivr.net/npm/rich-input/index.js"&gt;&lt;/script&gt;</code></pre>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#links-and-resources" name="links-and-resources">#</a> Links and Resources</h3>
<p>Check out the following links for more info and demos:</p>
<ul>
<li>🚀 <strong>Project Homepage &amp; Live Demo:</strong> <a href="https://rich-input.netlify.app/">https://rich-input.netlify.app/ &rarr;</a></li>
<li>📦 <strong>NPM Package:</strong> <a href="https://www.npmjs.com/package/rich-input"><code>rich-input</code> on npm &rarr;</a></li>
<li>💻 <strong>Source Code:</strong> <a href="https://github.com/bramus/rich-input"><code>bramus/rich-input</code> on GitHub &rarr;</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#spread-the-word" name="spread-the-word">#</a> Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/…">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117269454186489042">🦣 Mastodon</a></li>
<li><a href="https://lnkd.in/p/eAb2xezC">💼 LinkedIn</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/09/14/introducing-rich-input-a-github-like-search-filter-text-input-to-embed-on-your-site/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WebKit supports interactive-widget … and hopefully Safari will too?</title>
		<link>https://www.bram.us/2026/09/11/webkit-supports-interactive-widget-and-hopefully-safari-will-too/</link>
					<comments>https://www.bram.us/2026/09/11/webkit-supports-interactive-widget-and-hopefully-safari-will-too/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Fri, 11 Sep 2026 21:55:05 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[metatags]]></category>
		<category><![CDATA[viewport]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36717</guid>

					<description><![CDATA[<p>About a month ago, WebKit gained support for <code>interactive-widget</code>. While it’s not clear whether this will end up in Safari or not, this is pretty exciting news: If you have ever had a virtual keyboard obscure your fixed navigation bar, then <code>interactive-widget</code> is for you.</p><p>This post details a quick recap of what the feature does, how we got here, and a sneak peek at it running in WebKit.</p>]]></description>
										<content:encoded><![CDATA[<div class="intro">
<p>About a month ago, WebKit gained support for <code>interactive-widget</code>. While it’s not clear whether this will end up in Safari or not, this is pretty exciting news: If you have ever had a virtual keyboard obscure your fixed navigation bar, then <code>interactive-widget</code> is for you.</p>
<p>This post details a quick recap of what the feature does, how we got here, and a sneak peek at it running in WebKit.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-backstory-interop-2022" name="the-backstory-interop-2022">#</a> The backstory: Interop 2022</h3>
<p>Back in 2022, I did a lot of research into viewports as the lead of the <a href="https://github.com/web-platform-tests/interop-2022-viewport">Viewport Investigation Effort</a>, part of Interop 2022.</p>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/viewport-investigation-effort-testpages.png"><img decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/viewport-investigation-effort-testpages.png" alt="" width="560" height="374" class="alignnone size-medium wp-image-36727" /></a><figcaption>Screenshot of <a href="https://interop-2022-viewport.netlify.app/">the Viewport Investigation Effort Demo Pages</a></figcaption></figure>
<p>While that effort still hasn’t led to an actual Interop Proposal <em>(due to the lack of proper mobile testing, something that is <a href="https://github.com/web-platform-tests/interop-mobile-testing">in the works</a> ever since 2023 😏)</em>, one of the outcomes was that we at Chrome <a href="https://developer.chrome.com/blog/viewport-resize-behavior#changing_the_default_behavior_in_chrome_108">changed how viewports get resized</a> when <a href="https://github.com/web-platform-tests/interop-2022-viewport/blob/main/explainers/virtual-keyboard.md">the virtual keyboard</a> gets shown in Chrome on Android.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#enter-interactive-widget" name="enter-interactive-widget">#</a> Enter <code>interactive-widget</code></h3>
<p>Where Chrome used to resize the <a href="https://github.com/web-platform-tests/interop-2022-viewport/blob/main/explainers/layout-viewport.md">layout viewport</a> by default, its behavior changed in Chrome 108. As of that version, which shipped in 2022, Chrome only resizes <a href="https://github.com/web-platform-tests/interop-2022-viewport/blob/main/explainers/visual-viewport.md">the visual viewport</a>, thereby aligning its behavior with that of Safari.</p>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/resize-behavior-chrome-old-and-new.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/resize-behavior-chrome-old-and-new.png" alt="" width="560" height="315" class="alignnone size-medium wp-image-36730" /></a><figcaption>Chrome’s old and new resize behavior. In the new behavior, which aligns with what Safari does, Chrome only resizes the visual viewport (orange dotted outline) when the virtual keyboard gets shown. The Layout Viewport (blue dashed outline) remains untouched. This shipped in 2022.</figcaption></figure>
<p>The side-effect of this change, though, is that you could end up with <code>position: fixed</code> content that gets obscured by the virtual keyboard:</p>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/resize-behavior-chrome-old-and-new-with-fixed-content.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/resize-behavior-chrome-old-and-new-with-fixed-content.png" alt="" width="560" height="315" class="alignnone size-medium wp-image-36731" /></a><figcaption>Chrome’s old and new resize behavior, with <code>position: fixed</code> content. In the new behavior, that content can get obscured by the virtual keyboard.</figcaption></figure>
<p>As some developers heavily relied on the old behavior — or just want to have a fixed bottom toolbar that sits atop the virtual keyboard – we at Chrome <a href="https://github.com/bramus/viewport-resize-behavior/blob/main/explainer.md">developed</a> a way to control what should happen when the virtual keyboard pops up: the <code>interactive-widget</code> directive for use in <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/name/viewport">the viewport <code>&lt;meta&gt;</code> tag</a>.</p>
<pre><code class="language-html" style="tab-size: 2">&lt;meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content"&gt;</code></pre>
<p>The directive accepts three values:</p>
<ul>
<li><code>resizes-visual</code>: Resize only the Visual Viewport but not the Layout Viewport <em>(default behavior)</em>.</li>
<li><code>resizes-content</code>: Resize both the Visual Viewport and Layout Viewport.</li>
<li><code>overlays-content</code>: Do not resize any viewport. This is similar to using the <a href="http://brm.us/virtual-keyboard">Virtual Keyboard API</a> with <code>overlaysContent</code> set to <code>true</code>.</li>
</ul>
<p>Put visually:</p>
<figure>
<a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/resize-behavior-side-by-side.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/resize-behavior-side-by-side.png" alt="" width="560" height="315" class="alignnone size-medium wp-image-36732" /></a><figcaption>The impact of the various values for <code>interactive-widget</code>. The orange dotted box is the Visual Viewport. The blue one is the Layout Viewport. Both resize differently depending on the value of <code>interactive-widget</code>.</figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#available-in-webkit" name="available-in-webkit">#</a> Available in WebKit</h3>
<p>About a month ago (mid-August), I was surprised to read a comment on the WebKit GitHub repo that <a href="https://github.com/WebKit/standards-positions/issues/65#issuecomment-5346189958"><code>interactive-widget</code> had been implemented in WebKit</a>.</p>
<p>Eager to test this out, it quickly became clear that this was not exactly possible without <a href="https://github.com/WebKit/WebKit/#building-for-apple-platforms">building WebKit yourself</a> and running it using a simple browser shell called <code>MobileMiniBrowser</code> on the iPhone Simulator. A pity, as I don’t think all web developers out there can find their way around that.</p>
<p>Anywho, today I bit the bullet and built WebKit locally. Some time later (with fan noises as background music throughout) I had <code>MobileMiniBrowser</code> open in an iPhone Simulator with <a href="https://viewport-resize-behavior.netlify.app/">the Viewport Resize Behavior demos</a> open.</p>
<div class="table">
<div>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-visual.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-visual.png" alt="" width="293" height="560" class="alignnone size-medium wp-image-36723" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-visual.png 852w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-visual-293x560.png 293w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-visual-585x1120.png 585w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-visual-768x1469.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-visual-803x1536.png 803w" sizes="auto, (max-width: 293px) 100vw, 293px" /></a><figcaption>MobileMiniBrowser showing a test page set to <code>resizes-visual</code></figcaption></figure>
</div>
<div>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-content.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-content.png" alt="" width="293" height="560" class="alignnone size-medium wp-image-36724" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-content.png 852w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-content-293x560.png 293w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-content-585x1120.png 585w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-content-768x1469.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-resizes-content-803x1536.png 803w" sizes="auto, (max-width: 293px) 100vw, 293px" /></a><figcaption>MobileMiniBrowser showing a test page set to <code>resizes-content</code></figcaption></figure>
</div>
<div>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-overlays-content.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-overlays-content.png" alt="" width="293" height="560" class="alignnone size-medium wp-image-36725" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-overlays-content.png 852w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-overlays-content-293x560.png 293w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-overlays-content-585x1120.png 585w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-overlays-content-768x1469.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/MobileMiniBrowser-overlays-content-803x1536.png 803w" sizes="auto, (max-width: 293px) 100vw, 293px" /></a><figcaption>MobileMiniBrowser showing a test page set to <code>overlays-content</code></figcaption></figure>
</div>
</div>
<p>While <code>interactive-widget</code> works fine in <code>MobileMiniBrowser</code> <em>(hooray!)</em>, I still would like to test things in a <em>real</em> Safari on iOS. For that I will probably have to wait for Safari 27.1 to come, which, I guess, will be the version that will include this change. It is not clear though, because <a href="https://developer.apple.com/documentation/safari-technology-preview-release-notes">the Safari Technology Preview release notes</a> have no mention of <code>interactive-widget</code>, even though Safari on desktop shows it in the list of feature flags …</p>
<figure>
<a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/safari-stp-feature-flag-interactive-widget.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/safari-stp-feature-flag-interactive-widget.png" alt="" width="560" height="423" class="alignnone size-medium wp-image-36737" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/09/safari-stp-feature-flag-interactive-widget.png 1896w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/safari-stp-feature-flag-interactive-widget-560x423.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/safari-stp-feature-flag-interactive-widget-1120x846.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/safari-stp-feature-flag-interactive-widget-768x580.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/safari-stp-feature-flag-interactive-widget-1536x1160.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/safari-stp-feature-flag-interactive-widget-1568x1184.png 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a><figcaption>The feature flag in Safari Technology Preview 252 on macOS</figcaption></figure>
<p>Reason I would like to test Safari on iOS itself, is because its UI differs a lot from the <code>MobileMiniBrowser</code>. For example, I’d like to see if and how Safari’s floating address bar influences things here. It’s probably fine already — there’s clever people work on Safari after all — but ever since <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/name/viewport#viewport-fit"><code>viewport-fit</code></a> ended up <a href="https://bsky.app/profile/bram.us/post/3lyxisc4mt22s">getting broken in Safari 26</a> <em>(and still is broken to this day)</em>, I’d like to double check.</p>
<p><em>(💭 Side thought: If only there were a Safari Technology Preview for iOS …)</em></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#browser-support" name="browser-support">#</a> Browser Support</h3>
<div class="note note--supergreen">
<p>💡 Although this post was originally published in September 2026, the list below is constantly being updated. <em>Last update: September 11, 2026</em>.</p>
</div>
<p><code>interactive-widget</code> is supported in the following browsers:</p>
<dl class="grid">
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>✅ Supported in Chrome 108 (Android)</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>✅ Supported in Firefox 133 (Android)</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>⏳ In development. Implemented in the WebKit source, but not yet shipped in a public release of Safari or Safari Technology Preview.</p>
</dd>
</dl>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#closing-thoughts" name="closing-thoughts">#</a> Closing thoughts</h3>
<p>I’m excited to see <code>interactive-widget</code>, a feature I helped shape, become part of WebKit. Hopefully it will come to Safari as well, so that developers can get control over how the viewport should resize when the virtual keyboard gets shown. To learn more about <code>interactive-widget</code>, check out the following resources I’ve authored:</p>
<ul>
<li><a href="https://www.htmhell.dev/adventcalendar/2024/4/">Control the Viewport Resize Behavior on mobile with <code>interactive-widget</code> &rarr;</a></li>
<li><a href="https://github.com/bramus/viewport-resize-behavior/blob/main/explainer.md">Viewport vs Virtual Keyboard Resize Behavior Explainer &rarr;</a></li>
<li><a href="https://developer.chrome.com/blog/viewport-resize-behavior">Prepare for viewport resize behavior changes coming to Chrome on Android &rarr;</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/09/11/webkit-supports-interactive-widget-and-hopefully-safari-will-too/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Introducing &#x3C;&#x200B;mermaid-element&#x3E;, a custom element to display Mermaid diagrams</title>
		<link>https://www.bram.us/2026/09/10/introducing-mermaid-element-a-custom-element-to-display-mermaid-diagrams/</link>
					<comments>https://www.bram.us/2026/09/10/introducing-mermaid-element-a-custom-element-to-display-mermaid-diagrams/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 10 Sep 2026 12:48:19 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[custom elements]]></category>
		<category><![CDATA[diagram]]></category>
		<category><![CDATA[mermaid]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36704</guid>

					<description><![CDATA[<p>Today I learned that <a href="https://codepen.io/">CodePen</a> supports <a href="https://mermaid.js.org/">Mermaid diagrams</a> — <a href="https://chriscoyier.net/">Chris Coyier</a> has a <a href="https://codepen.io/collection/xepBVz">a whole collection with examples</a> here — and I also learned that there is no custom element to easily include these in your markup … so I built one.</p>]]></description>
										<content:encoded><![CDATA[<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/mermaid-element.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/mermaid-element.png" alt="" width="560" height="239" class="alignnone size-medium wp-image-36707" /></a></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="intro">
<p>Today I learned that <a href="https://codepen.io/">CodePen</a> supports <a href="https://mermaid.js.org/">Mermaid diagrams</a> — <a href="https://chriscoyier.net/">Chris Coyier</a> has a <a href="https://codepen.io/collection/xepBVz">a whole collection with examples</a> here — and I also learned that there is no custom element to easily include these in your markup … so I built one.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<p>Looking at Chris’s demos, I see CodePen relies on Markdown code blocks with backticks (<code>```mermaid</code>) to render the diagrams. Seeing that in action got me wondering: is there a standalone custom element out there to drop Mermaid diagrams declaratively onto any web page? Turns out, there wasn’t.</p>
<p>So I built one, assisted by Antigravity: <a href="https://mermaid-element.netlify.app/"><code>&lt;mermaid-element&gt;</code></strong></a>.</p>
<p>It’s a zero-config, dependency-free web component that renders Mermaid diagrams inside an open Shadow DOM. Simply import the script and put your Mermaid syntax directly inside the custom element:</p>
<pre><code class="language-html" style="tab-size: 2">&lt;script type="module" src="https://cdn.jsdelivr.net/npm/mermaid-element/index.js"&gt;&lt;/script&gt;
&lt;mermaid-element&gt;
  graph TD
    Client[Client Request] --&gt; LB[Load Balancer]
    LB --&gt; Server1[Server 01]
    LB --&gt; Server2[Server 02]
&lt;/mermaid-element&gt;</code></pre>
<p><em>(Yes, you can also get the component <a href="https://www.npmjs.com/package/mermaid-element">from npm</a>)</em></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<p>By default, <code>&lt;mermaid-element&gt;</code> is a drop in component that will automatically load Mermaid <em>(version 12)</em> on demand via jsDelivr. However, if you want to use a different version of Mermaid, you can pin a specific version or supply a custom endpoint using the <code>mermaid</code> attribute. Just set <code>mermaid</code> to a version string <em>(e.g. <code>mermaid="10.9.8"</code>)</em> or a full URL, and <code>&lt;mermaid-element&gt;</code> will use that.</p>
<p>The component also comes with a built-in in-memory cache. If you have multiple diagrams on the same page, they will download Mermaid only once. Also, if your app already bundles Mermaid, you can assign your own copy to <code>MermaidElement.defaultMermaid</code> to bypass external CDN requests altogether.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<p>I also built an interactive playground to show some examples and to show how you can configure the component. The resulting HTML is shown right underneath the visualization, ready for you to copy and paste:</p>
<p class="codepen" data-height="1020" data-pen-title="&amp;lt;mermaid-element&amp;gt; playground" data-version="2" data-default-tab="result" data-slug-hash="QwpEOPj" data-user="bramus" style="height: 1020px; 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/editor/bramus/pen/01a08b11-6e3d-7f5f-afff-15b5a868ea35"><br />
  &lt;mermaid-element&gt; playground</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>)<br />
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><script async src="https://public.codepenassets.com/embed/index.js"></script></p>
<p>The <a href="https://mermaid-element.netlify.app/"><code>&lt;mermaid-element&gt;</code></strong> website</a> has some more examples (and instructions).</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<p>Check out the live documentation or grab the package from npm:</p>
<ul>
<li><a href="https://mermaid-element.netlify.app/"><code>&lt;mermaid-element&gt;</code> Website &amp; Live Demo &rarr;</a></li>
<li><a href="https://www.npmjs.com/package/mermaid-element"><code>mermaid-element</code> on npm &rarr;</a></li>
<li><a href="https://github.com/bramus/mermaid-element"><code>mermaid-element</code> on GitHub &rarr;</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/09/10/introducing-mermaid-element-a-custom-element-to-display-mermaid-diagrams/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Animating CSS Grid Layouts with CSS Anchor Positioning</title>
		<link>https://www.bram.us/2026/09/07/animating-css-grid-layouts-with-css-anchor-positioning/</link>
					<comments>https://www.bram.us/2026/09/07/animating-css-grid-layouts-with-css-anchor-positioning/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Mon, 07 Sep 2026 21:27:48 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36513</guid>

					<description><![CDATA[<p>At CSS Day (back in June), someone asked me how Brave (the browser) might have built their New Tab Page which features some tiles laid out in a grid. While the grid is nothing spectacular, something special happens with it when you resize the viewport: as the available space changes, the grid nicely reorganizes itself in an animated way. The animations are also interruptible, so no View Transitions seem to be at play.</p><p>So, how did they do it? I’m not sure. But if I were to recreate it, I’d use CSS Anchor Positioning with regular CSS Transitions on top.</p>]]></description>
										<content:encoded><![CDATA[<figure>
  <div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36513-1" width="640" height="427" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/animated-css-grid-2.mp4?_=1" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/animated-css-grid-2.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/09/animated-css-grid-2.mp4</a></video></div><figcaption>Recording of <a href="https://codepen.io/editor/bramus/pen/01a07dd4-4d23-7e7b-9f41-3e0aa126751f">the high-fidelity demo</a>.</figcaption></figure>
<div class="intro">
<p>At <a href="https://cssday.nl/">CSS Day</a> (back in June), someone asked me how Brave (the browser) might have built their New Tab Page which features some tiles laid out in a grid. While the grid in itself is nothing spectacular, something special happens with it when you resize the viewport: as the available space changes, the grid nicely reorganizes itself in an animated way. The animations are also interruptible, so no View Transitions seem to be at play.</p>
<p>So, how did they do it? I’m not sure. But if I were to recreate it, I’d use CSS Anchor Positioning with regular CSS Transitions on top.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
<p>💁‍♂️ Not familiar with Anchor Positioning yet? Check out my post <a href="https://www.bram.us/2026/02/28/anchors-aweigh-sotb2026/">Anchors Aweigh!</a> which explains all you need to know about it <em>(video included!)</em>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-technique" name="the-technique">#</a> The Technique</h3>
<p>The effect we are after is this one: as the grid resizes, the grid cells nicely animate to their new position:</p>
<figure>
  <div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36513-2" width="640" height="268" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/animated-css-grid.mp4?_=2" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/animated-css-grid.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/09/animated-css-grid.mp4</a></video></div><figcaption>Recording of <a href="https://codepen.io/bramus/pen/ogBYjgm">the animated grid demo</a>.</figcaption></figure>
<p>The trick here is to use <strong>CSS Anchor Positioning</strong> and anchor the cell’s content to the cell itself. Since you can’t anchor content directly onto a grid’s cell — grid cells are virtual concepts — you need to inject an extra wrapper div that is set to take up the cell’s available space and act as the anchor. Regular CSS transitions are then responsible for nicely animating things as they shift around.</p>
<pre><code class="language-html" style="tab-size: 2">&lt;div class="grid"&gt;
	&lt;div class="cell"&gt;
		&lt;div class="content"&gt;CONTENT&lt;/div&gt;
	&lt;/div&gt;
	&lt;div class="cell"&gt;
		&lt;div class="content"&gt;CONTENT&lt;/div&gt;
	&lt;/div&gt;
	…
&lt;/div&gt;</code></pre>
<pre><code class="language-css" style="tab-size: 2">.grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, 6rem);
	gap: 1em;
}

.cell {
	height: auto;
	aspect-ratio: 1;
	
	anchor-scope: --a;
	anchor-name: --a;
}

.content {
	position: absolute;
	position-anchor: --a;
	inset: anchor(inside);
	width: 6rem;
	height: 6rem;

	transition: inset 0.2s ease;
}</code></pre>
<p>There are four key details that make this whole setup click:</p>
<ol>
<li>
<p>Because anchor names are by default global, they need to be scoped to a specific cell’s subtree. <code>anchor-scope: --a;</code> takes care of that, allowing every <code>.cell</code> to reuse the <code>--a</code> identifier without clashing with any other cells.</p>
</li>
<li>
<p>The <code>aspect-ratio: 1;</code> on the <code>.cell</code>s ensures they take up as much height as they are wide.</p>
</li>
<li>
<p>The <code>transition</code> declaration ensures the <code>inset</code> nicely transitions from one value to the other.</p>
</li>
<li>
<p>The <code>.content</code> divs are given explicit dimensions (<code>width</code> and <code>height</code>). Without it, the <code>.content</code> divs would stretch and squeeze as they interpolate across the resizing grid.</p>
</li>
</ol>
<p>Unlike with View Transitions, this technique requires absolutely no JavaScript and the animations are perfectly interruptible. If you rapidly resize the viewport back and forth, the tiles don’t get queued up waiting for animation frames or transition promises to finish—they immediately redirect mid-flight toward their newly calculated positions.</p>
<div class="note">
<p>Note: This currently does not work in Firefox because it does not do <a href="https://drafts.csswg.org/css-anchor-position-1/#interleaving">style and layout interleaving</a> for Anchor Positioning. Instead of resolving to interpolable pixel values, it retains the <code>anchor()</code> value at <a href="http://brm.us/value-processing">computed value time</a>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#demo" name="demo">#</a> Demo</h3>
<p>Here is <a href="https://codepen.io/bramus/pen/ogBYjgm">the demo</a> I built to demonstrate this behavior:</p>
<p class="codepen" data-height="700" data-pen-title="Animated Reorganizing Grid with Anchor Positioning (No View Transitions!)" data-default-tab="result" data-slug-hash="ogBYjgm" data-user="bramus" 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/bramus/pen/ogBYjgm"><br />
      Animated Reorganizing Grid with Anchor Positioning (No View Transitions!)</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>)<br />
    on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><script async src="https://public.codepenassets.com/embed/index.js"></script></p>
<p>Try resizing the embed or the <code>.container</code> in the demo itself to see the grid rearrange itself.</p>
<p>Or, if you want something more high-fidelity (and that more closely matches the Brave New Tab Page IIRC):</p>
<p class="codepen" data-height="650" data-pen-title="Animated CSS Grid with CSS Anchor Positioning" data-version="2" data-default-tab="result" data-slug-hash="zxZqpqa" data-user="bramus" style="height: 650px; 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/editor/bramus/pen/01a07dd4-4d23-7e7b-9f41-3e0aa126751f"><br />
  Animated CSS Grid with CSS Anchor Positioning</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>)<br />
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#spread-the-word" name="spread-the-word">#</a> Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3muxheuln3224">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117231888486742468">🦣 Mastodon</a></li>
<li><a href="https://lnkd.in/p/ez-NhfSa">💼 LinkedIn</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
<p>If this post felt familiar:</p>
<ul>
<li>I actually <a href="https://bsky.app/profile/bram.us/post/3moe5buoab22w">shared a demo on Bluesky</a> about this back in June. I only got to writing this blog post today. Oh well.</p>
</li>
<li><a href="https://chriscoyier.net/">Chris Coyier</a> covered the approach of my demo on Frontend Masters. In his write-up, he looks back at how Masonry.js famously animated fluid layouts, and shows how my code brings that exact behavior natively to CSS. Definitely go check out his article: <a href="https://frontendmasters.com/blog/masonry-with-animation-in-css/">Masonry (with Animation) in CSS</a>.</li>
</ul>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/09/07/animating-css-grid-layouts-with-css-anchor-positioning/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/09/animated-css-grid.mp4" length="5531795" type="video/mp4" />
<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/09/animated-css-grid-2.mp4" length="557058" type="video/mp4" />

			</item>
		<item>
		<title>Introducing &#x3C;&#x200B;hic-pageflip&#x3E;: A Pageflip/Flipbook Custom Element powered by HTML-in-Canvas</title>
		<link>https://www.bram.us/2026/09/02/html-in-canvas-pageflip/</link>
					<comments>https://www.bram.us/2026/09/02/html-in-canvas-pageflip/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Wed, 02 Sep 2026 20:53:39 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[html-in-canvas]]></category>
		<category><![CDATA[pageflip]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36636</guid>

					<description><![CDATA[<p>Way back in 2005, I used a Flash PageFlip component to display a book on a website. Ever since, recreating a modern version has been on my backlog to investigate. With the arrival of the experimental HTML-in-Canvas API, we can finally bring it back — this time displaying real HTML.</p>]]></description>
										<content:encoded><![CDATA[<figure><div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36636-3" width="640" height="278" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/hic-pageflip.mp4?_=3" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/hic-pageflip.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/09/hic-pageflip.mp4</a></video></div><figcaption>Recording of <a href="https://hic-pageflip.netlify.app/">the demo</a>.</figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="intro">
<p>Way back in 2005, I used a Flash PageFlip component to display a book on a website. Ever since, recreating a modern version has been on my backlog to investigate. With the arrival of the experimental HTML-in-Canvas API, we can finally bring it back — this time displaying real HTML.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note note--warning">
<p><strong>⚠️👨‍🔬 Experimental Feature!</strong></p>
<p>The technology used to power this <code>&lt;hic-pageflip&gt;</code> component is still experimental and is undergoing significant changes. To see the component in action, you need Chrome with <code>chrome://flags#canvas-draw-element</code> enabled.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>PageFlip?</h3>
<p><strong>PageFlip</strong> is a classic UI pattern that simulates a hyper-realistic, interactive book or magazine: users can click, drag, and peel page corners to flip through content with dynamic lighting, curls, and shadows. It was popularized in 2005–2006 by Iparigrafika who shipped a Flash Component.</p>
<figure>
<a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/orig-pageflip.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/orig-pageflip.png" alt="" width="560" height="381" class="alignnone size-medium wp-image-36649" /></a><figcaption>Screenshot of the (now defunct) PageFlip website <em>(<a href="https://web.archive.org/web/20060307042409/http://www.iparigrafika.hu/pageflip/">archived</a>)</em></figcaption></figure>
<p>While recreating this effect on the web has technically been possible for years using 2D Canvas or WebGL shaders, it always came with massive accessibility and usability drawbacks. Because the pages were trapped inside a canvas pixel grid or rendered as static images, you lost selectable text, clickable links, screen reader support, and browser features like find-in-page.</p>
<p>Using HTML to render pages has also been possible by leveraging CSS transforms <em>(see <a href="https://online.fliphtml5.com/vnefl/pwjz/">this example</a>)</em>. However, that effect was always somewhat “fake”: it’s a pure 2D clip effect with a shadow laid on top, instead of true 3D folding and curling as described in <a href="https://blog.flirble.org/2010/10/08/the-anatomy-of-a-page-curl/">Chris Luke’s “The anatomy of a page curl”</a>.</p>
<p><script src="https://unpkg.com/@cloudfour/image-compare/dist/index.min.js"></script></p>
<figure>
<image-compare label-text=""><br />
  <img loading="lazy" decoding="async" slot="image-1" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-2d.jpg" alt="The 2D fold effect" width="560" height="505" class="size-medium wp-image-36653" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-2d.jpg 1276w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-2d-560x505.jpg 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-2d-1120x1010.jpg 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-2d-768x693.jpg 768w" sizes="auto, (max-width: 560px) 100vw, 560px" /><br />
  <img loading="lazy" decoding="async" slot="image-2" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-3d.jpg" alt="The 3D fold effect" width="560" height="505" class="size-medium wp-image-36651" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-3d.jpg 1276w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-3d-560x505.jpg 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-3d-1120x1010.jpg 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/pageflip-3d-768x693.jpg 768w" sizes="auto, (max-width: 560px) 100vw, 560px" /><br />
</image-compare><figcaption>Comparison of a 2D vs 3D fold effect. Look closely at the shadow and the curvature of the page, and how it affects the text. Use the slider to reveal either the 2D or 3D effect screenshot.</figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>HTML-in-Canvas was made for this</h3>
<p>Up until now, you always had to compromise: either get authentic 3D page curls with Canvas or WebGL at the cost of throwing away accessibility, or stick to real HTML with CSS transforms and settle for a flat 2D clipping illusion.</p>
<p>The new (and still experimental) <a href="https://developer.chrome.com/blog/html-in-canvas-origin-trial">HTML-in-Canvas API</a> changes the game. By letting you draw live DOM elements directly into canvas contexts and WebGL textures, you no longer have to choose. You can build true 3D conical and cylindrical page curls, while the pages remain fully interactive and accessible, with selectable text and clickable links.</p>
<p>Armed with Google Antigravity and the math from <a href="https://blog.flirble.org/2010/10/08/the-anatomy-of-a-page-curl/">Chris Luke&#8217;s article</a>, I set out to build it.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Keeping Things Easy: <code>&lt;hic-pageflip&gt;</code></h3>
<p>In under two hours, I had a working pageflip prototype powered by HTML-in-Canvas up and running. I then continued the work to further refine the interactions and visuals, and eventually had Antigravity refactor the whole thing into an easy-to-use web component: <strong><code>&lt;hic-pageflip&gt;</code></strong> </p>
<p>You can see it in action on the <a href="https://hic-pageflip.netlify.app/">demo website</a> which is also embedded here:</p>
<p><iframe src="https://hic-pageflip.netlify.app/" height="600"></iframe></p>
<p>The source code is <a href="https://github.com/bramus/hic-pageflip">available on GitHub</a> and you can <a href="https://www.npmjs.com/package/hic-pageflip">install the package from NPM</a>.</p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>Installation</h4>
<p>You can install the package <a href="https://www.npmjs.com/package/hic-pageflip">from npm</a>:</p>
<pre class="language-bash"><code class="language-bash">npm install hic-pageflip</code></pre>
<p>Import the package in your application to have it register the custom elements:</p>
<pre class="language-javascript"><code class="language-javascript">import 'hic-pageflip';</code></pre>
<p>You can also load it directly from a CDN and use it in your HTML, without npm at all. For example, using jsDelivr:</p>
<pre class="language-html"><code class="language-html">&#x3C;script type=&#x22;module&#x22; src=&#x22;https://cdn.jsdelivr.net/npm/hic-pageflip&#x22;&#x3E;&#x3C;/script&#x3E;</code></pre>
<p>Once installed and imported, you can start using it.</p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>Minimal Example</h4>
<p>With the component, creating a pageflip interface powered by HTML-in-Canvas becomes as easy as this:</p>
<pre class="language-html"><code class="language-html">&lt;hic-pageflip engine=&quot;3d&quot; page-width=&quot;300&quot; page-height=&quot;450&quot; page-background=&quot;#c1c8c7&quot;&gt;
&#x9;&lt;!-- Slide 1: Cover --&gt;
&#x9;&lt;hic-pageflip-page&gt;
&#x9;&#x9;&lt;div class=&quot;content&quot;&gt;
&#x9;&#x9;&#x9;&lt;h1&gt;Cover Page&lt;/h1&gt;
&#x9;&#x9;&#x9;&lt;p&gt;This is HTML inside a 3D WebGL pageflip!&lt;/p&gt;
&#x9;&#x9;&lt;/div&gt;
&#x9;&lt;/hic-pageflip-page&gt;

&#x9;&lt;!-- Slide 2 --&gt;
&#x9;&lt;hic-pageflip-page&gt;
&#x9;&#x9;&lt;div class=&quot;content&quot;&gt;
&#x9;&#x9;&#x9;&lt;h2&gt;Inside Left Page&lt;/h2&gt;
&#x9;&#x9;&#x9;&lt;p&gt;Selectable text and clickable &lt;a href=&quot;https://github.com/bramus/hic-pageflip&quot; target=&quot;_top&quot;&gt;links&lt;/a&gt; work natively.&lt;/p&gt;
&#x9;&#x9;&lt;/div&gt;
&#x9;&lt;/hic-pageflip-page&gt;

&#x9;&lt;!-- Slide 3 --&gt;
&#x9;&lt;hic-pageflip-page&gt;
&#x9;&#x9;&lt;div class=&quot;content&quot;&gt;
&#x9;&#x9;&#x9;&lt;h2&gt;Inside Right Page&lt;/h2&gt;
&#x9;&#x9;&#x9;&lt;p&gt;And images (with the right CORS headers)!&lt;/p&gt;
&#x9;&#x9;&#x9;&lt;p&gt;&lt;img src=&quot;https://gravatar.com/avatar/c47bf5e271115acb3820392b0a5c9574&quot; alt=&quot;Bramus&quot; height=&quot;50&quot; width=&quot;50&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;/p&gt;
&#x9;&#x9;&lt;/div&gt;
&#x9;&lt;/hic-pageflip-page&gt;

&#x9;&lt;!-- Slide 4: Backcover --&gt;
&#x9;&lt;hic-pageflip-page&gt;
&#x9;&#x9;&lt;div class=&quot;content&quot;&gt;
&#x9;&#x9;&#x9;&lt;h2&gt;Backcover&lt;/h2&gt;
&#x9;&#x9;&#x9;&lt;p&gt;And CSS as well &mldr;&lt;/p&gt;
&#x9;&#x9;&#x9;&lt;p class=&quot;rainbow&quot;&gt;Rainbow text, Yay!&lt;/p&gt;
&#x9;&#x9;&lt;/div&gt;
&#x9;&lt;/hic-pageflip-page&gt;
&lt;/hic-pageflip&gt;</code></pre>
<p>The enclosing <code>&lt;hic-pageflip&gt;</code> component creates the pageflip interface, and the <code>&lt;hic-pageflip-page&gt;</code> components are the pages contained within.</p>
<p>With some mininal styling applied, the code above looks like this:</p>
<p><iframe height="600" style="width: 100%;" scrolling="no" title="&lt;hic-pageflip&gt; demo" src="https://codepen.io/editor/bramus/embed/01a05ee7-e4cb-703d-b441-e9316147d7d0?default-tab=result" frameborder="no" loading="lazy" allowtransparency="true"><br />
  See the Pen <a href="https://codepen.io/editor/bramus/pen/01a05ee7-e4cb-703d-b441-e9316147d7d0">&lt;hic-pageflip&gt; demo</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>) on <a href="https://codepen.io">CodePen</a>.<br />
</iframe></p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>Customization</h4>
<p>Use the attributes on the <code>&lt;hic-pageflip&gt;</code> component to control a bunch of things, including which engine to use:</p>
<ol>
<li><strong>2D Engine (<code>engine="2d"</code>)</strong>: Uses 2D canvas affine matrix reflections, clipping, and dynamic drop shadows.</li>
<li><strong>3D Engine (<code>engine="3d"</code>)</strong>: Uses WebGL vertex shaders for true cylindrical and conical page curls, based on <a href="https://blog.flirble.org/2010/10/08/the-anatomy-of-a-page-curl/">Chris Luke’s page curl algorithm</a>.</li>
</ol>
<p>The default is <code>3d</code>, the one with the true page curl effect.</p>
<p>The <code>&lt;hic-pageflip&gt;</code> component itself only offers the pageflip interface, without any controls. However, you can control the pageflip programmatically by calling methods directly on the component:</p>
<pre class="language-javascript"><code class="language-javascript">const pageflip = document.querySelector('hic-pageflip');
// Go to next page
pageflip.next();
// Go to previous page
pageflip.previous();
// Go to page 4
pageflip.goToPage(4);
// Flip to a page
pageflip.flipTo(4);</code></pre>
<p>Hook these up to your own buttons and you’re good to go 🙂</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Links and resources</h3>
<p>To recap, here are all the links you need:</p>
<ul>
<li>🚀 <strong>Project Homepage + Demo</strong>: <a href="https://hic-pageflip.netlify.app/">https://hic-pageflip.netlify.app/ &rarr;</a></li>
<li>🤖 <strong>Minimal Demo</strong>: <a href="https://codepen.io/editor/bramus/pen/01a05ee7-e4cb-703d-b441-e9316147d7d0"><code>&lt;hic-pageflip&gt;</code> demo (CodePen) &rarr;</a></li>
<li>💻 <strong>Source Code</strong>: <a href="https://github.com/bramus/hic-pageflip"><code>bramus/hic-pageflip</code> &rarr;</a></li>
<li>📦 <strong>NPM Package</strong>: <a href="https://npmjs.com/package/hic-pageflip"><code>hic-pageflip</code> &rarr;</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3muktnmp3zk2i">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117203444201725394">🦣 Mastodon</a></li>
<li><a href="https://lnkd.in/p/eyps4dAa">💼 LinkedIn</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/09/02/html-in-canvas-pageflip/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/09/hic-pageflip.mp4" length="1280297" type="video/mp4" />

			</item>
		<item>
		<title>Feature Detecting “Undetectable” CSS Features with @supports named-feature()</title>
		<link>https://www.bram.us/2026/08/27/feature-detecting-undetectable-css-features-with-supports-named-feature/</link>
					<comments>https://www.bram.us/2026/08/27/feature-detecting-undetectable-css-features-with-supports-named-feature/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 27 Aug 2026 21:31:42 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[feature detection]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36574</guid>

					<description><![CDATA[<p>In CSS you can feature detect support for things like selectors, properties + values, and <a href="https://brm.us/at-rule">at-rules</a> using <code>@supports</code>. But how do you feature detect a change to an underlying implementation, or two existing properties suddenly working together? Enter <code>@supports named-feature()</code>, a function designed to expose these specific capabilities without relying on hacky workarounds.</p>]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/08/@supports_named-feature.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/@supports_named-feature.png" alt="" width="560" height="332" class="alignnone size-medium wp-image-36614" /></a></p>
<div class="intro">
<p>In CSS you can feature detect support for things like selectors, properties + values, and <a href="https://brm.us/at-rule">at-rules</a> using <code>@supports</code>. But how do you feature detect a change to an underlying implementation, or two existing properties suddenly working together? Enter <code>@supports named-feature()</code>, a function designed to expose these specific capabilities without relying on hacky workarounds.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The OG use-case: “Flexbox Gap”</h3>
<p>Back in the day <em>(and by back in the day, I mean 2020 😅)</em>, I wrote about <a href="https://www.bram.us/2020/05/12/spacing-grid-flexbox-items-in-css-with-the-gap-property/">the <code>gap</code> property for Flex layouts landing in Chromium</a>. The property already worked with Grid Layouts, and the post celebrated the property also starting to have an effect on Flex layouts.</p>
<p>Historically, this <code>gap</code> property <a href="https://lists.w3.org/Archives/Public/www-style/2015Jun/0221.html">debuted in 2015</a> as <code>grid-gap</code> <em>(as a shorthand for <code>grid-row-gap</code> and <code>grid-column-gap</code>)</em>, and worked exclusively for CSS Grid Layout. Later on, in 2017, driven by author demand, the CSS Working Group <a href="https://github.com/w3c/csswg-drafts/issues/1036#issuecomment-320241018">decided</a> that spacing out items was useful across all layout methods. So they dropped the <code>grid-</code> prefix and created the shorthand <code>gap</code> property (and its longhands <code>row-gap</code> and <code>column-gap</code>) that applies to both Grid and Flex layouts.</p>
<pre><code class="language-diff">- grid-gap: 2em;
+ gap: 2em;</code></pre>
<p>Browsers followed suit, and started treating <code>grid-gap</code> as an alias for the newly created <code>gap</code> property. But — BUT! — that didn’t mean that <code>gap</code> also immediately worked with Flex layouts in browsers. While aliasing <code>grid-gap</code> to <code>gap</code> was quick and easy, there was a massive <em>gap</em> (pun intended 🥁) between it being specced to work with Flex layout and a browser actually supporting “Flexbox gap”.</p>
<p>During this limbo, there was no straightforward way to detect whether the browser supported “Flexbox gap” or not. You couldn&#8217;t do <code>@supports (gap: 1em)</code> because the browser would truthfully say <em>“Yes! I support that!”</em> (for Grid). You also couldn’t do <code>@supports (display: flex)</code>, as the browser also said yes. And combining both — through <code>@supports (display: flex) and (gap: 1em)</code> — also didn’t work because the browser only checks whether it can <em>parse</em> the passed in <code>&lt;suppports-condition&gt;</code>s, not whether they have a combined effect.</p>
<pre><code class="language-css">/* This only checks if CSS can parse these declarations, not if they work together */
@supports (display: flex) and (gap: 1em) {
  …
}</code></pre>
<p>So right after Chromium shipped “Flexbox Gap” in 2020, I filed <a href="https://github.com/w3c/csswg-drafts/issues/5062">w3c/csswg-drafts#5062</a> at the CSS Working Group asking how to properly feature detect <code>gap</code> also playing nice with Flexbox.</p>
<p>Turns out this was already on their radar, and the request got duped into the more generically worded <a href="https://github.com/w3c/csswg-drafts/issues/3559">w3c/csswg-drafts#3559</a> from 2019, which was about <em>“testing support of properties and values with partial implementations”</em>.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Enter <code>named-feature()</code></h3>
<p>The discussion in <a href="https://github.com/w3c/csswg-drafts/issues/3559">w3c/csswg-drafts#3559</a> eventually turned silent but at the end of 2023 <a href="https://github.com/w3c/csswg-drafts/issues/3559#issuecomment-1868151989">it became very relevant again</a> as browsers were about to support <code>align-content</code> having an effect on <code>display: block</code> containers.</p>
<p>Two months later, the Working Group <a href="https://github.com/w3c/csswg-drafts/issues/3559#issuecomment-1885332213">accepted</a> a <a href="https://github.com/w3c/csswg-drafts/issues/3559#issuecomment-1868169502">proposal by David Baron</a> to solve this. I really like David’s original pitch to solving this problem:</p>
<blockquote>
<p>I think it might be reasonable to add specific, one-off solutions when we think they&#8217;re important enough. They&#8217;d need to be for significant enough features that we&#8217;d be willing to add an extra keyword to CSS for them, and that an implementor wouldn&#8217;t miss that it needed to be implemented, and that we&#8217;d be willing to write specific web-platform-tests to verify and monitor the results closely to make sure the rollout doesn&#8217;t go wrong. And we could probably choose reasonably verbose keywords.</p>
</blockquote>
<p><mark>The result is <code>named-feature()</code>, which is a function that allows you to feature-detect very specific behaviors or behavior changes that cannot be caught using the traditional <code>@supports</code>-checks. The function require a keyword as its argument, and the list of allowed keywords is <a href="https://drafts.csswg.org/css-conditional-5/#dfn-support-named-feature">predefined in the spec</a>.</mark></p>
<pre><code class="language-css">@supports named-feature(some-specific-behavior) {
  /* CSS for browsers that support 'some-specific-behavior' here … */
}</code></pre>
<p>Keywords are only added for the handful of tricky edge cases where feature detection is highly requested but impossible to do using other mechanisms in CSS.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Real-World Use Cases</h3>
<p>Currently the CSS Working Group has specified two keywords for <code>named-feature()</code>. I&#8217;m quite happy about these, as I proposed both of them at the Working Group 🙂</p>
<p style="text-align: center; font-size: 14; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>1. Transform-Aware Anchor Positioning</h4>
<p>CSS Anchor Positioning initially shipped without taking transforms into account. Because author demand was huge, things changed and the spec got updated to support this use-case. The change <a href="https://www.bram.us/2025/11/20/anchor-positioning-is-transform-aware-in-chrome-144/">shipped in Chrome 144</a>, is <a href="https://developer.apple.com/documentation/safari-release-notes/safari-27-release-notes#:~:text=Added%20support%20for%20transform%2Daware%20anchor%20positioning.%20(175401339)">included in the upcoming Safari 27</a>, and is <a href="https://groups.google.com/a/mozilla.org/g/dev-platform/c/ESPZgAnSDss/m/0cf-kcITEAAJ?e=48417069" target="_blank" rel="noopener">currently being prototyped in Firefox</a>.</p>
<p>While you could already easily feature detect Anchor Positioning itself, you couldn&#8217;t detect if the browser respects transforms on the anchor. To solve this, you can now use the keyword that <a href="https://github.com/w3c/csswg-drafts/issues/13678">was resolved on in w3c/csswg-drafts#13678</a>: <code>anchor-position-follows-transforms</code>.</p>
<pre><code class="language-css">@supports named-feature(anchor-position-follows-transforms) {
  /* ✅ The browser supports transform-aware anchor positioning! */
}</code></pre>
<p>While not originally slated for their 27.0 release — the release that includes Transform-Aware Anchor Positioning — I reached out to the Safari team to add support for <code>named-feature(anchor-position-follows-transforms)</code>, and <a href="https://github.com/WebKit/WebKit/pull/70571">the PR got merged earlier this month</a>. Firefox is currently prototyping Transform-Aware Anchor Positioning, and they <a href="https://groups.google.com/a/mozilla.org/g/dev-platform/c/2Xn5rT6tZBc/m/U-9oQhIUEAAJ">announced</a> that they will ship <code>named-feature()</code> simultaneously.</p>
<p style="text-align: center; font-size: 14; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>2. Single-Axis Scroll Containers</h4>
<p>Another feature that needs more advanced feature detection is the recent change that allows containers to be a scroller for only a single axis. This fixes a longstanding issue where <code>position: sticky</code> on one axis gets trapped by an unrelated scroller on the other axis, as detailed in my post on <a href="https://www.bram.us/2026/03/30/css-sticky-per-axis/">CSS sticky per axis</a>.</p>
<p>While you can feature detect Single-Axis Scroll Containers by <a href="https://github.com/w3c/csswg-drafts/issues/13677#issuecomment-4118565796">throwing lots of JavaScript at the problem</a>, there was no way to do this in CSS. But thanks to <a href="https://github.com/w3c/csswg-drafts/issues/13677">my proposal in w3c/csswg-drafts#13677</a>, we now have the <code>single-axis-scroll-container</code> keyword for use with <code>@supports named-feature(single-axis-scroll-container)</code>:</p>
<pre><code class="language-css">@supports named-feature(single-axis-scroll-container) {
  /* ✅ Single-axis scroll containers are supported, so sticky per axis just works! */
}</code></pre>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#browser-support" name="browser-support">#</a> Browser Support</h3>
<div class="note note--info">
<p>💡 Although this post was originally published in August 2026, the list below is constantly being updated. <em>Last update: August 27, 2026</em>.</p>
</div>
<p>Support for <code>named-feature()</code> looks as follows:</p>
<dl class="grid">
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>✅ Supported in Chrome 150</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>☑️ Supported in Firefox 156 (soon in beta)</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>☑️ Supported in Safari Technology Preview</p>
</dd>
</dl>
<p>What is more interesting though, is support for the various keywords.</p>
<p>Support for <code>anchor-position-follows-transforms</code>:</p>
<dl class="grid">
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>✅ Supported in Chrome 150</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>☑️ Supported in Firefox 156 (soon in beta)</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>☑️ Supported in Safari Technology Preview</p>
</dd>
</dl>
<p>Support for <code>single-axis-scroll-container</code>:</p>
<dl class="grid">
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>✅ Supported in Chrome 153</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>❌ No Support. There is no bug tracking this yet.</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>❌ No Support. There is no bug tracking this yet.</p>
</dd>
</dl>
<div class="note">
<p>One unfortunate thing to note here that you&#8217;ll get false negatives for <code>anchor-follows-transforms</code> Chrome 144–150 and Safari 27 as there was a gap between shipping the feature itself and the feature detection part with <code>named-feature()</code>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>What about other features?</h3>
<p>If you are wondering about the older features I mentioned here, and why they don’t have a keyword, here goes:</p>
<dl>
<dt>CSS <code>gap</code> with <code>display: flex</code></dt>
<dd>All browser support this by now. Too much time has passed since it shipped and this feature detection through <code>named-feature()</code> becoming available.</dd>
<dt><code>align-content</code> having an effect on <code>display: block</code></dt>
<dd>Chrome did it’s homework and did <em>(internal)</em> testing against the top X websites. The only notable breakage that came out of that was Netflix being broken <em>(they had <code>align-content</code> on an element that switched from <code>block</code> to <code>flex</code> through a media query)</em> but we got them to fix their CSS.</dd>
</dl>
<p>And then there’s also these features that are currently hard to detect:</p>
<dl>
<dt>Style Queries</dt>
<dd>
<dd>
<p>I have filed <a href="https://github.com/w3c/csswg-drafts/issues/13975">w3c/csswg-drafts#13975</a> for it a while ago. The tricky thing with it though, is that a lot of time has passed since Chrome first shipped it in March 2023 and Safari in September 2024 … so shipping a check now would yield a lot of false negatives. Additionally, there is <a href="https://www.bram.us/2024/10/06/feature-detect-style-queries-support-in-css/">a documented workaround to feature detect style queries</a>.</p>
</dd>
</dl>
<p>For completeness: Other at-rules — such as <code>@property</code> — can be feature detected using <a href="https://brm.us/at-rule"><code>at-rule()</code></a>.</p>
<div class="note">
<p>Even with <code>at-rule()</code> and <code>named-feature()</code> now existing, some things are still not covered by. For example, you can’t use those to feature detect support for <code>scrolled</code> scroll-state queries <em>(see <a href="https://brm.us/hidey-bar-2">The Hidey Bar</a>)</em>. While one could argue to add an extra keyword to <code>named-feature()</code>, the thing that will solve this are <a href="https://github.com/w3c/csswg-drafts/issues/12622">custom supports conditions (<code>@supports-condition</code>)</a>. At the time of writing, that feature only has a WG resolution though.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3 id="spread-the-word">Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3mu3skq6dak2c">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117169627581544218">🦣 Mastodon</a></li>
<li><a href="https://lnkd.in/p/ejJRP2YH">💼 LinkedIn</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/08/27/feature-detecting-undetectable-css-features-with-supports-named-feature/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>The Future of CSS: Target Multiple Classes with the Class Prefix Selector</title>
		<link>https://www.bram.us/2026/08/20/the-future-of-css-target-multiple-classes-with-the-class-prefix-selector/</link>
					<comments>https://www.bram.us/2026/08/20/the-future-of-css-target-multiple-classes-with-the-class-prefix-selector/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Wed, 19 Aug 2026 22:35:47 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[selectors]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36541</guid>

					<description><![CDATA[<p>To target multiple classes that share the same prefix, you'd typically have to resort to adding an extra base classes to your markup or to using badly performing attribute selectors. To make things easier, CSS is getting a new selector: The Class Prefix Selector (<code>.prefix-*</code>).</p>]]></description>
										<content:encoded><![CDATA[<p><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/class-prefix-selector.png" alt="" width="560" height="372" class="alignnone size-medium wp-image-36553" /></a></p>
<div class="intro">
<p>To target multiple classes that share the same prefix, you&#8217;d typically have to resort to adding an extra base classes to your markup or to using badly performing attribute selectors. To make things easier, CSS is getting a new selector: The Class Prefix Selector (<code>.prefix-*</code>).</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note note--warning">
<p><strong>⚠️ This post is about an upcoming CSS feature. You can’t use it … yet.</strong></p>
<p>This feature is hot off the press — <a href="https://github.com/w3c/csswg-drafts/issues/10001#issuecomment-5204871059">it was resolved on only two weeks ago</a> — and currently only exists in <a href="https://drafts.csswg.org/selectors-5/#class-prefix">spec text</a>. The spec will most likely see some changes before this is ready for a browser to implement.</p>
<p>If you have any feedback on the shape of this in-development feature, leave feedback below or at the CSS Working Group in <a href="https://github.com/w3c/csswg-drafts/issues/10001">w3c/csswg-drafts#10001</a></div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3 id="the-problem">The Problem: Targeting Multiple Prefixed Classes</h3>
<p>When coming up with classnames for use in the <code>class</code> attribute, a common practice is to use a prefix to retain some grouping or hierarchy. You might be familiar with classes like <code>.btn-primary</code>, <code>.btn-secondary</code>, <code>.btn-danger</code>, and so on.</p>
<p>To apply a base style to all of these buttons today, you typically have to list them all out, or introduce a separate <code>.btn</code> base class:</p>
<pre><code class="language-css">/* Adding a base class */
.btn {
  padding: 0.5rem 1rem;
  border-radius: 4px;
}

/* Or listing everything... yuck! */
.btn-primary,
.btn-secondary,
.btn-danger {
  padding: 0.5rem 1rem;
  border-radius: 4px;
}</code></pre>
<p>Some of you even resort to substring-matching attribute selectors, but those selectors perform badly:</p>
<pre><code class="language-css">/* Works, but performs badly */
[class^="btn-"],
[class*=" btn-"] {
  padding: 0.5rem 1rem;
}</code></pre>
<p>Looking at <a href="https://gist.github.com/bramus/1de3bc824ea3d9b47540b023dc165723">a benchmark I ran</a> using <a href="https://github.com/GoogleChromeLabs/css-selector-benchmark"><code>css-selector-benchmark</code></a>, a regular class selector ran at more than 6000 runs/s, whereas <code>[class*=" btn-"]</code> dipped as low as 328 runs/s … that’s almost 20 times slower and also 3ms out of your frame budget just to match an element!</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3 id="the-solution">The Solution: The Class Prefix Selector</h3>
<p>Just two weeks ago, at the CSS Working Group F2F meeting in Berlin (August 2026), we resolved to add a dedicated <strong>Class Prefix Selector</strong> to the CSS Selectors Level 5 specification. The idea was originally pitched by <a href="http://lea.verou.me/">Lea Verou</a> back in 2024 <em>(<a href="https://github.com/w3c/csswg-drafts/issues/10001">w3c/csswg-drafts/#10001</a>)</em>, and also championed by Lea (and <a href="https://tabatkins.com/">Tab Atkins—Bittner</a>) at the F2F.</p>
<p>The syntax is incredibly straightforward:</p>
<pre><code class="language-css">.btn-* {
  padding: 0.5rem 1rem;
  border-radius: 4px;
}</code></pre>
<p>That’s it! The <code>-*</code> part at the end makes the selector a <strong>Class Prefix Selector</strong> and will try to  match any class that begins with that hyphen-separated prefix.</p>
<p>It’s a huge win for utility classes and design systems, allowing you to easily target groups of related elements without having to bloat your HTML payload or write fragile attribute selectors.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3 id="empty-string">What about the empty string?</h3>
<p>An interesting question that popped up during the discussions is whether <code>.foo-*</code> should match the empty string <em>(<a href="https://github.com/w3c/csswg-drafts/issues/14291">w3c/csswg-drafts/#14291</a>)</em>, meaning: should <code>.foo-*</code> also match an element that <em>merely</em> has the <code>.foo-</code> class?</p>
<p>While the exact default behavior is still being ironed out, currently the selector is specified to only match classes that start with the prefix and that have at least one character beyond the prefix <em>(and the first such character beyond the prefix is not also a hyphen)</em></p>
<p>So no, <code>class="foo-"</code> would NOT be matched by <code>.foo-*</code>, which I think is fine. That same selector also would not match <code>class="foo--"</code>, which is also probably fine.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3 id="non-dashes">What about non-dashes?</h3>
<p>The Class Prefix Selector is currently limited to hyphen-separated prefixes, at least at first. Other separators, like <code>_</code>, might be added as possibilities in the future as we receive request from authors like yourself about what would be needed.</p>
<p>One thing that is already quite clear right now, is that there must at least be <em>some</em> separator. Arbitrary prefixes <em>(like <code>.foo*</code>)</em> are not going to be allowed for at least two reasons:</p>
<ol>
<li>You could accidentally overselect: <code>.foo*</code> would also match <code>.footer</code></li>
<li>Selector Performance: Browsers typically create buckets for class selectors for quick selector matching. Adding arbitrary wildcards defeat that optimization entirely. With the <code>-</code> as a separator, the browser can already create extra buckets when parsing the HTML, long before CSS ever gets parsed and starts matching.</li>
</ol>
<p>Similarly, wildcards in the middle of a selector (such as <code>.card-*-primary</code>) are also not going to be allowed.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3 id="why-not-the-pipe">Why not reuse the <code>|=</code> selector?</h3>
<div class="note">
<p>This section was added on 2026.08.21, after Brian <a href="https://bsky.app/profile/bkardell.com/post/3mtjenjw7s22p">asked</a> about this</p>
</div>
<p>In the issue thread and during the CSSWG call, the idea of reusing <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Attribute_selectors#attrvalue_3">the existing &#8220;dash match&#8221; attribute selector (<code>|=</code>)</a> was brought up. Instead of introducing the new <code>.foo-*</code> syntax, what if we just wrote <code>[class|="foo"]</code>?</p>
<p>I also initially thought this would be a good idea, but the working group ultimately decided against it for valid reasons.</p>
<p>First of all, there are a few issues with the <code>|=</code> selector that prevent it from being a drop-in solution for wildcard class matching:</p>
<ul>
<li>The <code>|=</code> operator was originally created for language attributes (like <code>[lang|="en"]</code>). By design, it matches the hyphenated prefix (like <code>en-us</code>) <em>but also the exact value</em> (<code>en</code>). As argued on the call, this is a massive footgun for utility classes: if you want to target all <code>.bi-*</code> icons, you probably don&#8217;t want those styles accidentally leaking to a standalone <code>.bi</code> base class.</li>
<li>To make the <code>|=</code> selector work for class matching, its behavior would need to change. The way the selector currently works is by only checking values from the <em>very beginning</em> of an attribute&#8217;s string. If you have an element with multiple classes like <code>&lt;div class="card btn-primary"&gt;</code>, the selector <code>[class|="btn"]</code> would completely fail to match it because <code>btn-primary</code> isn&#8217;t at the start of the <code>class</code> attribute.</li>
<li>Selector Performance <em>(see previous mentions about this)</em>.</li>
</ul>
<p>The most important reason, though, is The Bigger Picture™ for wildcards in CSS: there is an ongoing, larger effort within the CSSWG (also championed by Lea) to <a href="https://github.com/w3c/csswg-drafts/issues/14224">standardize wildcards across all of CSS</a></p>
<p>By <a href="https://github.com/w3c/csswg-drafts/issues/14224#issuecomment-5177254686">choosing <code>-*</code> as the syntax for prefixes</a>, the syntax can later be reused for future extensions such as wildcard attribute <em>names</em> (e.g. <code>[data-*]</code>) and wildcard (custom) element names (e.g. <code>custom-framework-*</code>).</p>
<p>Because the <code>|=</code> selector only works for attribute <em>values</em>, it is effectively a dead end for those other use cases.</p>
<div class="note">
<p>For completeness: Right after we discussed the issue at the CSS WG, <a href="https://github.com/w3c/csswg-drafts/issues/14289">w3c/csswg-drafts#14289</a> was filed to explore if we can relax <code>|=</code> so that it fits the bill. <em>If</em> that can be done, it would not tick the The Bigger Picture™ box, though.</p>
</div>
<hr>
<h3><a href="#browser-support" name="browser-support">#</a> Browser Support</h3>
<div class="note note--info">
<p>💡 Although this post was originally published in August 2026, the list below is constantly being updated. <em>Last update: August 20, 2026</em>.</p>
</div>
<p>Since this was literally just resolved at the CSSWG F2F in Berlin two weeks ago, browser support is currently non-existent. To follow along with the progress – if any – you can follow these browser issues:</p>
<dl class="grid">
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>❌ No Support</p>
<p>Subscribe to <a href="https://crbug.com/550093337">CrBug #550093337</a> to follow along.</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>❌ No Support</p>
<p>There is no bug tracking this yet.</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>❌ No Support</p>
<p>There is no bug tracking this yet.</p>
</dd>
</dl>
<p>This feature is still in its early days and needs to be fleshed out further, so could be that it takes a few more years before you can use it in production …</p>
<hr>
<h3><a href="#feature-detection" name="feature-detection">#</a> Feature Detection</h3>
<p>You can feature detect support with a regular <code>@supports</code> rule:</p>
<pre><code class="language-css">@supports selector(.foo-*) {
  /* Browser has support */
}</code></pre>
<p>The following CodePen uses this and will light green when you browser supports it:</p>
<p class="codepen" data-height="520" data-pen-title="CSS Class Prefix Selector Support test" data-default-tab="result" data-slug-hash="qERerxM" data-user="bramus" style="height: 520px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/bramus/pen/qERerxM"><br />
  CSS Class Prefix Selector Support test</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>)<br />
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><script async src="https://public.codepenassets.com/embed/index.js"></script></p>
<hr>
<h3 id="spread-the-word">Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3mthseyexik2r">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117124577120728021">🦣 Mastodon</a></li>
<li><a href="https://lnkd.in/p/eGvCJhXH">💼 LinkedIn</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/08/20/the-future-of-css-target-multiple-classes-with-the-class-prefix-selector/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title>The Case for Tri-State Dark Mode Toggles</title>
		<link>https://www.bram.us/2026/08/18/the-case-for-tri-state-dark-mode-toggles/</link>
					<comments>https://www.bram.us/2026/08/18/the-case-for-tri-state-dark-mode-toggles/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Tue, 18 Aug 2026 08:26:19 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[dark mode]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36447</guid>

					<description><![CDATA[<p>Adding a Dark Mode toggle to your website is a nice touch that many users appreciate. When implementing one, the default instinct for many developers is to reach for a simple binary switch: Light on one side, Dark on the other.</p>

<p>While a two-state control might seem like the most straightforward solution, I firmly believe it’s flawed from a user experience perspective. Instead, we should all be using tri-state controls. Let me explain why.</p>]]></description>
										<content:encoded><![CDATA[<p><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/tri-state-dark-mode.png" alt="" width="560" height="353" class="alignnone size-medium wp-image-36528" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/08/tri-state-dark-mode.png 1844w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/tri-state-dark-mode-560x353.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/tri-state-dark-mode-1120x707.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/tri-state-dark-mode-768x485.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/tri-state-dark-mode-1536x970.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/tri-state-dark-mode-1568x990.png 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /></p>
<div class="intro">
<p>Adding a Dark Mode toggle to your website is a nice touch that many users appreciate. When implementing one, the default instinct for many developers is to reach for a simple binary switch: Light on one side, Dark on the other.</p>
<p>While a two-state control might seem like the most straightforward solution, I firmly believe it’s flawed from a user experience perspective. Instead, we should all be using tri-state controls. Let me explain why.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>A raised eyebrow</h3>
<p>The whole thing started about two weeks ago. While browsing through <a href="https://developer.chrome.com/docs/modern-web-guidance">Modern Web Guidance</a>’s guides, I noticed that <a href="https://github.com/GoogleChrome/modern-web-guidance-src/blob/main/guides/visual-design/dark-mode/guide.md">the dark mode guide</a> advises authors to use a two-state control when offering a Dark Mode toggle to their users.</p>
<blockquote>
<p>Use a two-state control</p>
</blockquote>
<p>This had me raise my eyebrow, as I strongly believe that these type of controls <em>should</em> be a tri-state control.</p>
<p>Here’s what I wrote back in 2020 when I <a href="https://www.bram.us/2020/04/26/the-quest-for-the-perfect-dark-mode-using-vanilla-javascript/">recreated a Dark Mode toggle by Josh W. Comeau using Vanilla JavaScript</a>:</p>
<blockquote>
<p>🤔 One thing that I personally find missing from Joshua’s implementation is a way to reset the override. To not diverge too much from his implementation I’ve added the reset option via a separate link. <mark>What ideally could be done is to use radio buttons (instead of a checkbox) or a dropdown to change between auto, light, and dark.</mark></p>
</blockquote>
<p>So, I went on and quickly filed <a href="https://github.com/GoogleChrome/modern-web-guidance-src/issues/1182">GoogleChrome/modern-web-guidance-src#1182</a> to get the MWG guide fixed. I didn’t add too much reasoning, as I (wrongly) assumed this was a well lit path. A bit later, <a href="https://github.com/GoogleChrome/modern-web-guidance-src/pull/830">guide author</a> <a href="https://lea.verou.me/">Lea Verou</a> replied, pointing to the UX motivation part of the guide, which reads:</p>
<blockquote>
<p>DON&#8217;T expose all three states (system, light, dark). While the rationale is plausible — &#8220;Follow system (currently dark)&#8221; is a distinct user intent from &#8220;Always dark&#8221; — it provides suboptimal UX:</p>
<ul>
<li>Users cannot meaningfully express intent for problems they don&#8217;t currently have. A manual toggle is a temporary comfort adjustment (&#8220;it&#8217;s too bright right now&#8221;), not a long-term preference (&#8220;make sure this never changes&#8221;).</li>
<li>Two of the three options always produce the same visual result, violating the principle of feedback.</li>
</ul>
</blockquote>
<p>I’m not really convinced of that argumentation, because:</p>
<ul>
<li><q>A manual toggle is a temporary comfort adjustment</q><br />→ Why shouldn’t it be a long term thing? The chosen option is persisted after all …</li>
<li><q>Two of the three options always produce the same visual result, violating the principle of feedback.</q><br />→ While the whole page might not change indeed, the selected option does change so there is <em>local</em> visual feedback.</li>
</ul>
<p>Furthermore, solely focusing on the feedback part is ignoring other benefits of the tri-state control, including predictability and clarity — <a href="https://en.wikipedia.org/wiki/Don%27t_Make_Me_Think">“Don&#8217;t Make Me Think”</a></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The F2F, The Debate, and The Data</h3>
<p>Coincidentally, the week after I filed the issue, the CSS Working Group Face-To-Face (F2F) took place in Berlin, where both Lea and I were present.</p>
<p>We spent some time trying to convince each other of our respective views, but quickly realized we couldn’t convince each other and also needed some numbers to back up our claims. Lacking some broader research about this topic, we both turned to social media to ask folks for their input.</p>
<p>I posted the following on <a href="https://bsky.app/profile/bram.us/post/3msaprh5l622k">Bsky</a> and <a href="https://front-end.social/@bramus/117036573689092920">Mastodon</a>:</p>
<blockquote>
<p>QQ about Dark Mode Toggles on websites</p>
<p>How many options do you show to users?</p>
<ul>
<li>Three values: System, Light, or Dark</li>
<li>Two values: Light, Dark (with one of them mapping to “System” behind the scenes).</li>
</ul>
<p>Why the one or the other?</p>
<p>(I have an opinion about it, but I’m looking for external input)</p>
</blockquote>
<p>Tallying up the results after about 24 hours, the answers were overwhelmingly in favor of a tri-state approach, with some of the replies even including some good arguments for their choice as well:</p>
<ul>
<li><strong>Tri-state:</strong> 43</li>
<li><strong>Two-state:</strong> 7</li>
</ul>
<p><em>(Note: 4 folks also answered that they don’t offer a control at all, and strictly follow the system preference — to me, that is the minimum you should do)</em></p>
<p>Simultaneously, Lea also <a href="https://github.com/LeaVerou/blog/discussions/141">ran a poll</a>, asking if folks <q>ever clicked on the override that matches their OS</q>. While it’s good to try and get data, <a href="https://github.com/LeaVerou/blog/discussions/141#discussioncomment-17894134">I found the question to be incomplete</a>.</p>
<p>A bit later, Lea whipped up a case for a two-state control which she published on her blog: <a href="https://lea.verou.me/blog/2026/dark-mode-toggles/">Dark mode toggles: two states are enough</a> <em>(you should read the whole thing)</em>.</p>
<p>However, after reading it, I still disagree. I firmly believe a tri-state control is the way to go.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The Problem with Two-State Controls</h3>
<p><mark>In short, I don’t believe that being able to force a site into a specific mode should be dependent on the time of the day.</mark></p>
<p>The two-state approach highlighted in Lea&#8217;s post urges you to either store <code>system</code> or <em>“the computed opposite of system”</em> (i.e. <code>light</code> or <code>dark</code> — whichever is the opposite of the OS is) when using the control. If you carefully think about this, you’ll quickly realize that this runs into a very specific UX problem when the user&#8217;s OS is set to auto-change from light to dark based on the time of the day.</p>
<p>Consider this scenario:</p>
<ol>
<li>The user’s OS is set to auto-switch Light/Dark based on the time of day.</li>
<li>They visit a website during the day, so they see the <strong>Light</strong> theme.</li>
<li>Using the site&#8217;s two-state control, they try out the <strong>Dark</strong> theme. They decide they don&#8217;t like it, so they revert it back to <strong>Light</strong>.</li>
<li>They visit the site again at night.</li>
</ol>
<p>Because the two-state toggle maps one of its values back to &#8220;System&#8221; behind the scenes, the site is now <code>dark</code>, although they <em>explicitly</em> chose <code>light</code> the last time they interacted with the control. You may now expect an incoming bug report for the control not properly working …</p>
<div class="note" style="font-size: 90%" id="not-buying-it">
<h4>“Yeah. I’m not buying it.” <sup>(*)</sup></h4>
<p>Lea has a dedicated section on this confusion in her post: <a href="https://lea.verou.me/blog/2026/dark-mode-toggles/#but-what-if-users-get-confused%3F">“What if the user gets confused?”</a>, but I am not convinced of her argumentation:</p>
<ul>
<li><q>This control is entirely tangential to the actual user goal for visiting the website</q> → When a user wants to change the theme (for whatever reason), their goal temporarily changes, and they should have a clear control to do so in any case.</li>
<li><q>A tri-state control introduces permanent UI complexity</q> → There is nothing complex about a tri-state control, as it — unlike a two-state control — clearly communicates what each button does.</li>
<li><q>Additionally, color appearance is not just a pure function of color …</q> → That seems irrelevant when it comes to making the case for a two-state or tri-state control.</li>
<li><q>The title and icon could make the state clearer</q> → With that I do agree (see further down) but the post is not pushing for that solution.</li>
<li><q>Even if there is an ingenious UI that exposes three states</q> → There is nothing ingenious about showing three buttons for three states. It’s the Dumbest Approach™ one could possibly take here.</li>
</ul>
<p>Furthermore Lea mentions that a tri-state control has its place in a separate settings panel. However, many — if not most — sites don’t even have such a settings panel so that seems a lot ot require from a webdeveloper …</p>
<p><small>(* That’s <a href="https://www.youtube.com/watch?v=qHAAmdskBqU">a Scrubs Reference</a> there 😀)</small></p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Doing Two-State Right <em>(with caveats)</em></h3>
<p>Now, to be clear, I am not <em>entirely</em> against two-state controls. They <em>can</em> work, as long as they clearly communicate to the user what is actually going on.</p>
<p>If you <em>really</em> want to offer a two-state control, here are a couple of ways to do it:</p>
<h4>1. Explicitly label the <code>system</code> state</h4>
<p>Include a label like <em>“auto”</em> (or an equivalent icon) on the value that follows the OS. This still carries the problem that the user can’t fixate their current OS preference, but at least the behavior is clearly communicated.</p>
<p>I actually took this approach in my <a href="https://brm.us/chrome-dark-mode-toggle">chrome-dark-mode-toggle</a> POC extension, which adds a Dark Mode toggle button to Chrome. It has one icon to click which toggles between <em>System</em> and <em>Computed Opposite of System</em>. Crucially, there is a clear &#8220;Auto&#8221; label present on the control so you know when it&#8217;s deferring to the OS.</p>
<h4>2. Write explicit values to storage</h4>
<p>Opposite to Lea’s recommendation, write <em>the actual values</em> (an explicit <code>light</code> or <code>dark</code>, never <code>system</code>) to storage so that you can actually force both states persistently.</p>
<p>However, if you do this, you inevitably need to offer a &#8220;reset&#8221; option so users can go back to following the system … at which point you might as well just make the control a tri-state one.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The Sweet Spot?</h3>
<p>Another approach I’ve seen recently is <a href="https://vale.rocks/micros/20260810-0330">Vale’s take on things</a>, which shows two controls while still offering three states.</p>
<p>By default, when the site is following the OS preference, <em>neither</em> of the options are highlighted. This implicitly acts as the “System” state. When a user explicitly clicks Light or Dark, the UI highlights the choice, representing the two extra states that write explicit values to storage. When clicking the chosen value again, you end up back with no option selected.</p>
<p class="codepen" data-height="400" data-pen-title="Light/Dark/System Theme Setting" data-version="2" data-default-tab="result" data-slug-hash="emgXqvQ" data-user="OuterVale" style="height: 400px; 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/editor/OuterVale/pen/019fe98c-2d9b-71e4-849d-726543589635"><br />
  Light/Dark/System Theme Setting</a> by Declan Chidlow (<a href="https://codepen.io/OuterVale">@OuterVale</a>)<br />
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><script async src="https://public.codepenassets.com/embed/index.js"></script></p>
<p>I really like Declan’s approach. It&#8217;s clean, and doesn&#8217;t sacrifice functionality.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Clarity over Brevity</h3>
<p>When building these types of control, I think that one of the main goals here should be to keep things predictable for users. Once a user has interacted with a control, they have mentally set it to the specific state they have chosen. Things then changing based on the time of day are an unexpected surprise.</p>
<p>Three explicit options (System, Light, Dark) for a Dark Mode Toggle (or Vale&#8217;s clever UI trick) don’t come with this type of unexpected surprise. As Erik Kroes perfectly put it in <a href="https://mastodon.social/@erikKroes/117049971814620910">his reply on Mastodon</a>:</p>
<blockquote>
<p>&#8220;Tri-state&#8221; prioritizes clarity over brevity.</p>
</blockquote>
<p>And when it comes to user interfaces, clarity is exactly what users want most 🙂</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/08/18/the-case-for-tri-state-dark-mode-toggles/feed/</wfw:commentRss>
			<slash:comments>8</slash:comments>
		
		
			</item>
		<item>
		<title>Unlock Immediate Diagonal Scrolling with CSS scroll-axis-lock: none</title>
		<link>https://www.bram.us/2026/08/09/unlock-diagonal-scrolling-with-css-scroll-axis-lock-none/</link>
					<comments>https://www.bram.us/2026/08/09/unlock-diagonal-scrolling-with-css-scroll-axis-lock-none/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Sun, 09 Aug 2026 08:16:50 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[scroll]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36440</guid>

					<description><![CDATA[<p>When you scroll a 2D scroller on a website, the browser does a lot of work behind the scenes to keep you on track. One of the things it typically does is scroll axis-locking <em>(aka “railing”)</em>, ignoring some minor scroll deltas in the non-main scrolling axis. While helpful most of the times, this sometimes can get in the way such as in map or image zoom interfaces. With the new CSS <code>scroll-axis-lock</code> property you can disable the browser's default scroll axis-locking behavior, allowing users to immediately perform a diagonal scroll.</p>]]></description>
										<content:encoded><![CDATA[<figure><div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36440-4" width="640" height="522" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-comparison.mp4?_=4" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-comparison.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-comparison.mp4</a></video></div><figcaption>Recording of <a href="https://whole-fog-anteater.codepen.app/">the demo</a> comparing what the browser does with and without scroll railing.</figcaption></figure>
<div class="intro">
<p>When you scroll a 2D scroller on a website, the browser does a lot of work behind the scenes to keep you on track. One of the things it typically does is scroll axis-locking <em>(aka “railing”)</em>, ignoring some minor scroll deltas in the non-main scrolling axis. While helpful most of the times, this sometimes can get in the way such as in map or image zoom interfaces. With the new CSS <code>scroll-axis-lock</code> property you can disable the browser&#8217;s default scroll axis-locking behavior, allowing users to immediately perform a diagonal scroll.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Scroll Axis Locking?</h3>
<p>By default, a browser often &#8220;locks&#8221; a user&#8217;s scrolling gesture to a single axis when that gesture starts with significantly more movement in one axis than in the perpendicular axis.</p>
<p>For example, say a user drags their finger on the screen such that the element underneath the finger would be scrolled by 500px in the y-axis and 3px in the x-axis. Because the gesture is almost entirely vertical, the browser might interpret the user’s intent to be a perfectly vertical scroll. As such, the browser can ignore the 3px scroll delta and (possibly) all further x-axis deltas for the remainder of the gesture. In this case, the browser has &#8220;locked&#8221; the scroll to the y-axis — a technique also known as <em>“railing”</em>.</p>
<figure><div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36440-5" width="640" height="522" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-example.mp4?_=5" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-example.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-example.mp4</a></video></div><figcaption>Recording of <a href="https://whole-fog-anteater.codepen.app/">the demo</a> showing a vertical scroll performed on a trackpad. Even though the user input (the dashed red line) was not perfectly straight, the browser performed a perfect vertical scroll (solid blue line)</figcaption></figure>
<p>In many cases, this scroll-locking behavior improves the user&#8217;s experience by avoiding accidental scrolls along the other axis when the user&#8217;s intent was to scroll only one axis.</p>
<p>However, in cases where you wish an element to always be diagonally scrollable, this locking behavior forces the user to start their gesture at an angle that doesn&#8217;t trigger the locking, often not in alignment with the author&#8217;s intended user experience.</p>
<p>Thankfully, a recent change to CSS now offers control over this, namely the <code>scroll-axis-lock</code> property described in the <a href="https://drafts.csswg.org/css-overflow-5/#scroll-axis-locking">CSS Overflow 5 specification</a>.</p>
<div class="note">
<p>💁‍♂️ Note that <mark>this axis-locking behavior totally depends on the browser and platform</mark>. Safari and Firefox on macOS for example are very strict at it, while Chrome on macOS tries to more closely reflect your scroll’s intent <em>(but still applies <em>some</em> locking)</em></p>
<p>Compare these two screenshots of Safari and Chrome: Safari’s scroll axis-locking results in a more <a href="https://www.google.com/search?q=etch-a-sketch">etch-a-sketch</a>-like scroll when scrolling in the shape of a circle (or attempt thereof 😅).</p>
<div class="table">
<div>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-safari.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-safari.png" alt="" width="560" height="352" class="alignnone size-medium wp-image-36457" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-safari.png 1622w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-safari-560x352.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-safari-1120x704.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-safari-768x482.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-safari-1536x965.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-safari-1568x985.png 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a><figcaption>A circle-shaped scroll performed in Safari on macOS. The actual scroll (solid blue line) is much more angled the actual scroll input (dashed red line)</figcaption></figure>
</div>
<div>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-chrome.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-chrome-560x352.png" alt="" width="560" height="352" class="alignnone size-medium wp-image-36458" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-chrome-560x352.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-chrome-1120x705.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-chrome-768x483.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-chrome-1536x967.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-chrome-1568x987.png 1568w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-circle-chrome.png 1621w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a><figcaption>A circle-shaped scroll performed in Chrome on macOS. The scroll (solid blue line) resembles the scroll input (dashed red line), but is not 100% following the input.</figcaption></figure>
</div>
</div>
<p>As for touch-platforms: From what I can tell, Safari on iOS doesn’t scroll lock at all, whereas Chrome on Android does.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Control scroll axis-locking with <code>scroll-axis-lock</code></h3>
<p>The <code>scroll-axis-lock</code> property gives you control scroll axis-locking. The property accepts the following values:</p>
<ul>
<li><code>auto</code> <em>(default)</em>: The browser may lock the scroll to a single axis if it determines the gesture is predominantly 1-dimensional. The browser may allow you to break out of scroll axis lock as part of your current gesture, but the thresholds to do that are different per browser and platform.</li>
<li><code>none</code>: Disables the locking mechanism entirely. The scroll container will faithfully follow the user&#8217;s exact input, allowing for unrestricted diagonal panning.</li>
</ul>
<pre><code class="language-css">.scroller {
  scroll-axis-lock: auto; /* Default */
  /* or */
  scroll-axis-lock: none;
}</code></pre>
<p>In the following video, you can compare how both values behave. The recording show Chrome 153 on macOS, but with a different <code>scroll-axis-lock</code> value. The scroll is performed using a trackpad.</p>
<figure><div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36440-6" width="640" height="522" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-comparison.mp4?_=6" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-comparison.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-comparison.mp4</a></video></div><figcaption>Recording of <a href="https://whole-fog-anteater.codepen.app/">the demo</a> comparing what the browser does with and without scroll railing.</figcaption></figure>
<p>The difference also becomes very clear when doing scrolls that follow a circular motion:</p>
<div class="table">
<div>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axi-lock-auto.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axi-lock-auto-560x458.png" alt="" width="560" height="458" class="alignnone size-medium wp-image-36462" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axi-lock-auto-560x458.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axi-lock-auto-1120x915.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axi-lock-auto-768x628.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axi-lock-auto-1536x1255.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axi-lock-auto-1568x1281.png 1568w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axi-lock-auto.png 1625w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a><figcaption>A scroll performed in Chrome on macOS with <code>scroll-axis-lock: auto</code>. The scroll (solid blue line) resembles the scroll input (dashed red line), but is not 100% following the input.</figcaption></figure>
</div>
<div>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-none.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-none-560x457.png" alt="" width="560" height="457" class="alignnone size-medium wp-image-36463" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-none-560x457.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-none-1120x914.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-none-768x626.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-none-1536x1253.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-none-1568x1279.png 1568w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-none.png 1628w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a><figcaption>A scroll performed in Chrome on macOS with <code>scroll-axis-lock: none</code>. Because scroll axis-locking is disabled, the scroll (solid blue line) is identical to the scroll input (dashed red line).</figcaption></figure>
</div>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Try it yourself</h3>
<p>To feel the difference yourself <em>(and to create the screenshots and videos above)</em>, I built <a href="https://whole-fog-anteater.codepen.app/">a demo</a> that samples your pointer’s X/Y input deltas and compares them against the actual <code>scrollLeft</code> and <code>scrollTop</code> output of the container.</p>
<p><iframe src="https://whole-fog-anteater.codepen.app/" height="1100" frameborder="0" style="border: 1px solid;" data-resize>Check out the demo at <a href="https://whole-fog-anteater.codepen.app/">https://whole-fog-anteater.codepen.app/</a></iframe></p>
<p>If you try scrolling diagonally with <code>scroll-axis-lock: auto</code>, you&#8217;ll notice the status indicator flag can show a &#8220;Axis Locked&#8221; warning it detects that the browser is ignoring your perpendicular input. As mentioned before this totally depends on the browser and platform, so could be that you don’t see this warning at all.</p>
<p>If you set the toggle to <code>scroll-axis-lock: none</code>, and try again, scrolling straight is way more “jittery” and scrolling diagonally <em>immediately</em> pans in both directions simultaneously, exactly following your gesture.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#browser-support" name="browser-support">#</a> Browser Support</h3>
<div class="note note--info">
<p>💡 Although this post was originally published in August 2026, the list below is constantly being updated. <em>Last update: Aug 9, 2026</em>.</p>
</div>
<p>Support for <code>scroll-axis-lock</code> looks like this:</p>
<dl class="grid">
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>✅ Supported in Chromium 153</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>❌ No Support</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>❌ No Support</p>
</dd>
</dl>
<p><!-- 

<p>Subscribe to the bugs signal interest and stay up-to-date</p>

 --></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Feature Detection</h3>
<p>There is no real need to feature detect this, as you can safely use <code>scroll-axis-lock</code> today as a progressive enhancement. Since it doesn&#8217;t break anything when ignored, you can just declare it directly 🙂</p>
<p>If you really do need to feature detect it — perhaps to load some alternative code? — you can use an <code>@supports</code> rule in CSS or <code>CSS.supports()</code> in JS to check.</p>
<pre><code class="language-css">@supports (scroll-axis-lock: none) {
  /* Control over scroll-axis locking supported */
}</code></pre>
<pre><code class="language-js">if (CSS.supports('scroll-axis-lock: none')) {
  /* Control over scroll-axis locking supported */
}</code></pre>
<p>The following embed uses this feature detection to indicate whether your current browser supports <code>scroll-axis-lock</code>:</p>
<p class="codepen" data-height="500" data-pen-title="CSS scroll-axis-lock Support test" data-default-tab="result" data-slug-hash="vEgbwgr" data-user="bramus" style="height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/bramus/pen/vEgbwgr"><br />
  CSS scroll-axis-lock Support test</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>)<br />
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<p><script async src="https://public.codepenassets.com/embed/index.js"></script></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3msqqo6j7yc2b">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117072665955188819">🦣 Mastodon</a></li>
<li><a href="https://www.linkedin.com/posts/bramus_coming-soon-to-chrome-a-way-to-control-scroll-ugcPost-7492652230660231169-jCei/">💼 LinkedIn</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/08/09/unlock-diagonal-scrolling-with-css-scroll-axis-lock-none/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-example.mp4" length="417104" type="video/mp4" />
<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/08/scroll-axis-lock-comparison.mp4" length="514867" type="video/mp4" />

			</item>
		<item>
		<title>MDN Traffic Browser</title>
		<link>https://www.bram.us/2026/08/03/mdn-traffic-browser/</link>
					<comments>https://www.bram.us/2026/08/03/mdn-traffic-browser/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 13:39:30 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[dataviz]]></category>
		<category><![CDATA[mdn]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36426</guid>

					<description><![CDATA[<p>TIL: MDN exposes up-to-date page view data through <code>https://popularities.mdn.mozilla.net/current.csv</code>. Using Google AI Studio, I generated a visualizer to explore the data.</p>]]></description>
										<content:encoded><![CDATA[<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/08/ready-unit-fowl.codepen.app_.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/08/ready-unit-fowl.codepen.app_.png" alt="" width="560" height="294" class="alignnone size-medium wp-image-36427" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/08/ready-unit-fowl.codepen.app_.png 2400w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/ready-unit-fowl.codepen.app_-560x294.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/ready-unit-fowl.codepen.app_-1120x588.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/ready-unit-fowl.codepen.app_-768x403.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/ready-unit-fowl.codepen.app_-1536x806.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/ready-unit-fowl.codepen.app_-2048x1075.png 2048w, https://www.bram.us/wordpress/wp-content/uploads/2026/08/ready-unit-fowl.codepen.app_-1568x823.png 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a><figcaption>Screenshot of the visualizer, as <a href="https://ready-unit-fowl.codepen.app/">deployed onto CodePen</a></figcaption></figure>
<p>TIL: MDN exposes up-to-date page view data through <code>https://popularities.mdn.mozilla.net/current.csv</code>.</p>
<p>To visualize the data, I had <a href="https://aistudio.google.com/welcome">Google AI Studio</a> generate me a visualizer, which I deployed to CodePen</p>
<p>The data source file is downloaded by the app itself and stored in <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API">IndexedDB</a>, so this is a one-time static deploy (using <a href="https://codepen.io/2/whats-new">CodePen 2.0</a>). When your local data is considered old, you get a warning to re-download it.</p>
<p class="text-center"><a href="https://ready-unit-fowl.codepen.app/" class="button button-big">Check out “MDN Traffic Browser” &rarr;</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/08/03/mdn-traffic-browser/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
