<?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, 01 Jul 2026 16:14:59 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.5</generator>
	<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 fetchpriority="high" 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 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 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="(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/#respond</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>0</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-1" 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?_=1" /><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>
		<item>
		<title>Introducing view-transitions-toolkit, a collection of utility functions to more easily work with View Transitions.</title>
		<link>https://www.bram.us/2026/04/02/view-transitions-toolkit/</link>
					<comments>https://www.bram.us/2026/04/02/view-transitions-toolkit/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 02 Apr 2026 20:03:48 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36216</guid>

					<description><![CDATA[<p>In my work with View Transitions over the last several years, I’ve published everything from deep-dive articles, demos, and announcement videos at Google I/O. I’ve also done some more experimental things with it, such as <a href="https://www.bram.us/2025/02/07/view-transitions-applied-more-performant-view-transition-group-animations/">optimizing the keyframes</a> or <a href="https://www.bram.us/2024/04/29/if-view-transitions-and-scroll-driven-animations-had-a-baby-css-cafe/">driving a View Transition by scroll</a>.</p>

<p>To turn the lessons from these scattered experiments into something more reusable for both you and me, I’ve bundled the most frequent code patterns into a dedicated package: <a href="https://chrome.dev/view-transitions-toolkit/"><code>view-transitions-toolkit</code></a>.</p>]]></description>
										<content:encoded><![CDATA[<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/04/view-transitions-toolkit-main.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/04/view-transitions-toolkit-main.png" alt="" width="560" height="370" class="alignnone size-medium wp-image-36217" /></a><figcaption>Screenshot of <a href="https://chrome.dev/view-transitions-toolkit/">the View Transitions Toolkit homepage</a></figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="intro">
<p>In my work with View Transitions over the last several years, I’ve published everything from deep-dive articles, demos, and announcement videos at Google I/O. I’ve also done some more experimental things with it, such as <a href="https://www.bram.us/2025/02/07/view-transitions-applied-more-performant-view-transition-group-animations/">optimizing the keyframes</a> or <a href="https://www.bram.us/2024/04/29/if-view-transitions-and-scroll-driven-animations-had-a-baby-css-cafe/">driving a View Transition by scroll</a>.</p>
<p>To turn the lessons from these scattered experiments into something more reusable for both you and me, I’ve bundled the most frequent code patterns into a dedicated package: <a href="https://chrome.dev/view-transitions-toolkit/"><code>view-transitions-toolkit</code></a>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The <code>view-transitions-toolkit</code></h3>
<p><code>view-transitions-toolkit</code> is a collection of small, focused helper functions for use with View Transitions. They fill in some of the gaps to making advanced patterns much easier to implement.</p>
<ul>
<li><strong>Feature Detection</strong>: Get information about whether certain View Transitions sub-features are supported.</li>
<li><strong>Shim Support</strong>: Shim support for <code>document.activeViewTransition</code>.</li>
<li><strong>Animations</strong>: Utilities for extracting, measuring, and optimizing animations.</li>
<li><strong>Transition Playback Control</strong>: Pause, Resume, or Scrub the playback of a View Transition.</li>
<li><strong>Automatic Page Navigation Types</strong>: Automatically inject View Transition Types based on navigation origin/destination.</li>
</ul>
<p>The goal of the project is to provide these utilities so you don&#8217;t have to reinvent the wheel every time you want to do something slightly advanced with View Transitions.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Getting Started</h3>
<p>Using the toolkit is incredibly simple. First, install it from npm:</p>
<pre><code>npm i view-transitions-toolkit</code></pre>
<p>Then, you can import the modules you need and use their functions. Optimizing the keyframes of a View Transition group for example is a simple one-liner:</p>
<pre><code class="language-js">import { optimizeGroupAnimations, OPTIMIZATION_STRATEGY } from "view-transitions-toolkit/animations";

const t = document.startViewTransition(() => { … });
await t.ready;

// Optimize all Group Animations using the default SCALE strategy
optimizeGroupAnimations(t, "*");

// Optimize only the `::view-transition-group(box-flip)` animation using the SLIDE strategy
optimizeGroupAnimations(t, "box-flip", OPTIMIZATION_STRATEGY.SLIDE);</code></pre>
<p>I won’t go into details here, as all features are documented in <a href="https://github.com/GoogleChromeLabs/view-transitions-toolkit/tree/main/docs">the docs folder</a> of the repo.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Demos</h3>
<p>The project also comes with a public website that not only gives you the basic info, but also comes with a bunch of demos: <a href="https://chrome.dev/view-transitions-toolkit/">https://chrome.dev/view-transitions-toolkit/</a>.</p>
<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/04/view-transitions-toolkit-optimize.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/04/view-transitions-toolkit-optimize.png" alt="" width="560" height="370" class="alignnone size-medium wp-image-36218" /></a><figcaption>Screenshot of <a href="https://chrome.dev/view-transitions-toolkit/optimize/">the <code>optimize()</code> demo</a></figcaption></figure>
<p>The source of the demos is included in the repository as well.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Go check it out</h3>
<ul>
<li><a href="https://chrome.dev/view-transitions-toolkit/"><code>view-transitions-toolkit</code> demos  (Chrome.dev) &rarr;</a></li>
<li><a href="https://github.com/googlechromelabs/view-transitions-toolkit/"><code>view-transitions-toolkit</code> source (GitHub) &rarr;</a></li>
<li><a href="https://www.npmjs.com/package/view-transitions-toolkit"><code>view-transitions-toolkit</code> on NPM &rarr;</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3mijlrthmc22v">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/116335982870732034">🦣 Mastodon</a></li>
<li><a href="https://www.linkedin.com/feed/update/urn:li:activity:7445503375812567041/">💼 LinkedIn</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/04/02/view-transitions-toolkit/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>CSS position: sticky now sticks to the nearest scroller on a per axis basis!</title>
		<link>https://www.bram.us/2026/03/30/css-sticky-per-axis/</link>
					<comments>https://www.bram.us/2026/03/30/css-sticky-per-axis/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Mon, 30 Mar 2026 12:21:18 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[sticky]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36196</guid>

					<description><![CDATA[<p>If you’ve ever tried to build a data table with a sticky header and a sticky first column, you know the pain. You’d think a simple <code>position: sticky</code> with <code>top: 0</code> and <code>left: 0</code> would be enough, but the reality was that only one of both would stick.</p>

<p>A recent change to CSS fixes this: <code>position: sticky</code> now plays nice with <em>single-axis scrollers</em>, allowing you to have sticky elements that track different scroll containers on different axes. This change is available for testing in Chrome 148 with the experimental web platform features flag flipped.</p>]]></description>
										<content:encoded><![CDATA[<!-- Added by Post/Page Specific Custom Code plugin, thank you for using! -->
<style>/* DL GRID @src https://codepen.io/bramus/pen/POEaXg*/
@media(min-width: 30em) {
dl {
	display: grid;
	grid-template: auto / 10em 1fr;
}

dt {
  grid-column: 1;
}

dd {
  grid-column: 2;
}

dt, dd {
	margin: 0;
	padding: .3em .5em;
	border-top: 1px solid rgba(0,0,0,.1);
}

:is(dt, dd) > :first-child {
  margin-top: 0;
}
:is(dt, dd) > :last-child {
  margin-bottom: 0;
}
}</style>
<figure><div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36196-2" width="640" height="391" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/03/sticky-per-axis-2.mp4?_=2" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/03/sticky-per-axis-2.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/03/sticky-per-axis-2.mp4</a></video></div><figcaption>Recording of <a href="https://codepen.io/bramus/pen/VYKQwmK">the demo</a>.</figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="intro">
<p>If you’ve ever tried to build a data table with a sticky header and a sticky first column, you know the pain. You’d think a simple <code>position: sticky</code> with <code>top: 0</code> and <code>left: 0</code> would be enough, but the reality was that only one of both would stick.</p>
<p>A recent change to CSS fixes this: <code>position: sticky</code> now plays nice with <em>single-axis scrollers</em>, allowing you to have sticky elements that track different scroll containers on different axes. This change is available in for testing in Chrome 148 with the experimental web platform features flag flipped.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-situation" name="the-situation">#</a> The Situation</h3>
<p>To understand the impact, let&#8217;s quickly review the standard setup for a responsive table. You start with a wide HTML <code>&lt;table&gt;</code> full of data. To prevent this wide table from breaking your page layout on small screen devices, you typically wrap it in a container with <code>overflow-x: auto</code>.</p>
<pre><code class="language-html">&lt;div class="table-wrapper" style="overflow-x: auto;"&gt;
  &lt;table&gt;
    …
  &lt;/table&gt;
&lt;/div&gt;
</code></pre>
<p>Because you want the column headers (the <code>&lt;thead&gt;</code>) to stay visible when scrolling down the document, you apply <code>position: sticky; top: 0;</code> to them. Simultaneously, you want your first column to stay visible when scrolling sideways through the data, so you apply <code>position: sticky; left: 0;</code> there.</p>
<pre><code class="language-css">.table-wrapper {
  overflow-x: auto;
}

.table-wrapper thead {
  position: sticky;
  top: 0;
}

.table-wrapper td:first-child {
  position: sticky;
  left: 0;
}
</code></pre>
<p>However, this doesn&#8217;t work as expected. While the first column will stick to the left edge of the table wrapper when scrolling horizontally, the headers will scroll out of view along with the rest of the page.</p>
<p>This is because the <code>.table-wrapper</code> becomes the sticky reference for <em>both</em> axes. Because the wrapper only scrolls horizontally, the vertical sticking becomes completely ineffective. Your <code>top: 0</code> headers would just happily scroll out of view along with the rest of the page 😭</p>
<p>To get around this, you had to rely on lots of JavaScript to synchronize the scroll position, or rely on duplicated headers.</p>
<div class="note">
<p>Note: While there are <a href="https://css-tricks.com/a-table-with-both-a-sticky-header-and-a-sticky-first-column/">pure CSS solutions</a> to make sure the headers also stick within the <code>.table-wrapper</code>, the problem described here is different: instead of getting the headers to stick inside the <strong>same</strong> scroller, you want the headers to be stuck against a <strong>different</strong> scroller <em>(here: the <code>.table-wrapper</code> for horizontally sticky stuff, and the document’s scroller for vertically sticky stuff)</em>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-change" name="the-change">#</a> The Change</h3>
<p>Thanks to <a href="https://github.com/w3c/csswg-drafts/issues/12289#issuecomment-3206223182">a recent change in the overflow specification</a> that allows containers to be a scroller for only a single axis, <code>position: sticky</code> can now track two different scrollers — one per axis — for the stickiness.</p>
<p>This means that for our wide data table:</p>
<ul>
<li>The first column can be sticky on the horizontal axis against the table wrapper.</li>
<li>The headers can be sticky on the vertical axis against a completely different scroller — such as the document itself!</li>
</ul>
<p>No more duplicating headers. No more scroll-syncing JavaScript. Just plain CSS doing exactly what you&#8217;d expect it to do.</p>
<p>This change is available for testing in Chrome 148 with the Experimental Web Platform Features flag enabled.</p>
<div class="note">
<p>The request for this change was filed back in 2017, in CSS WG Issue <a href="https://github.com/w3c/csswg-drafts/issues/865">#865</a>. It’s not that often you see three-digit issues get resolved and implemented 😅</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#see-it-in-action" name="see-it-in-action">#</a> See it in action</h3>
<p>You can see this new behavior in action in this CodePen demo:</p>
<p class="codepen" data-height="760" data-pen-title="CSS `position: sticky` for Single Axis Scroll Containers" data-default-tab="result" data-slug-hash="VYKQwmK" data-user="bramus" style="height: 760px; 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/VYKQwmK"><br />
  CSS `position: sticky` for Single Axis Scroll Containers</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>If you view the demo in a supported browser, the first column will stick to the left edge of the table wrapper when scrolling horizontally, and the top headers will stick to the top of the viewport when scrolling vertically.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#an-important-detail-watch-your-overflow" name="an-important-detail-watch-your-overflow">#</a> An Important Detail: Watch your overflow</h3>
<p>If you look closely at the CSS in the demo, you might notice a very specific detail in how the <code>.table-wrapper</code> is styled.</p>
<pre><code class="language-css">.table-wrapper {
  overflow: auto clip;
}</code></pre>
<p>To make the wrapper scroll horizontally, you might instinctively reach for <code>overflow-x: scroll</code> (or <code>auto</code>). However, doing so will actually break the vertical stickiness we just achieved!</p>
<p>Why? Because in CSS, if you set <code>overflow-x</code> to <code>scroll</code> or <code>auto</code>, the browser automatically computes <code>overflow-y</code> to <code>auto</code> as well (assuming its value was <code>visible</code>). This turns the wrapper into a scroll container on the vertical axis too, which means it once again traps our headers and becomes their sticky reference.</p>
<p>To fix this, we need to explicitly tell the browser <em>not</em> to create a scroll container on the block axis. We do this by using the <code>clip</code> keyword:</p>
<pre><code class="language-css">.table-wrapper {
  /* ❌ This makes overflow-y compute to auto, breaking vertical stickiness */
  /* overflow-x: auto; */

  /* ✅ This creates a scroller on the inline axis, but clips the block axis */
  overflow: auto clip; 
}</code></pre>
<p>By setting <code>overflow-y</code> to <code>clip</code> (via the <code>overflow</code> shorthand), the wrapper does not become a scrollport on the vertical axis. The headers are therefore free to track the document viewport instead.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#browser-support" name="browser-support">#</a> Browser Support</h3>
<div class="note note--supergreen">
<p>💡 Although this post was originally published in March 2026, the section below is constantly being updated. <em>Last update: March 30, 2026</em>.</p>
</div>
<p>At the time of writing, this feature is only supported in Chrome 148 as an experimental feature.</p>
<dl>
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>👨‍🔬 Available in Chromium 148.0.7742.0 with the experimental web platform features flag flipped on.</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>❌ No support</p>
<p>Subscribe to <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2023702">Bug #2023702</a> for updates.</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>❌ No support</p>
</dd>
</dl>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#feature-detection" name="feature-detection">#</a> Feature Detection</h3>
<p>Because older browsers still use the inner container as the sticky reference for both axes, you might want to feature-detect this new behavior to provide JS-based fallbacks or conditionally load specific styles.</p>
<p>While we can&#8217;t easily detect this layout behavior directly with an <code>@supports</code> query in CSS yet — I have an <a href="https://github.com/w3c/csswg-drafts/issues/13677">open issue at the CSSWG for this</a> — you can feature detect this using JavaScript. The <a href="https://codepen.io/bramus/full/VYKQwmK">CodePen demo linked above</a> includes a script that does exactly this.</p>
<p>It handles the detection by evaluating how the browser resolves the sticky positioning offsets within a scrolling wrapper. Feel free to peek at the JS tab in the Pen to grab the snippet for your own projects.</p>
<div class="update">
<p><strong>UPDATE 2026.05.27</strong> The <a href="https://github.com/w3c/csswg-drafts/issues/13677#issuecomment-4556743599">resolution at the CSSWG is in</a> and Chrome Engineering is already busy implementing it in Chromium with the CL getting merged as I type this.</p>
<p>Thanks to this addition, detection is now possible in CSS using the following snippet:</p>
<pre><code class="language-css">@supports named-feature(single-axis-scroll-container) {
  /* Single Axis Scroll Containers are supported */
}</code></pre>
<p>This detection mechanism is available starting with Chrome 150, paving the way to shipping Single-Axis Scroll Containers to Chrome Stable later this year.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#spread-the-word" name="spread-the-word">#</a> Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3mibo3clru22q">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/116318130490039355">🦣 Mastodon</a></li>
<p>  <!-- 

<li>💼 LinkedIn</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/03/30/css-sticky-per-axis/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/03/sticky-per-axis-2.mp4" length="1589051" type="video/mp4" />

			</item>
		<item>
		<title>Cranking View Transtions up to 11 (2026.03.25 @ devs.gent)</title>
		<link>https://www.bram.us/2026/03/25/cranking-view-transtions-up-to-11-2026-03-25-devs-gent/</link>
					<comments>https://www.bram.us/2026/03/25/cranking-view-transtions-up-to-11-2026-03-25-devs-gent/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Wed, 25 Mar 2026 16:30:48 +0000</pubDate>
				<category><![CDATA[Elsewhere]]></category>
		<category><![CDATA[public speaking]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36235</guid>

					<description><![CDATA[Talk on View Transitions, given at the devs.gent March meetup]]></description>
										<content:encoded><![CDATA[<p>On Mar 25, 2026 I gave a talk at a <a href="https://www.meetup.com/devs-gent/events/312985269/">devs.gent Meetup</a>. The talk was called “Cranking View Transtions up to 11” and explored the more adventurous side 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>I also gave this talk at Beyond Tellerrand 2026 about a month later. You can find the slides of the talk <a href="https://www.bram.us/2026/06/25/cranking-view-transtions-up-to-11-2026-04-28-beyond-tellerrand/">in the Beyond Tellerrand 2026 post</a>.</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/03/25/cranking-view-transtions-up-to-11-2026-03-25-devs-gent/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title>More Easy Light-Dark Mode Switching: light-dark() is about to support images!</title>
		<link>https://www.bram.us/2026/03/19/more-easy-light-dark-mode-switching-light-dark-is-about-to-support-images/</link>
					<comments>https://www.bram.us/2026/03/19/more-easy-light-dark-mode-switching-light-dark-is-about-to-support-images/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 19 Mar 2026 11:24:41 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[dark mode]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36160</guid>

					<description><![CDATA[<p>CSS <strong><code>light-dark()</code> is being extended to support images.</strong></p>]]></description>
										<content:encoded><![CDATA[<!-- Added by Post/Page Specific Custom Code plugin, thank you for using! -->
<style>/* DL GRID @src https://codepen.io/bramus/pen/POEaXg*/
@media(min-width: 30em) {
dl {
	display: grid;
	grid-template: auto / 10em 1fr;
}

dt {
  grid-column: 1;
}

dd {
  grid-column: 2;
}

dt, dd {
	margin: 0;
	padding: .3em .5em;
	border-top: 1px solid rgba(0,0,0,.1);
}

:is(dt, dd) > :first-child {
  margin-top: 0;
}
:is(dt, dd) > :last-child {
  margin-bottom: 0;
}
}</style>
<p><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/03/light-dark-image.png" alt="" width="560" height="264" class="alignnone size-medium wp-image-36172" /></p>
<div class=intro>
<p>Back in 2023, I wrote about <a href="https://www.bram.us/2023/10/09/the-future-of-css-easy-light-dark-mode-color-switching-with-light-dark/">the future of CSS color switching</a> using the then-novel <code>light-dark()</code> function. It was a game-changer for colors, allowing us to ditch the repetitive <code>@media (prefers-color-scheme: ...)</code> blocks for simple property declarations.</p>
<p>But there was one glaring limitation: it only works for colors. If you wanted to swap out a background image, a mask, or a logo based on the user&#8217;s color scheme, you were stuck doing things the &#8220;old&#8221; way.</p>
<p>Well, I have good news. The spec has been updated, and <strong><code>light-dark()</code> is being extended to support images.</strong></p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#missing-piece" name="missing-piece">#</a> The Missing Piece</h3>
<p>As a recap, in CSS, you have to write something like this if you want to set background-images for light and dark mode:</p>
<pre><code class="lang-css" style="tab-size: 2">:root {
  --bg-image: url(light-pattern.png);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-image: url(dark-pattern.png);
  }
}

.element {
  background-image: var(--bg-image);
}</code></pre>
<p>This code has downsides: The necessary parts can be scattered all over the place and it only checks the global color-scheme preference without respecting local color-scheme overrides done with <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/color-scheme">the <code>color-scheme</code> CSS property</a>.</p>
<p>Thanks CSS <a href="https://drafts.csswg.org/css-color-5/#light-dark"><code>light-dark()</code></a> we can keep our code closely together, and can respond to the local used <code>color-scheme</code> value. In it’s updated form – in which <code>light-dark()</code> now also supports <code>&lt;image&gt;</code> values – that whole block can be collapsed into a single rule, and we can make it respect local <code>color-scheme</code> overrides:</p>
<pre><code class="lang-css" style="tab-size: 2">.element {
  color-scheme: dark;
  background-image: light-dark(url(light-pattern.png), url(dark-pattern.png));
}</code></pre>
<p>Sweet!</p>
<div class=note>
<p>Note that you must pass in either two <code>&lt;color&gt;</code> values or two <code>&lt;image&gt;</code> values as arguments to <code>light-dark()</code> … you can’t mix the types — see <a href="#other-values">further down</a> for an explanation why that is.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#browser-support" name="browser-support">#</a> Browser Support</h3>
<div class="note note--supergreen">
<p>💡 Although this post was originally published in March 2026, the section below is constantly being updated. <em>Last update: May 26, 2026</em>.</p>
</div>
<p>It’s early days, but the engines are already moving:</p>
<dl>
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>✅ Supported in Chrome 150. Chrome 150 is expected to go stable on Jun 17, 2026</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>✅ Supported in Firefox 150</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>❌ No support</p>
<p>Subscribe to <a href="https://bugs.webkit.org/show_bug.cgi?id=309689">WebKit Bug #309689</a> for updates.</p>
</dd>
</dl>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#feature-detection" name="feature-detection">#</a> Feature Detection</h3>
<p>If you want to start experimenting today while providing a fallback, you can use <code>@supports</code>. To detect support for images specifically, you can test it using <code>linear-gradient()</code> (which is treated as an <code>&lt;image&gt;</code> in CSS) or — another new addition — the keyword <code>none</code>:</p>
<pre><code class="lang-css" style="tab-size: 2">@supports (background-image: light-dark(none, none)) {
  /* Modern image-switching logic here */
}</code></pre>
<p>You can see the code in action this CodePen:</p>
<p class="codepen" data-height="590" data-pen-title="CSS light-dark(&amp;lt;image&amp;gt;, &amp;lt;image&amp;gt;) Support test" data-default-tab="result" data-slug-hash="bNwRmgm" data-user="bramus" style="height: 590px; 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/bNwRmgm">CSS light-dark(&lt;image&gt;, &lt;image&gt;) Support test</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>) 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><a href="#other-values" name="other-values">#</a> What about non-<code>&lt;color&gt;</code> and non-<code>&lt;image&gt;</code> values?</h3>
<p>Supporting non-<code>&lt;color&gt;</code> and non-<code>&lt;image&gt;</code> values still is an unanswered question (that probably won’t get solved). As explained in my original coverage, CSS can’t just simply accept <code>light-dark()</code> anywhere because <a href="https://www.w3.org/TR/css-syntax-3/#parse-grammar">the parser needs to know the value type of what it is parsing</a> ahead of time.</p>
<p>Internally, <code>light-dark()</code> is <a href="https://github.com/w3c/csswg-drafts/issues/12513#issuecomment-4085662455">about to be defined</a> as having two internal variants – one that accepts <code>&lt;color&gt;</code>s and one that accepts <code>&lt;image&gt;</code>s:</p>
<pre><code>light-dark() =  &#x3C;light-dark-color&#x3E; | &#x3C;light-dark-image&#x3E;
&#x3C;light-dark-color&#x3E; = light-dark(&#x3C;color&#x3E;, &#x3C;color&#x3E;)
&#x3C;light-dark-image&#x3E; = light-dark(&#x3C;image&#x3E;, &#x3C;image&#x3E;)</code></pre>
<p>Each variant has limitations on where it can be used: the version that does colors is only accepted where <code>&lt;color&gt;</code>s are allowed, and the version that does images is only accepted where <code>&lt;image&gt;</code>s are allowed.</p>
<p><pre><code>&#x3C;color&#x3E; = &#x3C;color-base&#x3E; | currentColor | &#x3C;system-color&#x3E; | 
          &#x3C;contrast-color()&#x3E; | &#x3C;device-cmyk()&#x3E; | <b>&#x3C;light-dark-color&#x3E;</b></code></pre>
<pre><code>&#x3C;image&#x3E; = &#x3C;url&#x3E; | &#x3C;image()&#x3E; | &#x3C;image-set()&#x3E; | &#x3C;cross-fade()&#x3E; | 
          &#x3C;element()&#x3E; | &#x3C;gradient&#x3E; | <b>&#x3C;light-dark-image&#x3E;</b></code></pre>
<p>If more types of values needed to be supported, that would required many more internal variants.</p>
<p>This split into two variants allows the CSS parser to discard invalid declarations at <em>parse time</em>. That, in turn, explains why you can’t mix the types in the arguments. Say CSS were to accept a mix of <code>&lt;color&gt;</code> and <code>&lt;image&gt;</code>, then you would be able declare something like <code>background: red light-dark(blue, url(dark.png));</code>. With a dark <code>color-scheme</code> that declaration would end up being OK, but in a light <code>color-scheme</code> you’d end up with <code>background: red blue</code> which is invalid.</p>
<h4>Looking ahead: <code>@function</code> + <code>color-scheme()</code> to the rescue!</h4>
<p>In the future there will be an way to have non-<code>&lt;color&gt;</code> and non-<code>&lt;image&gt;</code> <code>color-scheme</code>-dependent values in the future: using a CSS Custom Function and <code>color-scheme()</code>. Go check out my previous post on <a href="https://www.bram.us/2025/09/30/css-custom-light-dark/">implementing a custom <code>--light-dark()</code> function that works with <em>any</em> type of value</a> for the details. It’s just 3 lines of code, which I’ve included here as well:</p>
<pre><code class="language-css" style="tab-size: 2">@function --light-dark(--l, --d) {
  result: if(color-scheme(dark): var(--d); else: var(--l));
}</code></pre>
<p>To feature detect support for <code>@function</code>, <a href="https://www.bram.us/2026/03/15/at-rule/">check out <code>@supports at-rule()</code></a>. There currently is no easy way to detect support for <code>color-scheme()</code>, unless <a href="https://codepen.io/bramus/pen/bNEWZLV/2f99187f31ee53f87a1b48bb083cb41e">you want to jump through some hoops</a>.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#spread-the-word" name="spread-the-word">#</a> Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3mhfy336xs22w">🦋 BlueSky</a></li>
<li><a href="https://front-end.social/@bramus/116255783077151930">🦣 Mastodon</a></li>
<li><a href="https://www.linkedin.com/feed/update/urn:li:activity:7440377308567322625/">💼 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/03/19/more-easy-light-dark-mode-switching-light-dark-is-about-to-support-images/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title>Detect at-rule support in CSS with @supports at-rule(@keyword)</title>
		<link>https://www.bram.us/2026/03/15/at-rule/</link>
					<comments>https://www.bram.us/2026/03/15/at-rule/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Sun, 15 Mar 2026 19:51:22 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[feature detection]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36134</guid>

					<description><![CDATA[<p>Back in January 2022, I <a href="https://brm.us/at-rule-2022">wrote</a> about an exciting new CSS Working Group decision: a function to detect at-rule support using <code>@supports at-rule(@keyword)</code>. Fast forward to today, and the CSS Conditional Rules Module Level 5 specification has solidified how this feature works and Chromium (Chrome, Edge, etc.) is about to ship it in Chromium 148!</p>]]></description>
										<content:encoded><![CDATA[<!-- Added by Post/Page Specific Custom Code plugin, thank you for using! -->
<style>/* DL GRID @src https://codepen.io/bramus/pen/POEaXg*/
@media(min-width: 30em) {
dl {
	display: grid;
	grid-template: auto / 10em 1fr;
}

dt {
  grid-column: 1;
}

dd {
  grid-column: 2;
}

dt, dd {
	margin: 0;
	padding: .3em .5em;
	border-top: 1px solid rgba(0,0,0,.1);
}

:is(dt, dd) > :first-child {
  margin-top: 0;
}
:is(dt, dd) > :last-child {
  margin-bottom: 0;
}
}</style>
<p><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/03/at-rule.png" alt="" width="560" height="336" class="alignnone size-medium wp-image-36142" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/03/at-rule.png 2333w, https://www.bram.us/wordpress/wp-content/uploads/2026/03/at-rule-560x336.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/03/at-rule-1120x672.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/03/at-rule-768x461.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/03/at-rule-1536x922.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/03/at-rule-2048x1229.png 2048w, https://www.bram.us/wordpress/wp-content/uploads/2026/03/at-rule-1568x941.png 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /></p>
<div class="intro">
<p>Back in January 2022, I <a href="https://brm.us/at-rule-2022">wrote</a> about an exciting new CSS Working Group decision: a function to detect at-rule support using <code>@supports at-rule(@keyword)</code>. Fast forward to today, and the CSS Conditional Rules Module Level 5 specification has solidified how this feature works and Chromium (Chrome, Edge, etc.) is about to ship it in Chromium 148!</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The solution (Updated)</h3>
<p>The core idea remains the same as back in 2022: use the <code>at-rule()</code> function within an <code>@supports</code> query to check if a user agent recognizes a specific at-rule.</p>
<pre><code class="language-css">@supports at-rule(@starting-style) {
  /* CSS for browsers that support @starting-style here … */
}</code></pre>
<p>The function simply checks if the browser would accept an at-rule in any context. This is super useful for detecting entire new features like <code>@starting-style</code>.</p>
<p>You can also use <code>at-rule()</code> in the <em>supports-condition</em> when doing an <code>@import</code>:</p>
<pre><code class="language-css">/* This CSS only gets imported when @view-transition is supported (and the browser supports at-rule() as well) */
@import "view-transitions.css" supports(at-rule(@view-transition));</code></pre>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>No descriptors, preludes, or full blocks!</h3>
<p>In my <a href="https://brm.us/at-rule-2022">original coverage of <code>at-rule()</code></a>, I mentioned an extension to the feature that would theoretically allow you to detect support for specific descriptors (e.g. <code>at-rule(@counter-style; system: fixed)</code>) or even pass in a full at-rule block.</p>
<p>This extension — along with checking for at-rule <em>preludes</em> (= the part between the at-rule and the opening <code>{</code>) — <a href="https://github.com/w3c/csswg-drafts/issues/6966#issuecomment-3205037703">has been dropped</a>. The following examples will all fail, as those are not supported:</p>
<pre><code class="language-css">/* ❌ Passing in descriptors is not supported */
@supports at-rule(@counter-style; system: fixed) { … }

/* ❌ Passing in full at-rules is not supported */
@supports at-rule(@counter-style { system: fixed }) { … }

/* ❌ Passing in preludes of at-rules is not supported */
@supports at-rule(@container style(…)) { … }</code></pre>
<p>Because preludes are not allowed in <code>at-rule()</code>, it cannot be used to detect non-size <code>@container</code> queries support <em>(e.g. style queries, anchored queries, etc.)</em>. To detect those, fall back to sniffing out support for the various values of <code>container-type</code> — a technique you can already use today in all browsers:</p>
<pre><code class="language-css">@supports (container-type: anchored) {
  /* CSS for browsers that support anchored queries here … */
}</code></pre>
<p>To feature detect style queries — which has no tell-tale property/value pair — you can use <a href="https://www.bram.us/2024/10/06/feature-detect-style-queries-support-in-css/">this workaround I previously documented</a>:</p>
<pre><code class="language-css" style="tab-size: 2">html {
  --sentinel: 1;
}

@container style(--sentinel: 1) {
  /* Style Queries Supported! */
}</code></pre>
<div class="note">
<p>Note that you also can’t pass <code>@charset</code> into <code>at-rule()</code>, as it technically is <a href="https://drafts.csswg.org/css-syntax/#charset-rule">not an at-rule</a>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Browser Support</h3>
<p><code>@supports at-rule(@keyword)</code> is available in Chromium 148. The initial implementation was done <a href="https://chromium-review.googlesource.com/5972643">by Google</a> and Kevin from Microsoft <a href="https://chromium-review.googlesource.com/7037634">pushed it over the finish line</a> while also navigation the paperwork with the CSS Working Group.</p>
<dl>
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>✅ Supported in Chromium 148 and up.</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>❌ No support</p>
<p>Follow <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1751188">Issue #1751188</a> to stay up-to-date and signal interest.</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>❌ No support</p>
<p>Subscribe to <a href="https://bugs.webkit.org/show_bug.cgi?id=235400">Issue #235400</a> to stay up-to-date and signal interest.</p>
</dd>
</dl>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Feature Detection</h3>
<p>You can feature detect support for <code>at-rule()</code> by checking for support for <code>@supports</code> itself.</p>
<pre><code class="language-css">@supports at-rule(@supports) {
  /* ✅ The browser supports `@supports at-rule(…)` */
}</code></pre>
<p>You can see it in action in the following demo:</p>
<p class="codepen" data-height="600" data-pen-title="CSS @supports at-rule() test" data-default-tab="result" data-slug-hash="LERWgbe" data-user="bramus" style="height: 600px; 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/LERWgbe"><br />
  CSS @supports at-rule() 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>
<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/03/15/at-rule/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
