<?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>Thu, 22 Oct 2026 17:05:32 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>
	<item>
		<title>GTA.js, a reimplementation of the 1997 Grand Theft Auto engine in JavaScript and WebGL</title>
		<link>https://www.bram.us/2026/10/22/gta-js/</link>
					<comments>https://www.bram.us/2026/10/22/gta-js/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 22 Oct 2026 17:05:32 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[gta]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[webgl]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36982</guid>

					<description><![CDATA[Here’s a little sideproject I’ve been spending my tokens on: <a href="https://gtajs.wtf/">https://gtajs.wtf/</a>

It’s a reimplementation of the 1997 Grand Theft Auto engine in JavaScript and WebGL.]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/10/IMG_9723.png"><img fetchpriority="high" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/10/IMG_9723.png" alt="" width="560" height="294" class="alignnone size-medium wp-image-36986" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/10/IMG_9723.png 1200w, https://www.bram.us/wordpress/wp-content/uploads/2026/10/IMG_9723-560x294.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/10/IMG_9723-1120x588.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/10/IMG_9723-768x403.png 768w" sizes="(max-width: 560px) 100vw, 560px" /></a></p>
<p>Here’s a little sideproject I’ve been spending my tokens on: <a href="https://gtajs.wtf/">https://gtajs.wtf/</a></p>
<p>It’s a reimplementation of the 1997 Grand Theft Auto engine in JavaScript and WebGL.</p>
<p>The project is not entirely finished, but since I am leaving on vacation right now (I am currently at the airport), I might as well just publish what I have — and it&#8217;s already quite a lot I think 😀</p>
<p>Note that because this is a reimplementation (it’s not a DOSBox wrapper / emulator / decompiled version / …) there are differences compared to the original 1997 game in certain physics behaviors, animations, etc.</p>
<p>The mission engine is accurate though, as that is publicly documented (the almighty mtd.doc) 😊</p>
<p>And yes, I totally asked Gemini to implement most things, referring Gemini to open source reimplementations, public documentation and community notes, or by simply telling it how I recall the game from playing it (and editing it!) a lot back in the day.</p>
<p>Still a lot of things to do (cops!) but it’s already playable. Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/10/22/gta-js/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Scroll-Triggered Animations Debugger (DevTools Extension)</title>
		<link>https://www.bram.us/2026/10/14/scroll-triggered-animations-debugger-devtools-extension/</link>
					<comments>https://www.bram.us/2026/10/14/scroll-triggered-animations-debugger-devtools-extension/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Wed, 14 Oct 2026 17:27:10 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[devtools]]></category>
		<category><![CDATA[extension]]></category>
		<category><![CDATA[scroll-triggered animations]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36933</guid>

					<description><![CDATA[<p>I’m extremely pleased to announce the release of <a href="https://chromewebstore.google.com/detail/ganooodmdnigcfdddlijnhkipjklbgag">Scroll-Triggered Animations Debugger</a>, a DevTools extension to visualize and debug <a href="https://developer.chrome.com/blog/scroll-triggered-animations">CSS Scroll-Triggered Animations</a>.</p>]]></description>
										<content:encoded><![CDATA[<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/10/scroll-triggered-animations-debugger-extension-scaled.png"><img decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/10/scroll-triggered-animations-debugger-extension-560x364.png" alt="" width="560" height="364" class="alignnone size-medium wp-image-36934" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/10/scroll-triggered-animations-debugger-extension-560x364.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/10/scroll-triggered-animations-debugger-extension-1120x728.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/10/scroll-triggered-animations-debugger-extension-768x499.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/10/scroll-triggered-animations-debugger-extension-1536x998.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/10/scroll-triggered-animations-debugger-extension-2048x1331.png 2048w, https://www.bram.us/wordpress/wp-content/uploads/2026/10/scroll-triggered-animations-debugger-extension-1568x1019.png 1568w" sizes="(max-width: 560px) 100vw, 560px" /></a><figcaption>Screenshot of the Scroll-Triggered Animations Debugger Extension fopen in DevTools. The extension shows a minimap, the ranges, and fields to edit the ranges.</figcaption></figure>
<div class="intro">
<p>I’m extremely pleased to announce the release of <a href="https://chromewebstore.google.com/detail/ganooodmdnigcfdddlijnhkipjklbgag">Scroll-Triggered Animations Debugger</a>, a DevTools extension to visualize and debug <a href="https://developer.chrome.com/blog/scroll-triggered-animations">CSS Scroll-Triggered Animations</a>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<navigation>
<h3>Table of Contents</h3>
<ol>
<li><a href="#demo">Demo</a></li>
<li><a href="#download">Get the extension</a></li>
<li><a href="#backstory">Backstory</a></li>
<li><a href="#issues-limitations">Known Issues and Limitations</a></li>
<li><a href="#future">Future Plans</a></li>
<li><a href="#spread-the-word">Spread the word</a></li>
</ol>
</navigation>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#demo" name="demo">#</a> Demo</h3>
<p>To jump right in, here’s a video of the extension in action:</p>
<figure>
  <iframe width="560" height="315" src="https://www.youtube.com/embed/C5hn16x31I8?si=xsgA8h0NuUFy14It" 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><figcaption>Recording of the Scroll-Triggered Animations Debugger in action.</figcaption></figure>
<p>The extension adds a new pane named “Scroll-Triggered Animations” to Chrome DevTools’s Elements Panel and offer the following debugging features:</p>
<ul>
<li>Visualize the Scroll-Triggered Animation’s scroller, element, and subject in an interactive minimap</li>
<li>Supports both <code>ScrollTimeline</code> (<code>scroll()</code>) and <code>ViewTimeline</code> (<code>view()</code>).</li>
<li>Shows an active/inactive status light indicator that switches from red (Inactive) to green (Active) when the trigger activates.</li>
<li>Plays nice with <code>position: sticky</code>.</li>
<li>Visualize the <code>timeline-trigger-activation-range</code> (blue) and <code>timeline-trigger-active-range</code> (green) debug lines and boxes.</li>
<li>Edit the <code>timeline-trigger-activation-range</code> and <code>timeline-trigger-active-range</code> values live.</li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#download" name="download">#</a> Get the extension</h3>
<p>The extension is <a href="https://chromewebstore.google.com/detail/ganooodmdnigcfdddlijnhkipjklbgag" rel="nofollow">available on the Chrome Web Store</a>:</p>
<div class="table">
<div style="border: 1px solid #ccc;"><a href="https://chromewebstore.google.com/detail/ganooodmdnigcfdddlijnhkipjklbgag" rel="nofollow"><img decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2023/09/chrome-webstore.svg" alt="Available on the Chrome Web Store" style="max-width: 100%; display: block;"></a></div>
</div>
<p>If you like this extension, please leave a review on the Chrome Web Store.</p>
<p>There is also <a href="https://github.com/bramus/scroll-triggered-animations-debugger-extension/">a repository on GitHub</a> where you can report issues and file feature requests.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#backstory" name="backstory">#</a> Backstory</h3>
<p>While exploring CSS Scroll-Triggered Animations earlier, I built a set of demos that included an in-page debug overlay to help me make sense of what was going on, such as <a href="https://codepen.io/bramus/pen/bNpzXrV">this debug demo on CodePen</a>:</p>
<p class="codepen" data-height="677" data-pen-title="Scroll-Triggered Animations: Trigger Ranges Visualizer" data-default-tab="result" data-slug-hash="bNpzXrV" data-user="bramus" style="height: 677px; 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/bNpzXrV">Scroll-Triggered Animations: Trigger Ranges Visualizer</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>That demo visualizes the activation range <em>(in blue)</em>, the active range <em>(in green)</em>, and also includes a little status light that switches from red <em>(Inactive)</em> to green <em>(Active)</em> whenever the subject crosses the trigger boundaries.</p>
<p>Back in 2023, I had built the <a href="https://www.bram.us/2023/09/12/scroll-driven-animations-debugger/">Scroll-Driven Animations Debugger DevTools Extension</a> which solved a very similar problem for Scroll-Driven Animations by adding a minimap and range editor to Chrome DevTools’s Elements Panel.</p>
<p>So I fired up Antigravity, pointed it to the source code of my Scroll-Driven Animations Debugger extension along with the Scroll-Triggered Animations visualizations I had explored on CodePen, and tasked it with combining the two into a dedicated DevTools extension for Scroll-Triggered Animations.</p>
<p>Because the architecture for communicating between the inspected page and the DevTools sidebar pane was already figured out in the Scroll-Driven Animations Debugger, and the visualization was already prototyped in the CodePen, Gemini had all the info it needed to build this new Scroll-Triggered Animations extension in no time 🙂</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#issues-limitations" name="issues-limitations">#</a> Known Issues and Limitations</h3>
<p>While the extension is mature enough to be released as version 1.0.0, there are a few things I could not work around due to browser and API limitations. These are collected in <a href="https://github.com/bramus/scroll-triggered-animations-debugger-extension/issues?q=is%3Aissue+label%3AFAQ+">the FAQ for the extension</a>:</p>
<ul>
<li>
<p><a href="https://github.com/bramus/scroll-triggered-animations-debugger-extension/issues/1">Issue #1</a>: Ranges declared with relative or viewport units — such as <code>90vh</code> — get reported as calculated pixel values in the Range Editor.</p>
<p>This is not a bug in the extension, but a limitation of how the browser exposes these values: both the <code>Animation</code> instance and <code>getComputedStyle($element)</code> resolve specified relative values into computed <code>px</code> values. Short of parsing the raw stylesheets and running my own CSS cascade, there is no API to retrieve the authored units.</p>
<p>Ideally, I want to have a function (in DevTools, but preferably DOM) that allows me to get the specified styles for an element. This request is tracked in <a href="https://github.com/w3c/csswg-drafts/issues/10002">w3c/csswg-drafts#10002</a> and <a href="https://issues.chromium.org/issues/447469710">crbug//447469710</a></p>
</li>
<li style="margin-top: 1em">
<p><a href="https://github.com/bramus/scroll-triggered-animations-debugger-extension/issues/2">Issue #2</a> and <a href="https://github.com/bramus/scroll-triggered-animations-debugger-extension/issues/3">Issue #3</a>: The extension doesn’t work with elements inside an <code>iframe</code>, either showing a <em>“Could not read the required data from the element.”</em> error or falsely claiming that there are no Scroll-Triggered Animations attached to the element even though there are.</p>
<p>This is caused by a bug in Chrome DevTools <em>(<a href="https://crbug.com/1480418">crbug/1480418</a>)</em>: when querying <code>$0</code> via <code>chrome.devtools.inspectedWindow.eval</code> for an element inside an <code>iframe</code>, DevTools either reports <code>undefined</code> or lies and returns the last selected node from outside the <code>iframe</code>. Because of this browser limitation, the extension won’t work on CodePen’s Editor View directly — you’ll need to open the demo in CodePen’s standalone Debug View first.</p>
</li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#future" name="future">#</a> Future Plans</h3>
<p>The short-term plan is to fix any bugs that might surface once people start using the extension and look into a few <a href="https://github.com/bramus/scroll-triggered-animations-debugger-extension/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement">planned enhancements</a> <em>(feel free to file an idea <a href="https://github.com/bramus/scroll-triggered-animations-debugger-extension/issues">in the repo</a>)</em>.</p>
<p>Just like with <a href="https://brm.us/sda-debugger">the Scroll-Driven Animations Debugger Extension I built</a>, the long-term plan is for this extension to eventually become obsolete once Chrome DevTools gets built-in debugging support for Scroll-Triggered Animations. Until then, this extension exists to fill that gap.</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/3mxu4zkxjhs2a">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117440578962669338">🦣 Mastodon</a></li>
<li><a href="https://lnkd.in/p/eszBgxFq">💼 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/10/14/scroll-triggered-animations-debugger-devtools-extension/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Introducing css-typed-om, a complete and WPT-tested polyfill for CSS Typed OM</title>
		<link>https://www.bram.us/2026/10/13/introducing-css-typed-om-a-complete-and-wpt-tested-polyfill-for-css-typed-om/</link>
					<comments>https://www.bram.us/2026/10/13/introducing-css-typed-om-a-complete-and-wpt-tested-polyfill-for-css-typed-om/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 08:51:47 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[css]]></category>
		<category><![CDATA[polyfill]]></category>
		<category><![CDATA[typed om]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36944</guid>

					<description><![CDATA[<p>Being a big fan of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Typed_OM_API">CSS Typed OM</a>, I’ve wanted a proper polyfill for it for years. Just before my summer vacation back in July, with the help of Gemini/Antigravity, I finally built one: <a href="https://github.com/bramus/css-typed-om-polyfill"><code>css-typed-om</code></a>.</p>]]></description>
										<content:encoded><![CDATA[<figure>
	<a href="https://www.bram.us/wordpress/wp-content/uploads/2026/10/css-typed-om-polyfill.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/10/css-typed-om-polyfill.png" alt="" width="560" height="342" class="alignnone size-medium wp-image-36947" /></a><figcaption>Code example showing the CSS Typed OM, with the <code>css-typed-om</code> polyfill loaded on the first line.</figcaption></figure>
<div class="intro">
<p>Being a big fan of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Typed_OM_API">CSS Typed OM</a>, I’ve wanted a proper polyfill for it for years. Just before my summer vacation back in July, with the help of Gemini/Antigravity, I finally built one: <a href="https://github.com/bramus/css-typed-om-polyfill"><code>css-typed-om</code></a>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-problem" name="the-problem">#</a> Why CSS Typed OM? (And why a polyfill?)</h3>
<p>If you’ve ever read or manipulated styles in JavaScript using <code>element.style</code> or <code>window.getComputedStyle()</code>, you know the pain of dealing with strings. Want to read an element’s <code>width</code> and add <code>20px</code> to it? You have to parse the string, strip the unit, convert it to a number, do the math, and concatenate it back into a string. Throw <code>calc()</code>, <code>transform</code> matrices, or modern color spaces into the mix, and string manipulation quickly falls apart.</p>
<pre><code class="language-js">const styles = getComputedStyle($el);
const opacity = styles.getPropertyValue('opacity');

console.log(opacity); // '0.2'
console.log(typeof opacity); // String!?</code></pre>
<p>That’s where the <a href="https://developer.chrome.com/docs/css-ui/cssom">CSS Typed Object Model (CSS Typed OM)</a> comes in. Instead of strings, it exposes CSS values as typed JavaScript objects — such as <code>CSSUnitValue</code>, <code>CSSMathSum</code>, <code>CSSTransformValue</code>, and <code>CSSKeywordValue</code> — through <code>element.attributeStyleMap</code>, <code>element.computedStyleMap()</code>, and unit factory functions like <code>CSS.px(20)</code>.</p>
<pre><code class="language-js">const styles = $el.computedStyleMap();
const opacity = styles.get('opacity');

console.log(opacity); // CSSUnitValue
console.log(opacity.value); // 0.2
console.log(opacity.unit); // 'number</code></pre>
<p>However, browser support isn’t 100% uniform yet. While Chromium and Safari ship CSS Typed OM, Firefox’s native implementation is still a work in progress behind the <code>layout.css.typed-om.enabled</code> flag (see <a href="https://bugzil.la/1278697">Firefox bug 1278697</a>). And even in browsers that do support Typed OM, some newer parts of the spec — like the <code>CSSColorValue</code> subclasses (<code>CSSRGB</code>, <code>CSSOKLCH</code>, etc.) — aren’t available everywhere yet.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#enter-css-typed-om" name="enter-css-typed-om">#</a> Enter <code>css-typed-om</code></h3>
<p>Building a standalone CSS Typed OM polyfill has been sitting on my backlog for years — especially when seeing other projects like the <em>(partial)</em> <a href="https://github.com/flackr/scroll-timeline"><code>scroll-timeline</code> polyfill</a> having to roll and bundle parts of the CSS Typed OM implementation themselves just to support unit values like <code>CSS.percent()</code> or <code>CSS.px()</code>.</p>
<p>However, the <a href="https://drafts.css-houdini.org/css-typed-om-1/">CSS Typed OM specification</a> is <em>huge</em>, covering everything from a full CSS tokenizer and <code>calc()</code> expression simplifier to 2D/3D matrix math and WebIDL interface rules. Writing all of that by hand was simply too big of an undertaking for a side project. But now, thanks to AI, that long-standing goal finally came within reach: meet <a href="https://github.com/bramus/css-typed-om-polyfill"><code>css-typed-om</code></a>. Guided by the specification and the official Web Platform Tests, <code>css-typed-om</code> is a complete, standalone, and full tested polyfill that you can drop into any project. 🚀</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#installation-and-usage" name="installation-and-usage">#</a> Installation &amp; Usage</h3>
<p>You can install the package from npm:</p>
<pre style="tab-size: 2"><code class="language-bash">npm install css-typed-om</code></pre>
<p>Depending on your setup, you can use <code>css-typed-om</code> in two ways.</p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>1. As a Global Polyfill</h4>
<p>Import the package once at the entry point of your application to automatically patch <code>window.CSS</code>, <code>Element.prototype.computedStyleMap</code>, <code>HTMLElement.prototype.attributeStyleMap</code>, and all CSS Typed OM classes:</p>
<pre style="tab-size: 2"><code class="language-js">import 'css-typed-om';

// Read computed styles as typed objects
const element = document.querySelector('.my-element');
const width = element.computedStyleMap().get('width');

console.log(width.value); // e.g., 100
console.log(width.unit);  // e.g., 'px'

// Set inline styles using CSS unit factories
element.attributeStyleMap.set('opacity', CSS.number(0.5));
element.attributeStyleMap.set('margin-top', CSS.px(20).add(CSS.em(2)));</code></pre>
<div class="note">
<p>💁‍♂️ Don’t use a bundler? There’s also an IIFE build available that you can drop straight into a page via a <code>&lt;script src="https://unpkg.com/css-typed-om"&gt;&lt;/script&gt;</code> tag.</p>
</div>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>2. As an Explicit ES Module</h4>
<p>If you prefer importing the classes directly (for example in unit tests or modular libraries):</p>
<pre style="tab-size: 2"><code class="language-js">import { CSSUnitValue, CSSKeywordValue } from 'css-typed-om';

const width = new CSSUnitValue(100, 'px');
const display = new CSSKeywordValue('block');</code></pre>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#wpt-tested" name="wpt-tested">#</a> Tested against Web Platform Tests (WPT)</h3>
<p>When building a polyfill for a spec as sprawling as CSS Typed OM, custom unit tests alone aren’t enough. There are hundreds of edge cases around <code>calc()</code> simplification, serialization order, out-of-range value clamping, shorthand properties, and WebIDL interface rules.</p>
<p>To make sure the polyfill behaves just like a native implementation, I wired the repository directly up to the official <a href="https://github.com/web-platform-tests/wpt">Web Platform Tests (WPT)</a> suite. Running <code>npm run test:wpt</code> spins up headless Firefox (with its experimental native <code>layout.css.typed-om.enabled</code> flag explicitly disabled), injects the IIFE build of the polyfill, and runs all 400+ WPT test files in <code>/css/css-typed-om/</code>.</p>
<p>At the time of writing, the polyfill passes <strong>12,479 out of 13,134</strong> WPT subtests (~95%)! 🙂</p>
<div class="note">
<p><strong>🤔 Why not 100%?</strong> To keep the bundle size small instead of shipping a 400KB+ database of every CSS property grammar in existence, the polyfill delegates per-property syntax validation to the host browser’s native CSS parser under the hood. That means if WPT tests a bleeding-edge CSS property or keyword that the host browser itself hasn’t implemented in CSS yet, the browser’s CSS parser rejects it — which is actually what you want in practice, since <code>element.style</code> and <code>getComputedStyle()</code> stay 100% in sync with what the browser supports. An example here is the recent rename of the <code>anchors-valid</code> keyword to <code>anchor-valid</code>, which Firefox 157 which I use for testing does not support.</p>
<p>Additionally, some tests — like those testing <code>currentcolor</code> (or <code>auto</code> / percentages on layout properties like <code>width</code>) via <code>computedStyleMap()</code> — can never pass in a userland polyfill. That’s because <code>computedStyleMap()</code> has to read from <code>window.getComputedStyle()</code>, which returns the resolved value (such as <code>rgb(0, 0, 0)</code>) instead of the keyword <code>currentcolor</code> — leaving the polyfill with no way to know that <code>currentcolor</code> was originally specified.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#acknowledgements" name="acknowledgements">#</a> Acknowledgements &amp; Links</h3>
<p>First of all, a shout-out to <a href="https://github.com/johannesodland">Johannes Odland</a>, who had PR’d some significant CSS Typed OM parts into the <a href="https://github.com/flackr/scroll-timeline"><code>scroll-timeline</code> polyfill</a> back in the day. It’s his work that first sparked the idea of writing a full, standalone CSS Typed OM polyfill.</p>
<p>A special shout-out and thank you goes to <a href="https://github.com/jonathantneal">Jonathan Neal</a>, who kindly handed over the <code>css-typed-om</code> package name on npm <em>(which previously housed the now-archived <a href="https://github.com/csstools/css-typed-om"><code>csstools/css-typed-om</code></a> project)</em>. Thanks, Jonathan! 🙌</p>
<ul>
<li><a href="https://github.com/bramus/css-typed-om-polyfill"><code>css-typed-om</code> on GitHub &rarr;</a></li>
<li><a href="https://www.npmjs.com/package/css-typed-om"><code>css-typed-om</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><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/3mxqo3gtlfk2x">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117432771134987806">🦣 Mastodon</a></li>
<li><a href="https://lnkd.in/p/eHg8C_i5">💼 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/10/13/introducing-css-typed-om-a-complete-and-wpt-tested-polyfill-for-css-typed-om/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>&#x3C;&#x200B;calc-input&#x3E;, a custom input element that accepts mathematical formulas</title>
		<link>https://www.bram.us/2026/10/08/calc-input-a-custom-input-element-that-accepts-mathematical-formulas/</link>
					<comments>https://www.bram.us/2026/10/08/calc-input-a-custom-input-element-that-accepts-mathematical-formulas/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 21:22:43 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[calc]]></category>
		<category><![CDATA[custom elements]]></category>
		<category><![CDATA[forms]]></category>
		<category><![CDATA[input]]></category>
		<category><![CDATA[math]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36908</guid>

					<description><![CDATA[<p>Continuing my streak of small form-related Custom Elements <em>(like <code>&#60;rich-input&#62;</code>)</em>, I built <a href="https://github.com/bramus/calc-input"><code>&#60;calc-input&#62;</code></a>. It’s a custom input element that accepts mathematical formulas — such as <code>2 + 3</code> or <code>(2 + 3) * 4</code> — and automatically toggles between showing the formula on focus and the calculated result on blur.</p>]]></description>
										<content:encoded><![CDATA[<figure><div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36908-1" width="640" height="336" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/10/calc-input.mp4?_=1" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/10/calc-input.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/10/calc-input.mp4</a></video></div><figcaption>Recording of <a href="https://calc-input.netlify.app/">the <code>&lt;calc-input&gt;</code> demo</a>.</figcaption></figure>
<div class="intro">
<p>Continuing my streak of small form-related Custom Elements <em>(like <code>&lt;rich-input&gt;</code>)</em>, I built <a href="https://github.com/bramus/calc-input"><code>&lt;calc-input&gt;</code></a>. It’s a custom input element that accepts mathematical formulas — such as <code>2 + 3</code> or <code>(2 + 3) * 4</code> — and automatically toggles between showing the formula on focus and the calculated result on blur.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-problem" name="the-problem">#</a> The need for a smarter numeric input field</h3>
<p>I’m currently building a web app that has a lot of numeric inputs whose values need to be sourced from a different document. Sometimes filling in a field is a direct copy-paste of a number, but oftentimes there’s a calculation that needs to be done first — like taking one number, dividing it by <code>3</code>, and subtracting another number from it.</p>
<p>With a standard <code>&lt;input&gt;</code> element, doing this means constantly round-tripping to a separate calculator app, crunching the numbers there, and copying the result back into the browser. Even worse: if you come back to the form later to double-check your entries, you only see the final number and have no clue which formula or source numbers produced it 😬</p>
<p>Instead of having to do that calculator dance over and over again, I thought of creating a more powerful input element that lets you type the formula directly into the field: <a href="https://calc-input.netlify.app/"><code>&lt;calc-input&gt;</code></a></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#enter-calc-input" name="enter-calc-input">#</a> Enter <code>&lt;calc-input&gt;</code></h3>
<p><code>&lt;calc-input&gt;</code> is a custom input element that accepts mathematical formulas — such as <code>2 + 3</code> or <code>(2 + 3) * 4</code> — and automatically toggles between showing the formula on focus and the calculated result on blur.</p>
<p>To use it, install <a href="https://www.npmjs.com/package/calc-input"><code>calc-input</code> from npm</a> (or load it via a CDN) and drop the <code>&lt;calc-input&gt;</code> tag right into your HTML <code>&lt;form&gt;</code>:</p>
<pre><code class="language-html" style="tab-size: 2">&lt;script type="module" src="calc-input"&gt;&lt;/script&gt;

&lt;form&gt;
	&lt;calc-input name="size" value="2 + 3"&gt;&lt;/calc-input&gt;
	&lt;button type="submit"&gt;Submit&lt;/button&gt;
&lt;/form&gt;</code></pre>
<p>When focused, the input shows the entered formula (<code>2 + 3</code>). Upon blurring the field, it evaluates the expression and displays the calculated result (<code>5</code>). Re-focusing the field brings back the original formula so you can easily inspect or tweak it 🙂</p>
<p>Out of the box, it supports standard arithmetic operators (<code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>%</code>, <code>^</code> / <code>**</code>), nested parentheses like <code>(2 + 3) * 4</code>, unary operators, decimals, and common math functions (<code>sqrt</code>, <code>abs</code>, <code>round</code>, <code>floor</code>, <code>ceil</code>, <code>min</code>, <code>max</code>, <code>pow</code>). All expressions are evaluated using a safe recursive-descent parser — so no <code>eval()</code> is involved!</p>
<div class=note>
<p>Not so fun fact: I initially wanted to name this project formula-input, but turned out that name was already taken when I was tried publishing the package. This lead to the creation of <a href="https://brm.us/caniname">the <code>caniname</code> CLI tool</a> as a spinoff project.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#demo" name="demo">#</a> Demo</h3>
<p>Give the component a try in the interactive demo below:</p>
<p class="codepen" data-height="655" data-pen-title="&amp;lt;calc-input&amp;gt; demo" data-version="2" data-default-tab="result" data-slug-hash="VYpyrKW" data-user="bramus" style="height: 655px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/editor/bramus/pen/01a11c78-9eba-777b-98a7-fa51e8b7cb08"><br />
  &lt;calc-input&gt; demo</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>For more extended demos, check out <a href="https://calc-input.netlify.app/">the project website</a>.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#under-the-hood" name="under-the-hood">#</a> How it works under the hood</h3>
<p>Behind the scenes, <code>&lt;calc-input name="size"&gt;</code> renders three standard <code>&lt;input type="text"&gt;</code> elements in the Light DOM, toggling which one is visible upon focus:</p>
<pre><code class="language-html" style="tab-size: 2">&lt;calc-input name="size" value="2 + 3"&gt;
	&lt;input type="text" name="size" hidden&gt;
	&lt;input type="text" name="size--formula" hidden&gt;
	&lt;input type="text" name="size--result"&gt;
&lt;/calc-input&gt;</code></pre>
<p>At any given time, two of these inputs have the <code>hidden</code> attribute so the user only ever sees one input field. When blurred, the <code>--result</code> input is visible; when focused, <code>--formula</code> input takes its place for the user to edit.</p>
<p>Because these are regular Light DOM inputs, native HTML form submissions and constraint validation work seamlessly out of the box. When you enter an incomplete or unparsable formula like <code>2 + </code> and blur the field, <code>&lt;calc-input&gt;</code> keeps showing the formula input, highlights it with an invalid border, and calls <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity"><code>setCustomValidity()</code></a> to prevent the parent <code>&lt;form&gt;</code> from being submitted.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#configuration" name="configuration">#</a> Configuration &amp; Styling</h3>
<p>What actually gets submitted to the server is configurable through the <code>submit</code> attribute:</p>
<ul>
<li><code>submit="formula"</code> <em>(default)</em>: Submits all three fields, with the raw formula in the primary <code>name="size"</code> input.</li>
<li><code>submit="result"</code>: Submits all three fields, with the evaluated result in the primary <code>name="size"</code> input.</li>
<li><code>submit="formula-only"</code> / <code>submit="result-only"</code>: Omits the <code>--formula</code> and <code>--result</code> inputs from form submission so only the primary <code>name="size"</code> field is sent.</li>
</ul>
<p>If the default <code>--</code> delimiter in <code>size--formula</code> and <code>size--result</code> doesn’t match your backend naming conventions, you can customize it via the <code>separator</code> attribute (e.g. <code>separator="_"</code> or even <code>separator=""</code>).</p>
<p>For styling, <code>&lt;calc-input&gt;</code> uses <code>display: contents</code> and lets its inner <code>&lt;input&gt;</code> elements inherit box and typography properties (<code>border</code>, <code>border-radius</code>, <code>padding</code>, <code>background</code>, <code>color</code>, <code>font</code>, etc.). That way, any CSS styles you apply directly onto <code>calc-input</code> automatically style the underlying inputs:</p>
<pre><code class="language-css" style="tab-size: 2">calc-input {
	border: 2px solid #3b82f6;
	border-radius: 9999px;
	padding: 0.625rem 1.25rem;
}</code></pre>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#more" name="more">#</a> More info</h3>
<p>Check out the links below for the full documentation, more examples, and the source code:</p>
<ul>
<li><a href="https://calc-input.netlify.app/"><code>&lt;calc-input&gt;</code> Live Demo &amp; Documentation &rarr;</a></li>
<li><a href="https://github.com/bramus/calc-input"><code>&lt;calc-input&gt;</code> on GitHub &rarr;</a></li>
<li><a href="https://www.npmjs.com/package/calc-input"><code>calc-input</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><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/3mxfftxtbwk2f">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117407423741737051">🦣 Mastodon</a></li>
<li><a href="https://lnkd.in/p/…">💼 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/10/08/calc-input-a-custom-input-element-that-accepts-mathematical-formulas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/10/calc-input.mp4" length="84369" type="video/mp4" />

			</item>
		<item>
		<title>caniname: CLI tool to check project name availability across Netlify and NPM (and other sources)</title>
		<link>https://www.bram.us/2026/10/08/caniname-cli-tool-to-check-project-name-availability/</link>
					<comments>https://www.bram.us/2026/10/08/caniname-cli-tool-to-check-project-name-availability/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 19:52:35 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[cli]]></category>
		<category><![CDATA[tool]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36905</guid>

					<description><![CDATA[<p>Whenever I build a new tool or custom element (like <a href="https://show-keystrokes.netlify.app/"><code>&#60;show-keystrokes&#62;</code></a> or <a href="https://rich-input.netlify.app/"><code>&#60;rich-input&#62;</code></a>), I always end up doing the same manual dance: checking if the package name is still free on NPM and if the matching <code>&#60;name&#62;.netlify.app</code> subdomain is still unclaimed on Netlify. To automate that check, I built <a href="https://caniname.netlify.app/"><code>caniname</code></a>.</p>]]></description>
										<content:encoded><![CDATA[<figure>
	<a href="https://caniname.netlify.app/"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/10/og-image.jpg" alt='Screenshot of the caniname website and CLI preview' width="1200" height="630" class="alignnone size-medium" /></a><figcaption><code>caniname</code> CLI output preview</figcaption></figure>
<div class="intro">
<p>Whenever I build a new tool or custom element (like <a href="https://show-keystrokes.netlify.app/"><code>&lt;show-keystrokes&gt;</code></a> or <a href="https://rich-input.netlify.app/"><code>&lt;rich-input&gt;</code></a>), I always end up doing the same manual dance: checking if the package name is still free on NPM and if the matching <code>&lt;name&gt;.netlify.app</code> subdomain is still unclaimed on Netlify. To automate that check, I built <a href="https://caniname.netlify.app/"><code>caniname</code></a>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-need" name="the-need">#</a> The need</h3>
<p>As the classic saying goes, there are only two hard things in Computer Science — and one of them is naming things. And once you have finally found a good name for your new project (like I recently had to do for <a href="https://show-keystrokes.netlify.app/"><code>&lt;show-keystrokes&gt;</code></a> and <a href="https://rich-input.netlify.app/"><code>&lt;rich-input&gt;</code></a>), it can be pretty disappointing when that name ends up being taken already on NPM or as a domain on Netlify — something that you typically only find out upon publishing 😭</p>
<p>To save myself from manually checking both services over and over again, I built <a href="https://caniname.netlify.app/"><code>caniname</code></a>.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-tool" name="the-tool">#</a> The CLI tool</h3>
<p>There’s no need to install anything globally — you can run it straight via <code>npx</code>:</p>
<pre style="tab-size: 2"><code class="language-bash">npx caniname my-fancy-new-project</code></pre>
<p>For each name — yes, you can pass in multiple ones — <code>caniname</code> reports whether the Netlify subdomain and NPM package are available (<code>✔</code>) or already taken (<code>✖</code>, along with a link to the existing site or package). If you want to use it in scripts or CI, pass the <code>--json</code> (or <code>-j</code>) flag for structured JSON output, or import <code>checkName</code> and <code>checkNames</code> directly in Node.js.</p>
<pre style="tab-size: 2"><code class="language-bash">Checking availability for my-fancy-new-project…

  ✔ Netlify  my-fancy-new-project.netlify.app is available (https://my-fancy-new-project.netlify.app)
  ✖ NPM      rich-input is taken (https://www.npmjs.com/package/my-fancy-new-project)</code></pre>
<p>Right now the project only checks Netlify and NPM, but adding new service checks is pretty straightforward: every check lives in its own file inside <a href="https://github.com/bramus/caniname/tree/main/src/checks"><code>src/checks/</code></a> and gets picked up automatically. PRs with extra checks are very welcome 🙂</p>
<div class="note">
<p>💁‍♂️ <strong>Fun fact:</strong> I originally built this tool under the name <code>namecheck</code>. Upon running <code>./bin/namecheck.js namecheck</code> to dogfood it before publishing, it turned out that <code>namecheck</code> was already taken on both Netlify and NPM 😅 — so I used the tool itself to find an available name: <code>caniname</code>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#check-it-out" name="check-it-out">#</a> Go check it out</h3>
<p>Check out <code>caniname</code> at the following locations</p>
<ul>
<li><a href="https://caniname.netlify.app/"><code>caniname</code> Website &amp; Documentation &rarr;</a></li>
<li><a href="https://github.com/bramus/caniname"><code>caniname</code> on GitHub &rarr;</a></li>
<li><a href="https://www.npmjs.com/package/caniname"><code>caniname</code> on NPM &rarr;</a></li>
</ul>
<p><!--



<h3><a href="#spread-the-word" name="spread-the-word">#</a> Spread the word</h3>





<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>





<ul>
	

<li><a href="https://bsky.app/profile/bram.us/post/…">🦋 Bluesky</a></li>


	

<li><a href="https://front-end.social/@bramus/…">🦣 Mastodon</a></li>


	

<li><a href="https://lnkd.in/p/…">💼 LinkedIn</a></li>


</ul>



--></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/10/08/caniname-cli-tool-to-check-project-name-availability/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Bringing Microsoft Internet Explorer’s proprietary “Interpage Transitions” back to Modern Browsers using the View Transition API</title>
		<link>https://www.bram.us/2026/10/07/ie-page-transitions/</link>
					<comments>https://www.bram.us/2026/10/07/ie-page-transitions/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Wed, 07 Oct 2026 19:07:00 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[view transitions]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36888</guid>

					<description><![CDATA[<p>Back in late 2024, I started working on a fun end-of-year side project named <code>ie-page-transitions</code>. It”s a CSS + JS library that brings back Microsoft Internet Explorer’s proprietary “Interpage Transitions” to modern browsers, powered by the View Transition API. After sitting unfinished for a while, I finally got all 23 classic effects over the finish line and published the project on <a href="https://page-transitions.style/">page-transitions.style</a>.</p>]]></description>
										<content:encoded><![CDATA[<figure>
	<div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36888-2" width="640" height="360" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/10/ie-page-transitions.mp4?_=2" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/10/ie-page-transitions.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/10/ie-page-transitions.mp4</a></video></div><figcaption>Recording of <a href="https://page-transitions.style/">the demo</a> showcasing all 23 <code>RevealTrans</code> effects.</figcaption></figure>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="intro">
<p>Back in late 2024, I started working on a fun end-of-year side project named <code>ie-page-transitions</code>. It’s a CSS + JS library that brings back Microsoft Internet Explorer’s proprietary “Interpage Transitions” to modern browsers, powered by the View Transition API. After sitting unfinished for a while, I finally got all 23 classic effects over the finish line and published the project on <a href="https://page-transitions.style/">page-transitions.style</a>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#some-history" name="some-history">#</a> A Quick Trip Down Memory Lane (and Project History)</h3>
<p>If you built websites in the late 1990s or early 2000s, you might remember that Microsoft Internet Explorer 4.0 through 8.0 had a proprietary feature called <a href="https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms532847(v=vs.85)?redirectedfrom=MSDN#interpage-transitions">Interpage Transitions</a>. Using a simple <code>&lt;meta&gt;</code> tag in the <code>&lt;head&gt;</code> of your document, you could apply PowerPoint-style transitions — like <em>Box in</em>, <em>Circle out</em>, <em>Vertical blinds</em>, <em>Checkerboard</em>, or <em>Random dissolve</em> — to the entire browser viewport whenever a visitor entered or exited a web page.</p>
<pre style="tab-size: 2"><code class="language-html">&lt;!-- Page entry effect using effect number 7 (Wipe left) --&gt;
&lt;meta http-equiv="Page-Enter" content="revealTrans(Duration=0.5,Transition=7)"&gt;</code></pre>
<figure>
	<div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36888-3" width="640" height="438" loop muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/10/ie8-page-exit.mp4?_=3" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/10/ie8-page-exit.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/10/ie8-page-exit.mp4</a></video></div><figcaption>Recording of the Interpage Transitions on page exit, recorded in Internet Explorer 8.</figcaption></figure>
<p>At the end of 2024, it struck me that we now have all the primitives in modern CSS and the <a href="https://developer.chrome.com/docs/web-platform/view-transitions">View Transition API</a> to recreate those exact transitions cleanly on today’s web. I <a href="https://bsky.app/profile/bram.us/post/3ldiynl2d722i">teased an early prototype on Bluesky</a> back in December 2024 as a little EOY project, and I even showed it off in a few conference talks since.</p>
<p>However, I never got around to actually covering it here on <code>bram.us</code> because the project wasn’t finalized yet: I had only implemented a handful of the 23 effects and still needed to wire up the trickier ones like checkerboard grids, pixel dissolves, and random bars. Recently, thanks to Gemini, I was able to easily knock out the remaining effects, polish up the interactive demo site, and finally get the whole thing over the finish line! 🙂</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#ie-compatible" name="ie-compatible">#</a> Bringing the Oldskool Back</h3>
<p>Rather than inventing a new configuration format, <code>ie-page-transitions</code> brings the oldskool back by reading the exact same <code>&lt;meta http-equiv="Page-Enter"&gt;</code> and <code>&lt;meta http-equiv="Page-Exit"&gt;</code> tags that Internet Explorer used back in the day.</p>
<p>Under the hood, a tiny script parses the <code>revealTrans(Duration=…,Transition=…)</code> declaration from your <code>&lt;meta&gt;</code> tags and translates it into <a href="https://drafts.csswg.org/css-view-transitions-2/#selective-vt">Selective View Transitions with Active Types</a> (for example, <code>page-enter-effect-7</code>). The accompanying stylesheet then uses the <code>:active-view-transition-type()</code> pseudo-class to animate the <code>::view-transition-old(root)</code> and <code>::view-transition-new(root)</code> pseudo-elements using modern CSS <code>clip-path</code> shapes, multi-layer <code>mask-image</code> gradients, and registered custom properties (<code>@property</code>).</p>
<p>To use it on a Multi-Page Application (aka <em>“just a website”</em>), opt in to Cross-Document View Transitions, load the stylesheet and render-blocking module script, and drop in your classic IE <code>&lt;meta&gt;</code> tags:</p>
<pre style="tab-size: 2"><code class="language-html">&lt;!-- 1. Enable Cross-Document View Transitions --&gt;
&lt;style&gt;
	@view-transition {
		navigation: auto;
	}
&lt;/style&gt;

&lt;!-- 2. Include Stylesheet and Render-Blocking Script --&gt;
&lt;link rel="stylesheet" href="https://unpkg.com/ie-page-transitions/ie-page-transitions.css"&gt;
&lt;script src="https://unpkg.com/ie-page-transitions/ie-page-transitions.mpa.js" type="module" blocking="render"&gt;&lt;/script&gt;

&lt;!-- 3. Configure IE Page Transitions Effect(s) --&gt;
&lt;meta http-equiv="Page-Enter" content="revealTrans(Duration=0.5,Transition=23)"&gt;
&lt;meta http-equiv="Page-Exit" content="revealTrans(Duration=0.5,Transition=23)"&gt;</code></pre>
<p>A helper for Single-Page Applications (SPAs) is also included, alongside a set of CSS Custom Properties (like <code>--page-transitions-blinds-bands</code>, <code>--page-transitions-checkerboard-columns</code>, and <code>--page-transitions-random-dissolve-size</code>) in case you want to tweak the geometry of the effects.</p>
<p>Under the hood <code>ie-page-transitions</code> uses <a href="https://developer.chrome.com/docs/web-platform/view-transitions/same-document#view-transition-types">View Transition Types</a>. Browser support therefore only starts with Chrome 125+, Safari 18.2+, and Firefox 147+ for the SPA variant. For the MPA variant, Cross-Document View Transition support is required, so that leaves Chrome 126+ and Safari 18.2+.</p>
<p><mark>Because <code>ie-page-transitions</code> reuses the original <code>&lt;meta&gt;</code> tags originally introduced by Internet Explorer, the effects are supported not only in today’s modern browsers, but also in Internet Explorer 5.5 – 8.0 😆</mark>. In those versions of IE, the browser’s native DirectX <code>RevealTrans</code> filter will kick in automatically while in modern browsers <code>ie-page-transitions</code> takes over.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#check-it-out" name="check-it-out">#</a> Check It Out</h3>
<p>I’ve put together a dedicated website at <a href="https://page-transitions.style/">page-transitions.style</a> where you can preview all 23 effects, read the full documentation, and play with both the SPA and MPA demos right inside an embedded browser shell.</p>
<ul>
<li><a href="https://page-transitions.style/">page-transitions.style (Website &amp; Live Demos) &rarr;</a></li>
<li><a href="https://github.com/bramus/ie-page-transitions"><code>ie-page-transitions</code> on GitHub &rarr;</a></li>
<li><a href="https://www.npmjs.com/package/ie-page-transitions"><code>ie-page-transitions</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><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/3mxcnohohjk2h">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117401220195529247">🦣 Mastodon</a></li>
<li><a href="https://lnkd.in/p/eFScduAM">💼 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/10/07/ie-page-transitions/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/10/ie-page-transitions.mp4" length="2372382" type="video/mp4" />
<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/10/ie8-page-exit.mp4" length="6224611" type="video/mp4" />

			</item>
		<item>
		<title>Show keystrokes on a website with the &#x3C;&#x200B;show-keystrokes&#x3E; custom element</title>
		<link>https://www.bram.us/2026/09/23/show-keystrokes/</link>
					<comments>https://www.bram.us/2026/09/23/show-keystrokes/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Wed, 23 Sep 2026 08:32:27 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[custom elements]]></category>
		<category><![CDATA[keypress]]></category>
		<category><![CDATA[shortcuts]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36850</guid>

					<description><![CDATA[<p>When recording screen casts or doing live demos, I typically rely on a native macOS app to visualize my keystrokes on screen. Recently, however, that app got blocked on my work machine by a corporate security policy. Additionally, the keystrokes visualized through that app don’t show up when screen sharing a specific window or tab.</p><p>Because I needed to demo a website that relies heavily on keyboard navigation — and thus <em>really</em> needed those keystrokes to be visible to the audience — I found myself in a bit of a pickle. So with the help of Gemini, I decided to build a web-native solution to fill that gap: <a href="https://show-keystrokes.netlify.app/"><code>&#60;show-keystrokes&#62;</code></a></p>]]></description>
										<content:encoded><![CDATA[<figure>
	<div style="width: 640px;" class="wp-video"><video class="wp-video-shortcode" id="video-36850-4" width="640" height="411" loop autoplay muted preload="metadata" controls="controls"><source type="video/mp4" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/show-keystrokes.mp4?_=4" /><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/show-keystrokes.mp4">https://www.bram.us/wordpress/wp-content/uploads/2026/09/show-keystrokes.mp4</a></video></div><figcaption>Recording of the <a href="https://show-keystrokes.netlify.app/"><code>&lt;show-keystrokes&gt;</code> Demo &amp; Playground</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>When recording screen casts or doing live demos, I typically rely on a native macOS app to visualize my keystrokes on screen. Recently, however, that app got blocked on my work machine by a corporate security policy. Additionally, the keystrokes visualized through that app don’t show up when screen sharing a specific window or tab.</p>
<p>Because I needed to demo a website that relies heavily on keyboard navigation — and thus <em>really</em> needed those keystrokes to be visible to the audience — I found myself in a bit of a pickle. So with the help of Gemini, I decided to build a web-native solution to fill that gap: <a href="https://show-keystrokes.netlify.app/"><code>&lt;show-keystrokes&gt;</code></a></p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#usage" name="usage">#</a> Embed it and you’re good to go</h3>
<p>Using <code>&lt;show-keystrokes&gt;</code> is as simple as loading the module from npm (or a CDN) and dropping the custom element onto your page. Embed it on your site and you’re good to go:</p>
<pre><code class="language-html" style="tab-size: 2">&lt;script type="module" src="https://cdn.jsdelivr.net/npm/show-keystrokes/index.js"&gt;&lt;/script&gt;
&lt;show-keystrokes&gt;&lt;/show-keystrokes&gt;</code></pre>
<p>Out of the box, <code>&lt;show-keystrokes&gt;</code> listens for keyboard events and displays keyboard shortcuts (such as <code>⌘ + A</code> or <code>⇧ + ⌘ + T</code>) and navigational keys (such as <code>⇥</code>, <code>→</code>, <code>←</code>, <code>↵</code>, and <code>⎋</code>), while ignoring regular typing. If you <em>do</em> want to show every single key press, you can set the <code>keystrokes</code> attribute to <code>all</code> (or narrow it down to <code>shortcuts</code> or <code>navigational</code>).</p>
<pre><code class="language-html" style="tab-size: 2">&lt;script type="module" src="https://cdn.jsdelivr.net/npm/show-keystrokes/index.js"&gt;&lt;/script&gt;
&lt;show-keystrokes keystrokes="all"&gt;&lt;/show-keystrokes&gt;</code></pre>
<p>The component automatically adapts to your platform (showing <code>⌘</code> and <code>⌥</code> on macOS versus <code>CTRL</code> and <code>ALT</code> on Windows/Linux), supports both symbol glyphs and text labels (<code>⌘</code> vs <code>CMD</code>), and ships with two built-in themes that respond to light and dark mode via <code>light-dark()</code>. And oh, it’s fully styleable through by targeting the exposed <code>::part()</code>s and custom properties.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#positioning-and-modern-css" name="positioning-and-modern-css">#</a> Powered by the Popover API and CSS Anchor Positioning</h3>
<p>Depending on what you are demoing, you might want the keycaps to appear in different places. Using the <code>position</code> attribute, you can easily position the component in a corner of the viewport (the default is <code>viewport top right</code>), right next to the cursor (<code>position="pointer"</code>), or just inline in normal document flow (<code>position="normal"</code>).</p>
<pre><code class="language-html" style="tab-size: 2">&lt;!-- Fixed in the bottom-center of the viewport --&gt;
&lt;show-keystrokes position="viewport bottom center"&gt;&lt;/show-keystrokes&gt;

&lt;!-- Follow the cursor --&gt;
&lt;show-keystrokes position="pointer"&gt;&lt;/show-keystrokes&gt;

&lt;!-- Render inline in normal document flow --&gt;
&lt;show-keystrokes position="normal"&gt;&lt;/show-keystrokes&gt;</code></pre>
<p>Under the hood, <code>&lt;show-keystrokes&gt;</code> leans heavily on modern web platform features to make this work seamlessly without any <code>z-index</code> battles or JavaScript bounding-box math.</p>
<p>First, the component always stays on top of everything else on the page thanks to the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API">Popover API</a>. Its internal container is promoted to the browser’s Top Layer using <code>popover="manual"</code>, ensuring that no matter how complex the host page’s stacking contexts are, your keystrokes will never be clipped or hidden behind another element.</p>
<p>Second, the component positions and repositions itself thanks to <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Anchor_positioning">CSS Anchor Positioning</a>. In viewport mode, it anchors itself inside a full-viewport anchor element and aligns itself using <code>position-area</code>, <code>align-self</code>, and <code>justify-self</code>. In <code>pointer</code> mode, an invisible anchor tracks your cursor coordinates <em>(via custom properties)</em>, and the popover anchors itself next to it using <code>position-area</code>. The <code>position-try-fallbacks: flip-inline, flip-block, flip-inline flip-block</code> are set by default, so the browser automatically flips the keystroke overlay to the other side of the cursor whenever you move it too close to the edge of the viewport. 🙂</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#bookmarklet-and-extension" name="bookmarklet-and-extension">#</a> Run it on any site with the Bookmarklet or Browser Extension</h3>
<p>Because you don’t always control the source code of the site you want to demo, the project also comes with both a <a href="https://show-keystrokes.netlify.app/#bookmarklet"><em>bookmarklet</em></a> <a href="https://en.wikipedia.org/wiki/Bookmarklet">ⓘ</a> and a <a href="https://show-keystrokes.netlify.app/#extension">Browser Extension</a> (for both Firefox and Chrome) so you can inject <code>&lt;show-keystrokes&gt;</code> onto any webpage on the fly.</p>
<p>The bookmarklet is a quick drag-and-drop button (available on <a href="https://show-keystrokes.netlify.app/#bookmarklet">the demo page</a>) that dynamically imports the package from jsDelivr and calls the programmatic <code>create()</code> helper. For sites with a strict Content Security Policy (CSP) or when you want <code>&lt;show-keystrokes&gt;</code> to persist across page navigations, the <a href="https://chromewebstore.google.com/detail/show-keystrokes/djfbngdmoohldepblnidecjhmgommdmh">Chrome Extension</a> <em>(published)</em> or <a href="https://addons.mozilla.org/en-US/firefox/addon/show-keystrokes/">Firefox Extension</a> <em>(pending review)</em> has you covered. It bundles the component locally, lets you configure global defaults as well as per-origin overrides right from the popup, and includes a handy <code>⇧ + ⌘ + K</code> (or <code>⇧ + CTRL + K</code>) shortcut to toggle it on or off.</p>
<p>Building the extension was a bit of an afterthought once I had version 1.0.0 of the component ready. But thanks to Modern Web Guidance and <a href="https://github.com/GoogleChrome/modern-web-guidance-src/blob/main/skills-src/chrome-extensions/SKILL.md">its Chrome Extensions skill</a>, creating it was easy-peasy and done in no time <em>(also see <a href="https://developer.chrome.com/docs/extensions/ai/build-with-ai">this guide</a>)</em> 🙂</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#links" name="links">#</a> Links</h3>
<ul>
<li><a href="https://show-keystrokes.netlify.app/"><code>&lt;show-keystrokes&gt;</code> Live Demo &amp; Playground &rarr;</a></li>
<li><a href="https://github.com/bramus/show-keystrokes"><code>&lt;show-keystrokes&gt;</code> on GitHub &rarr;</a></li>
<li><a href="https://www.npmjs.com/package/show-keystrokes"><code>show-keystrokes</code> on npm &rarr;</a></li>
<li><a href="https://chromewebstore.google.com/detail/show-keystrokes/djfbngdmoohldepblnidecjhmgommdmh"><code>&lt;show-keystrokes&gt;</code> Chrome Extension &rarr;</a></li>
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/show-keystrokes/"><code>&lt;show-keystrokes&gt;</code> Firefox Extension &rarr;</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#spread-the-word" name="spread-the-word">#</a> Spread the word</h3>
<p>Feel free to reshare one of the following posts on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3mw3xrnstjk2m">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117314107823933436">🦣 Mastodon</a></li>
<p>	<!-- 

<li><a href="https://lnkd.in/p/…">💼 LinkedIn</a></li>

 -->
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/09/23/show-keystrokes/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		<enclosure url="https://www.bram.us/wordpress/wp-content/uploads/2026/09/show-keystrokes.mp4" length="4210288" type="video/mp4" />

			</item>
		<item>
		<title>New in Chrome 154: iframes that automatically resize themselves to their content</title>
		<link>https://www.bram.us/2026/09/23/responsive-iframes/</link>
					<comments>https://www.bram.us/2026/09/23/responsive-iframes/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Tue, 22 Sep 2026 22:15:05 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[iframe]]></category>
		<category><![CDATA[resize]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36670</guid>

					<description><![CDATA[<p>Chrome 154 adds support for <em>responsively-sized iframes</em>, letting an <code>&#60;iframe&#62;</code> size itself based on the intrinsic size of its embedded document. This is perfect for seamlessly embedding third-party comment widgets, varying-height social media embeds, or any other type of embed that uses an <code>&#60;iframe&#62;</code>.</p>]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/responsive-iframes.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/responsive-iframes.png" alt="" width="560" height="440" class="alignnone size-medium wp-image-36839" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/09/responsive-iframes.png 1786w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/responsive-iframes-560x440.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/responsive-iframes-1120x879.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/responsive-iframes-768x603.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/responsive-iframes-1536x1206.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/responsive-iframes-1568x1231.png 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a></p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="intro">
<p>Chrome 154 adds support for <em>responsively-sized iframes</em>, letting an <code>&lt;iframe&gt;</code> size itself based on the intrinsic size of its embedded document. This is perfect for seamlessly embedding third-party comment widgets, varying-height social media embeds, or any other type of embed that uses an <code>&lt;iframe&gt;</code>.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#the-code" name="the-code">#</a> The Code</h3>
<p>To make this work there is a two-way opt-in:</p>
<ul>
<li>The embedder (e.g. <code>index.html</code>) needs to set the <code>frame-sizing</code> CSS property with a value of <code>auto</code>, <code>content-height</code>, or <code>content-width</code> (or its logical variants) on the <code>iframe</code>.</li>
<li>The document loaded in the <code>&lt;iframe&gt;</code> (e.g. <code>iframe.html</code>) needs to include a meta tag to indicate that it’s OK to communicate its size the parent embedder.</li>
</ul>
<p>In code:</p>
<pre><code class="language-css">/* In styles.css for index.html */
iframe {
  frame-sizing: content-height;
  width: 100%;
}</code></pre>
<pre><code class="language-html">&#x3C;!-- In the iframe.html’s head --&#x3E;
&#x3C;meta name=&#x22;responsive-embedded-sizing&#x22; content=&#x22;allow-origins=*&#x22;&#x3E;</code></pre>
<p>The content’s size is communicated from the embedded document after page load. To communicate a new size, the document in the frame must call <code>window.requestResize();</code></p>
<p>Use <code>allow-origins</code> to limit which origins the document communicates its size info to.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#demo" name="demo">#</a> Demo</h3>
<p>Here’s a demo that contains a form which is an <code>&lt;iframe&gt;</code>. Whenever you go to a next step of the form, the <code>&lt;iframe&gt;</code> resizes itself to its contents … if your browser suppports <code>frame-sizing</code> that is.</p>
<p><iframe height="900" style="width: 100%;" scrolling="no" title="Responsively Sized &lt;iframe&gt; demo" src="https://codepen.io/editor/bramus/embed/01a0621c-73e0-7dd0-aef8-1a820116c459?default-tab=result" frameborder="no" loading="lazy" allowtransparency="true"><br />
  See the Pen <a href="https://codepen.io/editor/bramus/pen/01a0621c-73e0-7dd0-aef8-1a820116c459"><br />
  Responsively Sized &lt;iframe&gt; demo</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>)<br />
  on <a href="https://codepen.io">CodePen</a>.<br />
</iframe></p>
<p>Because the iframe resizes itself, the form is seamlessly embedded, without any scrollbar appearing at all.</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 September 2026, the list below is constantly being updated. <em>Last update: September 23, 2026</em>.</p>
</div>
<p>Support for responsively size iframes is this:</p>
<dl class="grid">
<dt>Chromium <em>(Blink)</em></dt>
<dd>
<p>✅ Supported in Chrome 154</p>
</dd>
<dt>Firefox <em>(Gecko)</em></dt>
<dd>
<p>❌ No Support. Follow <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2035008">Bug 2035008</a> to stay up-to-date.</p>
</dd>
<dt>Safari <em>(WebKit)</em></dt>
<dd>
<p>❌ No Support. Follow <a href="https://bugs.webkit.org/show_bug.cgi?id=314713">Bug 314713</a> to stay up-to-date.</p>
</dd>
</dl>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#learn-more" name="learn-more">#</a> Learn More</h3>
<p>Get all the details about responsive iframes on <code>developer.chrome.com</code>, in this article I co-authored: <a href="https://developer.chrome.com/blog/responsive-iframes">Responsive iframes in Chrome 154 &rarr;</a></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/09/23/responsive-iframes/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Ship cleaner packages (without the ./dist or ./src folder) by publishing a subfolder to NPM</title>
		<link>https://www.bram.us/2026/09/20/npm-publish-subfolder/</link>
					<comments>https://www.bram.us/2026/09/20/npm-publish-subfolder/#respond</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Sun, 20 Sep 2026 17:38:20 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[npm]]></category>
		<category><![CDATA[packages]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36662</guid>

					<description><![CDATA[<p>One detail that has always bugged me about publishing a package to NPM is how build output paths like <code>dist/</code> can leak into the public API.</p><p>Turns out, <code>npm publish</code> can do it, but to make it work seamlessly in a real-world project, you need a few extra pieces in place. Let me walk you through how it works.</p>]]></description>
										<content:encoded><![CDATA[<figure><a href="https://www.bram.us/wordpress/wp-content/uploads/2026/09/npm-publish-subfolder.png"><img loading="lazy" decoding="async" src="https://www.bram.us/wordpress/wp-content/uploads/2026/09/npm-publish-subfolder.png" alt="" width="560" height="315" class="alignnone size-medium wp-image-36825" srcset="https://www.bram.us/wordpress/wp-content/uploads/2026/09/npm-publish-subfolder.png 2400w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/npm-publish-subfolder-560x315.png 560w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/npm-publish-subfolder-1120x630.png 1120w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/npm-publish-subfolder-768x432.png 768w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/npm-publish-subfolder-1536x864.png 1536w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/npm-publish-subfolder-2048x1152.png 2048w, https://www.bram.us/wordpress/wp-content/uploads/2026/09/npm-publish-subfolder-1568x882.png 1568w" sizes="auto, (max-width: 560px) 100vw, 560px" /></a><figcaption>Visual of the build steps to take. Note that before running <code>npm publish ./dist</code> you need to prepare the files in <code>./dist</code> <em>(as detailed in this post)</em></figcaption></figure>
<div class="intro">
<p>One detail that has always bugged me about publishing a package to NPM is how build output paths like <code>dist/</code> can leak into the public API.</p>
<p>Turns out, <code>npm publish</code> can do it, but to make it work seamlessly in a real-world project, you need a few extra pieces in place. Let me walk you through how it works.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The Need: Clean imports without leaking implementation details</h3>
<p>When authoring a package, you typically have a directory layout like this:</p>
<pre><code>my-package/
├── src/
│   └── index.js
├── dist/
│   └── index.js
├── package.json
└── README.md</code></pre>
<p>Source code lives in <code>src/</code>, while your a build step typically spits out the distribution files into a <code>dist/</code> or <code>build/</code> folder. Or if your package has no build step, because you authored an ES Module in JavaScript, you can even ship <code>src/</code> as-is. This detail can leak into your public API, which is something you generally want to avoid, especially when loading the files from a CDN.</p>
<p>For example, the very first version of <a href="https://bsky.app/profile/bram.us/post/3ldiynl2d722i">my <code>ie-page-transitions</code> package</a> — which has no build step — shipped the <code>src</code> folder as part of the package structure.</p>
<pre><code>my-package/
├── src/
│   ├── ie-page-transitions.css
│   ├── ie-page-transitions.mpa.js
│   ├── ie-page-transitions.shared.js
│   └── ie-page-transitions.spa.js
├── package.json
└── README.md</code></pre>
<p>To users that use NodeJS (or a bundler) this detail is not visible, thanks to the <code>exports</code> field in the <code>package.json</code>.</p>
<pre><code>{
  "name": "ie-page-transitions",
  "version": "0.0.1",
  …
  "exports": {
    "./spa": "./src/ie-page-transitions.spa.js",
    "./mpa": "./src/ie-page-transitions.mpa.js",
    "./shared": "./src/ie-page-transitions.shared.js",
    "./css": "./src/ie-page-transitions.css"
  },
  …
}</code></pre>
<p>When using NodeJS, users can just import the files straight from the package without needing to know the files live in the <code>./src</code> folder. For example, the CSS can be imported as follow:</p>
<pre><code class="language-javascript">import styles from 'ie-page-transitions/css' with { type: "css" };</code></pre>
<p>But for users loading the package from a CDN, it’s a different story. When loading the package from a CDN like jsDelivr, users needed to import the files from locations that all include that <code>src</code> folder. That same CSS file for example needs to be loaded from this location:</p>
<pre><code>https://cdn.jsdelivr.net/npm/ie-page-transitions@0.0.1<strong>/src</strong>/ie-page-transitions.css</code></pre>
<p>I don’t like this, because <mark>having <code>src/</code> (or <code>dist/</code> or <code>build/</code>) in public import paths is an implementation detail that leaked into the public API</mark>. It looks clunky, exposes the internal folder structure, and is just annoying to type.</p>
<div class="note">
<p>I’m not the only one here. A quick search tells me that developers <a href="https://stackoverflow.com/questions/38935176/how-to-npm-publish-specific-folder-but-as-package-root">have been asking about this for more than 10 years by now</a>. PNPM actually supports a <a href="https://pnpm.io/package_json#publishconfigdirectory"><code>publishConfig.directory</code></a> setting in the <code>package.json</code> file to achieve this, but stock NPM doesn’t.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The Solution: <code>npm publish ./folder</code></h3>
<p>For my last three packages (<a href="https://brm.us/hic-pageflip"><code>hic-pageflip</code></a>, <a href="https://brm.us/mermaid-element"><code>mermaid-element</code></a>, and <a href="https://brm.us/rich-input"><code>rich-input</code></a>), which all build their source into a <code>./dist</code> folder, I’ve been using a little trick to <mark>publish only <em>the contents</em> of the <code>dist</code> folder to NPM</mark>. This makes the import paths clean and direct. For example, for <code>hic-pageflip</code>, when loading from a CDN like jsDelivr, it does not include <code>dist/</code>-part in the URL:</p>
<pre><code class="language-javascript">import { PageFlip } from 'https://cdn.jsdelivr.net/npm/hic-pageflip';</code></pre>
<p>To achieve this, I used a lesser-known capability of the npm CLI: <mark><code>npm publish</code> accepts a folder path as an argument.</mark></p>
<pre class="language-bash"><code class="language-bash">npm publish ./dist</code></pre>
<p>When you pass a folder path to <code>npm publish</code>, npm doesn&#8217;t package the current working directory. Instead, <strong>it treats that target folder as the package root</strong>.</p>
<p>Whatever is inside <code>./dist</code> will be placed directly at the top level of the tarball uploaded to the registry. </p>
<p>For this to work, you do need some extra preparation, but it&#8217;s worth it. Let me walk you through how it works.</p>
<div class="note">
<p>💁‍♂️ <em>“What about just reorganizing the project to not have a <code>dist</code> folder at all?”</em></p>
<p>A workaround I’ve seen developers use — see <a href="https://github.com/keithamus/invokers-polyfill">the invokers polyfill</a> as an example — is leaving all source files in the project root and dumping build output directly into that same project root. While that works, I believe that creates a mess: compiled <code>.js</code>, <code>.d.ts</code>, and <code>.map</code> files sit right alongside your dotfiles, test configurations, and source files, cluttering git and your working directory.</p>
<p>What you really want is simple: keep the actual source code in <code>./src</code> and keep <code>./dist</code> for build outputs, but have the contents of <code>./dist</code> become the root of the package when published to NPM.</p>
</div>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Making It Work: The Missing Pieces</h3>
<p>In order to publish the <code>dist</code> folder, there are two main things you need to do once your project has been built:</p>
<ol>
<li>Copy other necessary package files (<code>package.json</code>, <code>README.md</code>, <code>LICENSE</code>, etc.) into <code>./dist</code>.</li>
<li>Rewrite the <code>package.json</code> in <code>./dist</code> so its paths match the new root (and optionally remove the <code>scripts</code> from that file as well).</li>
</ol>
<p>Let’s look at both steps.</p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>1. Copy files you want to package into <code>./dist</code></h4>
<p>Because <code>npm publish ./dist</code> treats <code>./dist</code> as the package root and ignores everything outside of it, that folder needs to contain all the metadata files required for a valid package alongside your built code:</p>
<ul>
<li><code>package.json</code> (so npm knows your package name, version, dependencies, and entry points).</li>
<li><code>README.md</code> (so npmjs.com displays your documentation).</li>
<li><code>LICENSE</code> (so npm recognizes your license and consumers stay legally safe).</li>
</ul>
<p>Since all of these typically live in your project root, you need to copy them into <code>./dist</code> after building. You can also include other files if you want, of course.</p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>2. Rewrite the <code>package.json</code> in <code>./dist</code></h4>
<p>Simply copying your root <code>package.json</code> into <code>./dist</code> as-is won’t work — the <code>package.json</code> in the build folder needs to be adjusted.</p>
<p>In your root <code>package.json</code>, your entry points typically include the <code>./dist/</code> prefix:</p>
<pre class="language-json"><code class="language-json">{
  "name": "my-package",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "module": "./dist/index.js",
  "exports": {
    ".": "./dist/index.js",
    "./submodule": "./dist/submodule.js"
  }
}</code></pre>
<p>If you copy that <code>package.json</code> as-is into <code>./dist</code> and publish it, everything breaks because for published package, the root <em>is</em> the contents of <code>./dist</code> itself.</p>
<p>So the paths in the file <code>./dist/package.json</code> must be rewritten relative to the new root:</p>
<pre class="language-json"><code class="language-json">{
  "name": "my-package",
  "version": "1.0.0",
  "main": "./index.js",
  "module": "./index.js",
  "exports": {
    ".": "./index.js",
    "./submodule": "./submodule.js"
  }
}</code></pre>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>The Code: Assembling <code>./dist</code></h3>
<p>Rather than manually copying files and re-mapping every single export field by hand, you can automate both steps in a small Node.js script. For the <code>package.json</code> adjustments, a quick string replace does the trick: take your root <code>package.json</code>, strip out internal <code>scripts</code>, and replace <code>./dist/</code> with <code>./</code>.</p>
<p>Here’s a clean, zero-dependency Node.js script (<code>scripts/postbuild.js</code>) that does the heavy lifting:</p>
<pre class="language-javascript"><code class="language-javascript">import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..');
const distDir = path.join(rootDir, 'dist');
const srcDir = path.join(rootDir, 'src');

console.log('Preparing ./dist package...');

// 1. Clean and recreate the dist directory (if not already handled by your bundler)
fs.rmSync(distDir, { recursive: true, force: true });
fs.mkdirSync(distDir, { recursive: true });

// 2. Copy compiled/source files into dist/
fs.cpSync(srcDir, distDir, { recursive: true });

// 3. Copy essential metadata files from root to dist/
const filesToCopy = ['README.md', 'LICENSE'];
for (const file of filesToCopy) {
  const srcPath = path.join(rootDir, file);
  if (fs.existsSync(srcPath)) {
    fs.copyFileSync(srcPath, path.join(distDir, file));
  }
}

// 4. Prepare and rewrite package.json for dist/
const pkgPath = path.join(rootDir, 'package.json');
if (fs.existsSync(pkgPath)) {
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));

  // Strip scripts so consumers don't get internal build/test scripts
  delete pkg.scripts;

  // Replace ./dist/ with ./ so all paths point to the root of the published package
  const distPkgContent = JSON.stringify(pkg, null, 2).replaceAll('./dist/', './') + 'n';

  // Write out the tailored package.json to dist/
  fs.writeFileSync(
    path.join(distDir, 'package.json'),
    distPkgContent,
    'utf8'
  );
}

console.log('Successfully prepared ./dist for publishing!');</code></pre>
<p>You can (and should) set this up as a <code>postbuild</code> step in your root <code>package.json</code> so that it automatically runs every time <code>npm run build</code> finishes:</p>
<pre class="language-json"><code class="language-json">{
  "scripts": {
    "build": "…",
    "postbuild": "node scripts/postbuild.js"
  }
}</code></pre>
<p>Now, every time you run <code>npm run build</code>, the files and code in <code>./dist</code> end up as a self-contained, publication-ready package.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3><a href="#no-npm-publish" name="no-npm-publish">#</a> Safeguarding Against Muscle Memory: Prevent <code>npm publish</code> from running</h3>
<p>Typically, when authoring a package that has a build step, you have <code>npm run build</code> execute in a <code>prepublishOnly</code> task in your <code>package.json</code>:</p>
<pre class="language-json"><code class="language-json">{
  "scripts": {
    "build": "…",
    "postbuild": "node scripts/postbuild.js",
    "prepublishOnly": "npm run build"
  }
}</code></pre>
<p>That way, every time you run <code>npm publish</code>, npm automatically builds the project first before publishing.</p>
<p>However, with our subfolder setup, there’s a big catch here: developer muscle memory.</p>
<p>When you’re ready to ship an update and instinctively type <code>npm publish</code> into your terminal, npm will dutifully run <code>prepublishOnly</code> and execute <code>npm run build</code> … but then it’s still gonna try and publish the project root instead of <code>./dist</code> … uh-oh!</p>
<p>To prevent this from ever happening, you can repurpose <code>prepublishOnly</code> into a safeguard and combine it with a dedicated publish script (<code>npm run pub</code>) that runs <code>npm run build</code> before calling <code>npm publish ./dist</code>.</p>
<p>In your root <code>package.json</code>:</p>
<pre class="language-json"><code class="language-json">{
  "scripts": {
    "build": "…",
    "postbuild": "node scripts/postbuild.js",
    "prepublishOnly": "if [ -z $PUBLISH ]; then echo 'n🛑 ERROR: This package must be published using `npm run pub`n' &amp;&amp; exit 1; fi",
    "pub": "PUBLISH=true npm run build &amp;&amp; npm publish ./dist"
  }
}</code></pre>
<p>The trick here is the <code>$PUBLISH</code> environment variable. When you run <code>npm publish</code> directly, <code>$PUBLISH</code> is not set, and the script simply exits — preventing <code>npm publish</code> from running.</p>
<p>In the <code>pub</code> script (invoked via <code>npm run pub</code>), <code>$PUBLISH</code> gets explicitly set to <code>true</code>, which allows the build and <code>npm publish ./dist</code> to go through without being blocked.</p>
<p>This setup is simple, elegant, and completely foolproof 🙂</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>Testing Your Publish: Dry Runs and Tarballs</h3>
<p>Before you publish a package to the live registry for the first time with this setup, don&#8217;t just push it and hope for the best. Test it locally first.</p>
<p>Here are two essential commands to verify your setup:</p>
<h4>1. Perform a dry run with <code>npm publish ./dist --dry-run</code></h4>
<p>You can run <code>npm publish</code> with the <code>--dry-run</code> flag pointing to your folder:</p>
<pre class="language-bash"><code class="language-bash">npm publish ./dist --dry-run</code></pre>
<p>This runs the entire publishing pipeline — tarball creation, file filtering, checksum generation — without actually uploading anything to NPM.</p>
<p>It outputs a detailed manifest of everything that will be shipped:</p>
<pre><code>npm notice === Tarball Contents ===
npm notice 1.1kB  LICENSE
npm notice 1.8kB  README.md
npm notice 850B   index.js
npm notice 1.2kB  package.json
npm notice 4.2kB  core/engine.js
npm notice === Tarball Details ===
npm notice name:          hic-pageflip
npm notice version:       1.0.1
npm notice filename:      hic-pageflip-1.0.1.tgz
npm notice package size:  3.2 kB
npm notice unpacked size: 9.1 kB
npm notice total files:   5</code></pre>
<p>Look closely at that file list: notice how <code>index.js</code>, <code>README.md</code>, and <code>LICENSE</code> are all sitting right at the root, with no <code>dist/</code> prefix in sight? That&#8217;s your confirmation that the subfolder structure is working as intended.</p>
<p style="text-align: center; font-size: small; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h4>2. Inspect the created tarball with <code>npm pack</code></h4>
<p>The previous step already generates the tarball for publishing. You can also run <code>npm pack</code> on its own to generate a local tarball (e.g. <code>my-package-1.0.0.tgz</code>) for inspection.</p>
<pre class="language-bash"><code class="language-bash"># Be sure to run npm run build first
npm pack ./dist</code></pre>
<p>You can then extract and inspect the contents of the generated tarball:</p>
<pre class="language-bash"><code class="language-bash">tar -xzf my-package-1.0.0.tgz ./my-package-1.0.0</code></pre>
<p>Now you&#8217;ll see the contents of <code>./dist</code> sitting right in the folder <code>./my-package-1.0.0</code> which confirms that the package was correctly packaged and is ready for publishing.</p>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<h3>In Closing</h3>
<p>Publishing a subfolder instead of your repository root is one of those small tweaks that, I think, makes for a better package, without much added complexity. I’ve successfully used this pattern in multiple projects, including <a href="https://brm.us/hic-pageflip"><code>hic-pageflip</code></a> and <a href="https://brm.us/rich-input"><code>rich-input</code></a>.</p>
<p><mark>It feels a bit stupid that NPM does not have this functionality built in</mark>, especially since developers <a href="https://stackoverflow.com/questions/38935176/how-to-npm-publish-specific-folder-but-as-package-root">have been asking about this for more than 10 years by now</a>. A new <code>publishDirectory</code> field in the <code>package.json</code> that instructs <code>npm publish</code> to publish that specific subfolder would be welcome here. PNPM has this (<a href="https://pnpm.io/package_json#publishconfigdirectory"><code>publishConfig.directory</code></a>), and I think it’s long overdue for NPM to support this as well …</p>
<p><mark>Until then, you can use the pattern I detailed here in this post 🙂</mark></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 this post on social media to help spread the word:</p>
<ul>
<li><a href="https://bsky.app/profile/bram.us/post/3mvxqk2gl5k2g">🦋 Bluesky</a></li>
<li><a href="https://front-end.social/@bramus/117304591336096060">🐘 Mastodon</a></li>
<li><a href="https://lnkd.in/p/en8QPRNN">💼 LinkedIn</a></li>
</ul>
<p style="text-align: center; font-size: 28px; font-family: 'times new roman', times; margin: 3em 0;">~</p>
<div class="note">
	<p><b>🔥 Like what you see? Want to stay in the loop? Here's how:</b></p>
	<ul>
            <li><a href="https://bsky.app/profile/bram.us">🦋 Follow @bram.us on Bluesky</a></li>
            <li><a href="https://bram.us/feed">🔸 Follow bram.us using RSS</a></li>
	</ul>
	<p>I can also be found on <a href="https://x.com/bramus">𝕏 Twitter</a> and <a href="https://front-end.social/@bramus">🐘 Mastodon</a> but only post there sporadically.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bram.us/2026/09/20/npm-publish-subfolder/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Introducing &#x3C;&#x200B;rich-input&#x3E;, a GitHub-like search/filter text input to embed on your site</title>
		<link>https://www.bram.us/2026/09/14/introducing-rich-input-a-github-like-search-filter-text-input-to-embed-on-your-site/</link>
					<comments>https://www.bram.us/2026/09/14/introducing-rich-input-a-github-like-search-filter-text-input-to-embed-on-your-site/#comments</comments>
		
		<dc:creator><![CDATA[Bramus!]]></dc:creator>
		<pubDate>Mon, 14 Sep 2026 21:38:37 +0000</pubDate>
				<category><![CDATA[Original Content]]></category>
		<category><![CDATA[custom elements]]></category>
		<category><![CDATA[forms]]></category>
		<category><![CDATA[opaquerange]]></category>
		<category><![CDATA[search]]></category>
		<guid isPermaLink="false">https://www.bram.us/?p=36750</guid>

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



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