<?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>Wed, 19 Aug 2026 23:02:44 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.7</generator>
	<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/#respond</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 brittle attribute selectors or add extra base classes to your markup. To make things easier, CSS is getting a new selector: The Class Prefix Selector (<code>.prefix-*</code>).</p>]]></description>
										<content:encoded><![CDATA[<p><img fetchpriority="high" 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 brittle attribute selectors or add extra base classes to your markup. 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>
</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 can be notoriously brittle and ugly when dealing with multiple classes on a single element:</p>
<pre><code class="language-css">/* Works, but can be error-prone with whitespace */
[class^="btn-"],
[class*=" btn-"] {
  padding: 0.5rem 1rem;
}</code></pre>
<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>.</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.</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>
<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/543356377">CrBug #543356377</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>
<p>  <!-- 

<li><a href="https://www.linkedin.com/posts/…">💼 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>0</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 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="(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>Imagine 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>2</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-1" 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?_=1" /><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-2" 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?_=2" /><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 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="(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-3" 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?_=3" /><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>
		<item>
		<title>Styling the Navigation: Declarative Route and Navigation Matching in CSS</title>
		<link>https://www.bram.us/2026/07/30/styling-the-navigation-declarative-route-and-navigation-matching-in-css/</link>
					<comments>https://www.bram.us/2026/07/30/styling-the-navigation-declarative-route-and-navigation-matching-in-css/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 30 Jul 2026 19:17:12 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36401</guid>

					<description><![CDATA[<p>One of the things that we at Chrome have been thinking about for a while now is a way to do declarative route and navigation matching in CSS. Together with Noam Rosenthal and David Baron I’ve been working on it for the past few months.</p><p>After an initial introduction at the CSS Working Group back in January, and a quick feedback session at CSS Day in June, we think we have something that is ready for further discussion.</p><p>At next week’s CSS Working Group F2F (face-to-face) meeting in Berlin, we’ll be presenting our current line of thinking. This post is a quick intro to the concepts we’ll be covering.</p>]]></description>
										<content:encoded><![CDATA[<div class=intro>
<p>One of the things that we at Chrome have been thinking about for a while now is a way to do declarative route and navigation matching in CSS. Together with Noam Rosenthal and David Baron I’ve been working on this for the past few months.</p>
<p>After an initial introduction at the CSS Working Group back in January, and a quick feedback session with developers at CSS Day in June, we think we have something that is ready for further discussion.</p>
<p>At next week’s CSS Working Group F2F (face-to-face) meeting in Berlin, we’ll be presenting our current line of thinking. This post is a quick intro to the concepts we’ll be covering.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The Problem: Knowing where you&#8217;re going</h3>
<p>If you&#8217;ve played around with View Transitions, especially for Multi-Page Apps (MPAs), you&#8217;ve probably hit a familiar wall. Very often, you want to style a page differently based on <em>where you are coming from</em> and <em>where you are going to</em>.</p>
<p>Imagine a simple flow:</p>
<ul>
<li>When navigating from the index page to the about page, you want the whole site to slide to the left.</li>
<li>When navigating back from the about page to the index page, you want it to slide to the right.</li>
<li>When clicking a thumbnail in a list to go to a detail page, you want that specific clicked thumbnail to transition into the hero image.</li>
</ul>
<p>Right now, doing this requires JavaScript to intercept the navigation, figure out the URLs using <code>pageswap</code> and <code>pagereveal</code>, find the <code>NavigateEvent.sourceElement</code>, and dynamically set the View Transition Types based on those values.</p>
<p>To see it in action, check out this <a href="https://chrome.dev/view-transitions-toolkit/navigation-types/">this demo</a> that uses the components mentioned above.</p>
<figure>
<div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36401-4" width="640" height="612" preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/07/styling-the-navigation.mp4?_=4" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/07/styling-the-navigation.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/07/styling-the-navigation.mp4</a></video></div><figcaption>Recording of a View Transitions Demo that animates different depending on which page you are coming from and which page you are going to.</figcaption></figure>
<p>It works, but the script required to do it right grows over time and can become quite complicated. We wanted to see if styling the navigation experience can be done declaratively instead, in CSS.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Say Hello to Route and Navigation Matching 👋</h3>
<p>The <a href="https://drafts.csswg.org/css-navigation-1/">CSS Route and Navigation Matching Specification (<code style="white-space: nowrap;">css-navigation-1</code>)</a> introduces a few new key components to CSS that allow you to style the navigation and links declaratively.</p>
<blockquote><p>“CSS conditioned on the status of navigating between particular URLs.”</p></blockquote>
<p>Here is a quick <em>(and abbreviated)</em> overview of what we are proposing:</p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>1. Define Routes with <code>@route</code></h4>
<p>Instead of constantly typing out URL strings, you can define named routes using the <code>url-pattern()</code> function.</p>
<pre><code class="language-css">@route --home { pathname: url-pattern('/'); }
@route --about { pathname: url-pattern('/about'); }
@route --detail { pathname: url-pattern('/detail/:id'); }
</code></pre>
<p>Pattern matching is done using the syntax from the popular <a href="https://github.com/pillarjs/path-to-regexp">path-to-regexp</a> library, which is also used by JavaScript’s <code>URLPattern</code>.</p>
<p><i>(For completeness: You can also use the <code>protocol</code>, <code>hostname</code>, <code>port</code>, <code>pathname</code>, <code>search</code>, and <code>hash</code> descriptors to define a named route.)</i></p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>2. Query the navigation with <code>@navigation</code></h4>
<p>This is where the magic happens. You can query the current, ongoing, navigation state using the <code>@navigation</code> at-rule, checking the <code>from</code> and <code>to</code> endpoints.</p>
<pre><code class="language-css">/* Going from home to about? Slide left! */
@navigation (from: --home) and (to: --about) {
  @view-transition {
    navigation: auto;
    types: slide-all-to-left;
  }
}

/* Going back from about to home? Slide right! */
@navigation (from: --about) and (to: --home) {
  @view-transition {
    navigation: auto;
    types: slide-all-to-right;
  }
}
</code></pre>
<p>You can query against a named route (defined with <code>@route</code>) or a direct <code>url-pattern()</code>.</p>
<p><i>(For completeness: We are also introducing <code>between</code> and <code>at</code> keywords for the <em>“navigation relation”</em>.)</i></p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>3. Select the element that triggered the navigation with <code>:nav-source</code></h4>
<p>The <code>:nav-source</code> pseudo-class selector allows you to target the exact element that initiated the outgoing navigation. This is modeled after <code>NavigateEvent.sourceElement</code>.</p>
<p>For example, If you want to apply a <code>view-transition-name</code> only to the specific image that was clicked, you can do this:</p>
<pre><code class="language-css">@navigation (between: --home --detail) {
  @navigation (at: --home) {
    /* Target the clicked link's image */
    :nav-source img {
      view-transition-name: image;
    }
  }
  @navigation (at: --detail) {
    img#hero {
      view-transition-name: image;
    }
  }
}
</code></pre>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>4. Advanced link selection with <code>:link-to()</code></h4>
<p>The spec also introduces ways to style links based on what they link to using the <code>:link-to(...)</code> selector. Its argument is a named route, or <code>url-pattern()</code>. This is incredibly powerful for orchestrating complex UI transitions between routes.</p>
<pre><code class="language-css">/* Target the all links that links to the --detail route */
:link-to(--detail) {
  color: hotpink;
}</code></pre>
<div class="note">
<p>Note: Currently not covered is a way to do parameter matching with <code>:link-to()</code> — e.g. <em>“select the link that links to a <code>--detail</code> route and whose <code>id</code> is equal to <code>6</code>”</em> — as we are still working on that part to make sure the syntax is easy to use and covers all use cases.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>We need your feedback!</h3>
<p>Over the past months we have given this a lot of thought and have gone back and forth over the key features and their syntax, and now we think we have something that could work. As mentioned we will be presenting this at next week’s CSS Working Group F2F meeting, seeking resolutions to verify with the group that we are down the right path.</p>
<p>Back in June we did an introductory pitch at CSS Day, and a quick read of the room told us this was worth pursuing. Of course we are very open to feedback and would love to hear from you if you have any input.</p>
<p>Are we missing a use case? Do the keywords make sense? Or like one of the open questions we have is about the default base URL be used: should relative links be resolved against the document’s location or the stylesheet’s location?</p>
<p>Please take a look at the <a href="https://drafts.csswg.org/css-navigation-1/">current shape of the API</a> and leave your feedback in the <a href="https://github.com/w3c/csswg-drafts/issues/12594#issuecomment-5118003200">w3c/csswg-drafts#12594</a> at the CSSWG, or reach out to me (or Noam) on social media.</p>
<p>Let&#8217;s build this together! 🚀</p>
<p>PS: You can try this out in Chrome Canary with the Experimental Web Platform Features flag enabled 😉</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/3mrv6z3ncls25">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117010627044777658">🦣 Mastodon</a></li>
<li><a href="https://www.linkedin.com/feed/update/urn:li:activity:7488680552045260801/">💼 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/07/30/styling-the-navigation-declarative-route-and-navigation-matching-in-css/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/07/styling-the-navigation.mp4" length="1419999" type="video/mp4" />

			</item>
		<item>
		<title>What’s new in Web UI (Google I/O 2026)</title>
		<link>https://www.bram.us/2026/07/01/whats-new-in-web-ui-google-i-o-2026/</link>
					<comments>https://www.bram.us/2026/07/01/whats-new-in-web-ui-google-i-o-2026/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Wed, 01 Jul 2026 15:40:02 +0000</pubDate>
				<category><![CDATA[Elsewhere]]></category>
		<category><![CDATA[googleio]]></category>
		<category><![CDATA[public speaking]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36347</guid>

					<description><![CDATA[Mid May, at this year’s Google I/O, <a href="https://una.im/">Una</a> and I gave a talk “What’s new in Web UI” live at Shoreline in the Chrome tent.]]></description>
										<content:encoded><![CDATA[<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/07/IMG_3121_VSCO.jpg"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/07/IMG_3121_VSCO.jpg" alt="" width="560" height="747" class="size-medium wp-image-36362" /></a><figcaption>Photo of my laptop in Chrome tent before we opened. My laptop is showing the opening slide of the presentation.</figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<p>Mid May, at this year’s Google I/O, <a href="https://una.im/">Una</a> and I gave a talk “What’s new in Web UI” live at Shoreline in the Chrome tent. The talk was recorded and you can watch it <a href="https://www.youtube.com/watch?v=uT7MVcCQ4rw">here</a>:</p>
<p><iframe loading="lazy" width="560" height="315" src="https://www.youtube.com/embed/uT7MVcCQ4rw?si=t34VccdEpWMQc6hQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<blockquote><p>Take a fast-paced tour of the latest UI features landing in browsers today, with a lens on making beautiful web experiences. From updates to interactive capabilities like scroll-triggered animations and scoped view transitions, to the latest in UI components, learn how to build performant interfaces using native CSS and HTML primitives. Discover how to ship better user experiences with less code.</p></blockquote>
<p>You can find all the demos in <a href="https://codepen.io/collection/kkGxwx">this handy CodePen collection</a>. On Chrome for Developers, we have <a href="https://developer.chrome.com/blog/new-in-web-ui-io26?hl=en">an accompanying blog post that covers the contents of this talk</a>.</p>
<p><a href="https://www.youtube.com/watch?v=UcXWY057YuQ">Watch “What’s new in Web UI” on YouTube &rarr;</a><br />
<a href="https://developer.chrome.com/blog/new-in-web-ui-io26?hl=en">Read “What’s new in Web UI” on Chrome for Developers &rarr;</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/07/01/whats-new-in-web-ui-google-i-o-2026/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cranking View Transtions up to 11 (2026.05.07 @ All Day Hey!)</title>
		<link>https://www.bram.us/2026/06/25/cranking-view-transtions-up-to-11-2026-05-07-all-day-hey/</link>
					<comments>https://www.bram.us/2026/06/25/cranking-view-transtions-up-to-11-2026-05-07-all-day-hey/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 25 Jun 2026 14:49:27 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[public speaking]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36331</guid>

					<description><![CDATA[In May I spoke at the 10th (and final 😔) edition <a href="https://heypresents.com/conferences/2026">All Day Hey!</a> in Leeds, UK. I opened the conference with a talk on View Transitions.]]></description>
										<content:encoded><![CDATA[<p><figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-1.jpeg"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-1.jpeg" alt="" width="560" height="420" class="alignnone size-medium wp-image-36333" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-1.jpeg 1600w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-1-560x420.jpeg 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-1-1120x841.jpeg 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-1-768x576.jpeg 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-1-1536x1153.jpeg 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-1-1568x1177.jpeg 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a><figcaption>Me on stage at All Day Hey!</figure>
</p>
<div class="intro">
<p>In May I spoke at the 10th (and final 😔) edition <a href="https://heypresents.com/conferences/2026">All Day Hey!</a> in Leeds, UK. I opened the conference with a talk on View Transitions.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Table of Contents</h3>
<ul>
<li><a href="#the-talk">The Talk</a></li>
<li><a href="#slides">Slides</a></li>
<li><a href="#recording">Recording/Video</a></li>
<li><a href="#thanks">Thanks!</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-talk" name="the-talk">#</a> The Talk</h3>
<p>This talk dived deeply into the more experimental stuff of View Transitions.</p>
<blockquote>
<p>Ever wondered what happens when you push the View Transition API beyond its documented limits? This talk throws caution to the wind and explores the wild side of View Transitions, cranking them up to 11. We&#8217;ll combine them with Scroll-Driven Animations, trigger them automatically with MutationObserver, and even resurrect classic Internet Explorer’s Page Transitions using this modern API. Prepare for unconventional use cases, unexpected results, and a healthy dose of experimentation as we venture beyond the spec. If you&#8217;re a web developer who loves to tinker and push boundaries, this is the talk for you.</p>
</blockquote>
<p>The talk was a reprise of <a href="https://www.bram.us/2026/06/25/cranking-view-transtions-up-to-11-2026-04-28-beyond-tellerrand/">my rendition at Beyond Tellerrand just a few weeks earlier</a>, but with shuffled and altered content, after <a href="https://letorey.co.uk/">Dave Letorey</a> and <a href="https://remysharp.com/">Remy Sharp</a> gave me very valuable feedback while at BT. Thanks, lads!</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#slides" name="slides">#</a> Slides</h3>
<p>The slides of my talk are <a href="https://slidr.io/bramus/cranking-view-transtions-up-to-11-2026-05-07-nil-all-day-hey">up on slidr.io</a> are embedded below:</p>
<p><iframe loading="lazy" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true" allowtransparency="true" frameborder="0" id="slidrio-deck-3798" src="https://slidr.io/bramus/cranking-view-transtions-up-to-11-2026-05-07-nil-all-day-hey?embed=true" style="border:0; padding:0; margin:0; background:transparent;" width="560" height="490"><a href="https://slidr.io/bramus/cranking-view-transtions-up-to-11-2026-05-07-nil-all-day-hey#1" title="Cranking View Transitions up to 11 (2026.05.07 @ All Day Hey!)">Watch the slides</a></iframe></p>
<p>My deck is riddled with videos and animations, but because this is a PDF export these are not included in the embed above.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#recording" name="recording">#</a> Recording/Video</h3>
<p>The talk was recorded and you can <a href="https://www.youtube.com/watch?v=JYPAsGVx_WE&#038;t=115s">watch the video on YouTube</a>. It is also embedded below:</p>
<p><iframe loading="lazy" width="560" height="315" src="https://www.youtube.com/embed/JYPAsGVx_WE?si=5DDLKykYjVYu_5vA&amp;start=115" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#thanks" name="thanks">#</a> Thanks!</h3>
<p>I’ve heard many great things about All Day Hey! over the years, so it was on my wishlist to ever speak there. Back in November 2025 I sent a cold DM to organizer <a href="https://joshnesbitt.dev/">Josh Nesbitt</a>, offering to speak at his event. I knew I was <em>extremely late</em> with my request, and after some back and forth, I was absolutely delighted when Josh confirmed my slot.</p>
<p>It was almost a very close call, though. Just a few days before the event, I ate something bad and spent the better part of the night hugging my toilet. Thankfully, it was just food poisoning rather than a full-blown stomach bug, so I bounced back in time to make it to the event fully energized.</p>
<p>The day before the conference, we were treated an incredible speaker dinner. Honestly, the food was soooo good!</p>
<p><figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-3.jpeg"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-alldayhey-3.jpeg" alt="" width="540" height="747" class="alignnone size-medium wp-image-36334" /></a><figcaption>Josh and Jake at the speaker dinner.</figcaption></figure>
</p>
<p>One quick late night rehearsal run (with some last minute tweaks, of course) I was ready for the conference day itself the day after. And as expected, the atmosphere was fantastic. It was an incredibly friendly crowd, and it felt good to run into so many familiar faces again. Catching up and/or talking tech with Jake, Ana, Ryan, Dave, Remy, Oliver, Barry, Safari, Cassie, Alistair, Phil, James, Keith, Luke, Josh, … really made the day special.</p>
<p>One of the absolute highlights for me was an unexpected <a href="https://bsky.app/profile/jakearchibald.com/post/3mlaw6zq2i22a">shout-out from Jake Archibald on Bluesky</a>. He praised the job I’ve been doing with View Transitions since taking over the work after he left Google. Coming from him, that really meant the world to me.</p>
<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:l4m7ynxc47wzkgvlyyunx5n2/app.bsky.feed.post/3mlaw6zq2i22a" data-bluesky-cid="bafyreiferejgg73febs2tkcfdn45mlhtcdg5e6kun4l3lqkswa4val2hpa" data-bluesky-embed-color-mode="system">
<p lang="en">My only sadness about leaving Chrome at the time, was stopping working on view transitions, but @bram.us took that on, and is doing it _so well_.</p>
<p><a href="https://bsky.app/profile/did:plc:l4m7ynxc47wzkgvlyyunx5n2/post/3mlaw6zq2i22a?ref_src=embed">[image or embed]</a></p>
<p>&mdash; Jake Archibald (<a href="https://bsky.app/profile/did:plc:l4m7ynxc47wzkgvlyyunx5n2?ref_src=embed">@jakearchibald.com</a>) <a href="https://bsky.app/profile/did:plc:l4m7ynxc47wzkgvlyyunx5n2/post/3mlaw6zq2i22a?ref_src=embed">May 7, 2026 at 11:38 AM</a></p></blockquote>
<p><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note"><p>💁‍♂️ If you are a conference or meetup organiser, don't hesitate to <a href="https://www.bram.us/speaking-training/">contact me to come speak at your event</a>.</p></div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/06/25/cranking-view-transtions-up-to-11-2026-05-07-all-day-hey/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cranking View Transtions up to 11 (2026.04.28 @ Beyond Tellerrand)</title>
		<link>https://www.bram.us/2026/06/25/cranking-view-transtions-up-to-11-2026-04-28-beyond-tellerrand/</link>
					<comments>https://www.bram.us/2026/06/25/cranking-view-transtions-up-to-11-2026-04-28-beyond-tellerrand/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 25 Jun 2026 14:01:52 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[public speaking]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36323</guid>

					<description><![CDATA[Back in April I went to Beyond Tellerrand in Düsseldorf. I initially planned on going as an attendee, but when organizer Marc asked me to speak I couldn’t say no …]]></description>
										<content:encoded><![CDATA[<p><figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-1.jpeg"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-1.jpeg" alt="" width="560" height="420" class="alignnone size-medium wp-image-36324" /></a><figcaption>Me, on stage, referencing the fire alarm we had at the speaker hotel in the early morning (and sticking it to <a href="https://nielsleenheer.com/">Niels Leenheer</a> 😛)</figcaption></figure>
</p>
<div class="intro">
<p>Back in April I went to Beyond Tellerrand in Düsseldorf. I initially planned on going as an attendee, but when organizer <a href="https://marcthiele.com/">Marc</a> asked me to speak I couldn’t say no …</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Table of Contents</h3>
<ul>
<li><a href="#the-talk">The Talk</a></li>
<li><a href="#slides">Slides</a></li>
<li><a href="#recording">Recording/Video</a></li>
<li><a href="#thanks">Thanks!</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-talk" name="the-talk">#</a> The Talk</h3>
<p>I initially planned on giving the talk <a href="https://www.bram.us/2025/05/31/supercharge-web-ux-with-view-transitions-2025-05-28-webexpo/">“Supercharge Web UX with View Transitions”</a> at Beyond Tellerrand as the crowd is a mix of technical and non-techical people. However, after chatting with <a href="https://matthiasott.com/">Matthias</a> an Quinten, I decided to switch to “Cranking View Transtions up to 11” as it lines up better with <a href="https://beyondtellerrand.com/about">the concept of Beyond Tellerrand</a></p>
<blockquote><p>Ever wondered what happens when you push the View Transition API beyond its documented limits? This talk throws caution to the wind and explores the wild side of View Transitions, cranking them up to 11. We&#8217;ll combine them with Scroll-Driven Animations, trigger them automatically with MutationObserver, and even resurrect classic Internet Explorer’s Page Transitions using this modern API. Prepare for unconventional use cases, unexpected results, and a healthy dose of experimentation as we venture beyond the spec. If you&#8217;re a web developer who loves to tinker and push boundaries, this is the talk for you.</p></blockquote>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#slides" name="slides">#</a> Slides</h3>
<p>The slides of my talk are <a href="https://slidr.io/bramus/cranking-view-transtions-up-to-11-2026-04-28-nil-beyond-tellerrand">up on slidr.io</a> are embedded below:</p>
<p><iframe loading="lazy" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true" allowtransparency="true" frameborder="0" id="slidrio-deck-3797" src="https://slidr.io/bramus/cranking-view-transtions-up-to-11-2026-04-28-nil-beyond-tellerrand?embed=true" style="border:0; padding:0; margin:0; background:transparent;" width="560" height="490"><a href="https://slidr.io/bramus/cranking-view-transtions-up-to-11-2026-04-28-nil-beyond-tellerrand" title="Cranking View Transitions up to 11 (2026.04.28 @ Beyond Tellerrand)">Check out the slides</a></iframe></p>
<p>Unfortunately these exported slides don’t contain any of the slide transitions that supported the story I was bringing. Also missing are any recordings of the included demos (they’re just screenshots in the export), but you can click the links to check them out yourself.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#slides" name="recording">#</a> Recording/Video</h3>
<p>The talk was recorded but the recording has not been released yet. Instead, you can <a href="https://www.bram.us/2026/06/25/cranking-view-transtions-up-to-11-2026-05-07-all-day-hey/">watch the recording from my rendition at <em>All Day Hey!</em></a> which I did just two weeks later.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#thanks" name="thanks">#</a> Thanks!</h3>
<p>Beyond Tellerrand is one of those conferences that I have been following from the sidelines for quite a while, but for some reason, I never actually made it to the event—there was always <em>some</em> scheduling conflict that got in the way. But now, after all those years, I finally made it. It was great to see firsthand that organizer <a href="https://marcthiele.com/">Marc Thiele</a> is exactly the kind and warm person he always appeared to be from afar.</p>
<p>Everything at BT is so well thought of (like, they had <a href="https://www.youtube.com/watch?v=zGd6_t_Fqtk">custom made opening titles</a> and posters, created by Brendan Dawes), but what truly sets the event apart is its chill, relaxed, and incredibly welcoming atmosphere.</p>
<p>Marc also did an excellent job at curating the sessions. It was an absolute honor to share the stage with Oliver Reichtenstein, Niels Leenheer <em>(lasers! doom!)</em>, James Victore, Annie Atkins <em>(Wes Anderson is one of my favorite directors)</em>, Andre Michelle (OpenDAW!), James White, and Lauren Celenza — Your talks wildly inspired me and made a lasting impression.</p>
<p><em>(The fire alarm going off in the hotel at 5:30 also was quite memorable 😵)</em></p>
<p><figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-2-scaled.jpeg"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-2-560x420.jpeg" alt="" width="560" height="420" class="alignnone size-medium wp-image-36327" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-2-560x420.jpeg 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-2-1120x840.jpeg 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-2-768x576.jpeg 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-2-1536x1152.jpeg 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-2-2048x1536.jpeg 2048w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/bramus-bt-2-1568x1176.jpeg 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a><figcaption>Me at “The CSS Table”, with Nils, Brecht, and Matthias.</figcaption></figure>
</p>
<p>To top it all off, the real value you take away from this conference are the connections. Extra shout-out to Brecht De Ruyte, Matthias Ott, Quentin, Remy Sharp, Dave Letorey, Nils Binder, Schepp, Marco, Charis, and Daniela as well — It was great catching up between and after sessions with all of you.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note"><p>💁‍♂️ If you are a conference or meetup organiser, don't hesitate to <a href="https://www.bram.us/speaking-training/">contact me to come speak at your event</a>.</p></div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/06/25/cranking-view-transtions-up-to-11-2026-04-28-beyond-tellerrand/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Do Websites Need to Function Exactly the Same on Every Platform?</title>
		<link>https://www.bram.us/2026/06/21/do-websites-need-to-function-exactly-the-same-on-every-platform/</link>
					<comments>https://www.bram.us/2026/06/21/do-websites-need-to-function-exactly-the-same-on-every-platform/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Sun, 21 Jun 2026 21:59:57 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[opinion]]></category>
		<category><![CDATA[progressive enhancement]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36286</guid>

					<description><![CDATA[<p>While we’ve accepted that websites don't need to <em>look</em> the same everywhere, we seem to be stuck on a new hurdle: the idea that websites need to <em>function</em> the same everywhere.</p>]]></description>
										<content:encoded><![CDATA[<p><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/do-websites-need-to-function-exactly-the-same-on-every-platform.jpg" alt="" width="560" height="315" class="alignnone size-medium wp-image-36307" /></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Early 2000s’ Pixel Perfection</h3>
<p>If you, like me, were building websites back in the early 2000s, you’ll undoubtedly remember the absolute headache that was cross-browser compatibility.</p>
<p>What didn’t help back then in this era of Internet Explorer, Netscape, and Firefox was the stubborn, widespread belief that websites needed to <strong>look</strong> exactly the same in every single browser. Pixel perfection was the (flawed) holy grail.</p>
<p>Thankfully, the web community eventually came to its senses by embracing <a href="https://en.wikipedia.org/wiki/Progressive_enhancement">Progressive Enhancement</a>. The single-purpose website <a href="https://web.archive.org/web/20071212220950/http://dowebsitesneedtolookexactlythesameineverybrowser.com/"><code>dowebsitesneedtolookexactlythesameineverybrowser.com</code></a> that just displayed a giant, resounding “no” on the screen, was a great way to spread the word.</p>
<figure><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/dowebsitesneedtolookexactlythesameineverybrowser.png" alt="" width="560" height="372" class="alignnone size-medium wp-image-36291" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/06/dowebsitesneedtolookexactlythesameineverybrowser.png 2520w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/dowebsitesneedtolookexactlythesameineverybrowser-560x372.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/dowebsitesneedtolookexactlythesameineverybrowser-1120x743.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/dowebsitesneedtolookexactlythesameineverybrowser-768x510.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/dowebsitesneedtolookexactlythesameineverybrowser-1536x1019.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/dowebsitesneedtolookexactlythesameineverybrowser-2048x1359.png 2048w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/dowebsitesneedtolookexactlythesameineverybrowser-1568x1040.png 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /><figcaption>Screenshot of the <code>dowebsitesneedtolookexactlythesameineverybrowser.com</code> website.</figcaption></figure>
<p>The site was featured in quite a lot of conference talks, such as <a href="https://adactio.com/">Jeremy Keith</a>’s talk from <a href="https://border-none.net/2013/">border:none 2013</a> <em>(at the 39:18 mark)</em>:</p>
<p><iframe loading="lazy" width="560" height="315" src="https://www.youtube.com/embed/1dxlVt7MJdc?si=woQxLHrsPcpNLwFy&amp;start=2358" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<blockquote>
<p>But there’s a fundamental thing, a fundamental philosophical approach that you have to approach your work with, if you’re going to use progressive enhancement. And it’s simply answering the question, do websites need to look exactly the same in every browser?</p>
<p>A very simple question. And you can find out the answer to this question by going to the website, dowebsitesneedtolookexactlythesameineverybrowser.com, where you will see the answer written out there on the screen: No.</p>
<p><p>However, the answer will appear different, depending on the browser you’re using, because of CSS and JavaScript enhancements. It’s a self-describing definition of progressive enhancement done by Dan Cederholm.</p>
</blockquote>
<p>But still, it took years to shed that misconception. <mark>And while we’ve now accepted that websites don&#8217;t need to <em>look</em> the same everywhere, we seem to be stuck on a new hurdle: the idea that websites need to <em>function</em> the same everywhere.</mark></p>
<p>(Spoiler: No, they do <strong>not</strong>.)</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>A shifting paradigm</h3>
<p>A lot has changed since those early days. Safari and Chrome came into existence and practically took over. The iPhone arrived and set the baseline for what <a href="https://www.youtube.com/watch?v=MnrJzXM7a6o">a mobile device with a “breakthrough internet communicator”</a> should be. Today, browsers run on just about everything: laptops, phones, tablets with styluses, tablets with keyboards, smartwatches, you name it.</p>
<p>Each of these devices comes with its own input methods and interaction paradigms. On a desktop, you typically scroll with a mouse wheel. If you can’t use a mouse, you use the keyboard. On touch devices, you swipe with one finger and pinch-zoom with two fingers. And on spatial computing devices like the Vision Pro, you can scroll using your hands or <a 
href="https://www.youtube.com/watch?v=lFrwtJLZ83E">just using your eyes</a>.</p>
<figure><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/touch-core-gestures.png" alt="" width="560" height="324" class="alignnone size-medium wp-image-36295" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/06/touch-core-gestures.png 1810w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/touch-core-gestures-560x324.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/touch-core-gestures-1120x647.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/touch-core-gestures-768x444.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/touch-core-gestures-1536x888.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/touch-core-gestures-1568x906.png 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /><figcaption>Core Gestures on Touch Devices. Images by <a href="https://lukew.com/ff/entry.asp?1071">Luke Wroblewski</a>.</figcaption></figure>
<div class=note>
<p>Sidenote: Interestingly some of the paradigms bleed out from one platform to the other, and the lines blur. For example, I often catch myself wanting to click and drag a scroller on desktop, similar to how I scroll on a touch device. This actually led me to file <a href="https://github.com/w3c/csswg-drafts/issues/9811">Issue #9811 on Draggable Scrollers</a> at the CSSWG in 2024.</em></p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Embracing input-specific features</h3>
<p>One of the things I like about a lot of modern web apps out there is that they have added keyboard shortcuts to their UI. Sites like YouTube, GitHub, Gmail, and Feedly all offer them. The de facto standard now is to press the <kbd>?</kbd> key to bring up a dialog detailing all available keyboard shortcuts.</p>
<figure><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/github-shortcuts.png" alt="" width="560" height="376" class="alignnone size-medium wp-image-36298" /><figcaption>The GitHub website showing a dialog with the shortcuts it supports.</figcaption></figure>
<p>These shortcuts are an extra way to reaching a specific goal. On YouTube for example, you can hit the <kbd>f</kbd> key on your keyboard to fullscreen the currently playing video. Because this extra type of activation is strictly limited to the keyboard input method, it is unique to that input method. When there is no keyboard available, you can’t use it, and you still have to locate and click the fullscreen icon in the UI.</p>
<p>I like to use these shortcuts as the perfect example to show that it is perfectly fine for sites to offer different, alternative, functionality based on what the platform and input method can offer:</p>
<ul>
<li><strong>Got touch?</strong> Great, you can now swipe and pinch-zoom on things.</li>
<li><strong>Got a keyboard?</strong> Great, you can focus elements by tabbing into them.</li>
<li><strong>Got a pointer device?</strong> Great, things can now happen on hover.</li>
<li><strong>Using a keyboard?</strong> Great, you can use handy shortcuts.</li>
</ul>
<p>A practical example here is a modal dialog that is getting shown: depending on which platform and input mechanism combo you are using, you can close it by flinging it away, hitting the ESC key, doing a back swipe, tapping the backdrop, or by activating the close button.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The impact on Web Platform features</h3>
<p>These aren&#8217;t just abstract observations. The friction between platform-specific capabilities and the desire for universal functionality is actively bottlenecking very hot topics currently being developed for the web platform.</p>
<p style="text-align: center; font-size: 14; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>Interest Invokers</h4>
<p>For the past 1.5 years, there has been a lot of discussion on how <a href="https://open-ui.org/components/interest-invokers.explainer/">interest invokers</a> should work on touch devices. On touch devices, the de facto standard to express &quot;interest&quot; in an element is to long-press it. But browsers on touch devices already use that long press to reveal the system context menu (*). This clash pushed the Open UI folks to <a href="https://open-ui.org/components/interest-invokers.explainer/#touchscreen-options">explore more options</a>. Chrome is currently prototyping the <code>::interest-button</code> pseudo-element option behind a feature flag but it’s already becoming clear that, although well-intended, <a href="https://bsky.app/profile/lea.verou.me/post/3mo3zcjop2c2c">enabling it by default is doing more harm than good</a>.</p>
<p><em>(* Unless you do the proprietary <code>-webkit-touch-callout: none</code> on iOS)</em></p>
<div class=note>
<p>Sidenote: there is a misconception making rounds that <em>“Chrome has shipped this <code>::interest-button</code>”</em> ([<a href="https://bsky.app/profile/davatron5000.bsky.social/post/3monfad7a4226">1</a>][<a href="https://bsky.app/profile/rich-harris.dev/post/3mol7cjry2s2b">2</a>]). <strong>This is not the case</strong>, as the feature is only available behind a feature flag, for testing. So let it be clear: <a href="https://bsky.app/profile/did:plc:343p6xcgmvkpz5abgezlgyep/post/3monkekr2js2b">Features behind a feature flag have not shipped.</a></p>
</div>
<p style="text-align: center; font-size: 14; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>Overscroll Actions / Areas</h4>
<p><a href="https://open-ui.org/components/overscroll-actions.explainer/">Overscroll Actions / Areas</a> is another feature currently being explored at Open UI. A major sticking point is discoverability on desktop devices. Some folks are arguing that there <em>must</em> be a visible button in the UI to reveal these overscroll areas on desktop. My take? <a href="https://github.com/openui/open-ui/issues/1457">There shouldn&#8217;t be</a>. People naturally try out swipe/scroll gestures on trackpads nowadays, and just like keyboard shortcuts, this is simply an <em>extra</em> means of reaching a goal, not the only one. Your UIs should already be accessible without Overscroll Areas — Check out <a href="https://github.com/openui/open-ui/issues/1457">the issue</a> for more details on this (the email interface example).</p>
<p style="text-align: center; font-size: 14; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>Document Picture-in-Picture</h4>
<p>Picture-in-Picture (PiP) is a well-established paradigm for video. The newer <a href="https://github.com/WICG/document-picture-in-picture/">Document Picture-in-Picture API</a> extends this functionality, allowing you to pop <em>any</em> HTML content into a floating window. It&#8217;s brilliant for things like tabbing away from a Google Meet call and keeping the call controls and participant videos visible. Yet, WebKit <a href="https://github.com/WebKit/standards-positions/issues/41#issuecomment-4307350675">formally opposed</a> the API, stating it <q>is a paradigm that doesn’t map well to all platforms.</q></p>
<p>They specifically called out that <q>on iOS and iPadOS, system-level picture-in-picture is specifically designed for video content</q> … which seems like a “you-problem” IMHO.</p>
<p><em>(For completeness: they also mentioned being <q>concerned about the user experience implications of giving websites the ability to create always-on-top windows</q> but in the same spirit I also have my concerns about the <code>color</code> property because developers can create light-text-on-a-light-background experiences that also implicates UX …)</em></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Moving Forward</h3>
<p>The main theme across all these discussions, debates, and oppositions is a lingering belief that web features must work the exact same way on all platforms.</p>
<p>If we as a community (and as platform engineers) started accepting that <strong>websites do not need to function exactly the same on every platform</strong>, these feature discussions could have been finished a long time ago (or would at least go down easier), allowing web developers to just build better, progressively enhanced experiences for their users.</p>
<p>Singling out the Interest Invokers use-case: if the long press is a no-go, and the always-on <code>::interest-button</code> is pushing folks to force removing them <em>(as the initial replies suggest)</em>, then perhaps the answer is to make them opt-in or, even further, not having them at all? Like, I wouldn&#8217;t really mind <em>not</em> seeing an info card on GitHub or Wikipedia when using touch as the input mechanism, because the info show in those cards is available on the next page anyway … and perhaps that’s OK?</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/06/21/do-websites-need-to-function-exactly-the-same-on-every-platform/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>View Transitions: Use the new attr() or match-element for the view-transition-name?</title>
		<link>https://www.bram.us/2026/06/19/view-transition-name-attr-or-match-element/</link>
					<comments>https://www.bram.us/2026/06/19/view-transition-name-attr-or-match-element/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Fri, 19 Jun 2026 21:26:45 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[attr]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36265</guid>

					<description><![CDATA[<p>At CSS Day, <em>“YouTube guy”</em> <a href="https://www.kevinpowell.co/">Kevin Powell</a> showed a lot of demos that relied on <a href="https://www.bram.us/2025/01/20/css-attr-gets-an-upgrade/">the advanced <code>attr()</code> function</a>. In one of the examples he used <code>attr()</code> to set <code>view-transition-name</code> values — a technique I covered in <a href="https://www.bram.us/2025/01/20/css-attr-gets-an-upgrade/">my article on advanced <code>attr()</code> function</a>.</p><p>But then during the Q&#38;A, <a href="https://cydstumpel.nl/">Cyd Stumpel</a> wondered if he couldn’t just use <code>match-element</code> there.</p><p>The short answer to that question is yes. The longer answer is … <a href="http://brm.us/it-depends">It Depends™</a></p>]]></description>
										<content:encoded><![CDATA[<p><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/view-transitions.png" alt="" width="560" height="318" class="alignnone size-medium wp-image-36278" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/06/view-transitions.png 1295w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/view-transitions-560x318.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/view-transitions-1120x637.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/06/view-transitions-768x436.png 768w" sizes="auto, (max-width: 560px) 100vw, 560px" /></p>
<div class=intro>
<p>At CSS Day, <em>“YouTube guy”</em> <a href="https://www.kevinpowell.co/">Kevin Powell</a> showed a lot of demos that relied on <a href="https://www.bram.us/2025/01/20/css-attr-gets-an-upgrade/">the advanced <code>attr()</code> function</a>. In one of the examples he used <code>attr()</code> to set <code>view-transition-name</code> values — a technique I covered in <a href="https://www.bram.us/2025/01/20/css-attr-gets-an-upgrade/">my article on advanced <code>attr()</code> function</a>.</p>
<p>But then during the Q&amp;A, <a href="https://cydstumpel.nl/">Cyd Stumpel</a> wondered if he couldn’t just use <code>match-element</code> there.</p>
<p>The short answer to that question is yes. The longer answer is … <a href="http://brm.us/it-depends">It Depends™</a></p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><code>view-transition-name: attr(…);</code></h3>
<p>To recap, the technique Kevin showed is this:</p>
<pre><code class="language-html">&#x3C;style&#x3E;
  .card {
    view-transition-name: attr(data-id type(&#x3C;custom-ident&#x3E;), none);
    view-transition-class: card;
  }
&#x3C;/style&#x3E;</code></pre>
<p>It reads the <code>data-id</code> attribute’s value and casts it to a <code>&#x3C;custom-ident&#x3E;</code> for use as the <code>view-transition-name</code> value.</p>
<p>You can see Kevin apply it live here. When applied, all cards will get a <code>view-transition-name</code> and get snapshotted — and therefore also move — individually as part of the View Transition.</p>
<figure>
<div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36265-5" width="640" height="360" preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/06/IMG_4559-1080p-30fps.mp4?_=5" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/06/IMG_4559-1080p-30fps.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/06/IMG_4559-1080p-30fps.mp4</a></video></div><figcaption>Kevin live coding it at CSS Day. Note he starts off typing <code>data()</code> which is later corrected to <code>attr()</code> with some help from <a href="https://jakearchibald.com/">Jake</a> and myself.</figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><code>view-transition-name: match-element;</code></h3>
<p>The thing Cyd mentioned is <code>match-element</code>, which automatically gives an element a <code>view-transition-name</code> value based on the element’s identity. In Kevin’s demo this would totally work, and the demo would work just as with the <code>attr()</code> code.</p>
<pre><code class="language-html">&#x3C;style&#x3E;
  .card {
    view-transition-name: match-element;
    view-transition-class: card;
  }
&#x3C;/style&#x3E;</code></pre>
<p>So, what’s the fuss about then?</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><code>match-element</code> works … until it doesn’t</h3>
<p>The thing with <code>match-element</code> is that it uses <em>element identity</em> to assign <em>automagic</em> values. This element identity is not to be confused with the <code>id</code> attribute — they are two different things.</p>
<p>Think of this identity as an internal number that the browser assigns to created elements. Every time an element is created, the internal counter increments. So if you were to destroy an element and then recreate it including its <code>id</code>, you’d end up with an element that that has a different <em>identity</em>, even though it has the same <code>id</code>.</p>
<p>The side-effect of <code>match-element</code> using element identity, is that are two scenarios when you should <strong>not</strong> be using <code>match-element</code> at all:</p>
<ol>
<li>When you build an MPA <em>(aka: a website)</em>, <code>match-element</code> doesn’t work at all because nodes in different documents are never the same; they all have a different identity, so <code>match-element</code> can never match them.</li>
<li>If you need to style a specific element from the whole set, then you need the unique name to use in your <code>::view-transition-*()</code> selectors, which <code>match-element</code> doesn&#8217;t give you.</li>
</ol>
<p>So, there you have it 🙂</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class=tldr>
<p>TL;DR <code>match-element</code> is limited to Same-Document View Transitions and can’t be used to uniquely target any of the View Transition pseudos.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/06/19/view-transition-name-attr-or-match-element/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/06/IMG_4559-1080p-30fps.mp4" length="14596071" type="video/mp4" />

			</item>
	</channel>
</rss>
