<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Nathan Knowler</title>
        <link>https://knowler.dev/</link>
        <description>Some words.</description>
        <lastBuildDate>Mon, 24 Aug 2026 22:26:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Deno</generator>
        <language>en-CA</language>
        <copyright>Code and content by Nathan Knowler. Except if noted otherwise, content on this website is licensed under a CC BY-NC-SA 4.0 license.</copyright>
        <item>
            <title><![CDATA[2026 CSS wishlist]]></title>
            <link>https://knowler.dev/blog/2026-css-wishlist</link>
            <guid isPermaLink="false">2026-css-wishlist</guid>
            <pubDate>Wed, 07 Jan 2026 15:58:44 GMT</pubDate>
            <description><![CDATA[What I’d like to see prioritized for CSS in 2026, how those features will allow us to create better web-based design systems, and how they renew separation of concerns.]]></description>
            <content:encoded><![CDATA[<p>In 2024, I started the year with <a href="https://knowler.dev/blog/2024-css-wishlist">my CSS wishlist</a>. Since then, many of those wishes have been fulfilled, particularly in 2025. Style queries, the <code>@scope</code> rule, anchor positioning, and view transitions are all (almost) available in all browsers. This year I want to switch it up a bit and describe a few features that I’m looking forward to and ultimately what I think they will allow us to do with CSS.</p>
<h2 id="mixins">Mixins</h2>
<p>I want to be able to create presentational abstractions that only exist in CSS. “Wait,” you might say, “isn’t that how CSS already works?” While, yes, we do use CSS to create rules for various selectors, which can be seen as their own abstraction, those are dependant on the matching HTML. Today, there’s no real way to create a ruleset abstraction in CSS, unless you want <a href="https://bsky.app/profile/knowler.dev/post/3m6t345pslh2b">to abuse <code>@keyframes</code></a>.</p>
<p><a href="https://drafts.csswg.org/css-mixins-1/">CSS Mixins</a> would allow me to create a presentational abstraction, then apply it to multiple selectors throughout my stylesheet.</p>
<figure><pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">style</span>></span><span class="css">
<span class="hljs-keyword">@mixin</span> --button() {
	<span class="hljs-comment">/* Button styles */</span>
}

<span class="hljs-selector-tag">button</span> {
	<span class="hljs-keyword">@apply</span> --button();
}

<span class="hljs-selector-class">.button</span> {
	<span class="hljs-keyword">@apply</span> --button();
}
</span><span class="hljs-tag">&#x3C;/<span class="hljs-name">style</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">button</span>></span>A real button<span class="hljs-tag">&#x3C;/<span class="hljs-name">button</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>></span>A link that looks like a button<span class="hljs-tag">&#x3C;/<span class="hljs-name">a</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">my-button</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">template</span> <span class="hljs-attr">shadowrootmode</span>=<span class="hljs-string">open</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">style</span>></span><span class="css">
			<span class="hljs-selector-pseudo">:host</span> {
				<span class="hljs-keyword">@apply</span> --button();
			}
		</span><span class="hljs-tag">&#x3C;/<span class="hljs-name">style</span>></span>
	<span class="hljs-tag">&#x3C;/<span class="hljs-name">template</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">my-button</span>></span>
</code></pre><figcaption>Figure 1: a button component mixin which is applied to multiple light DOM selectors and used within a shadow root (that’s using Declarative Shadow DOM for demonstration purposes).</figcaption></figure>
<p>Currently, spec work for CSS Mixins has already begun and they have an early prototype in Chromium-based browsers.</p>
<h2 id="functions">Functions</h2>
<p>I want to be able to include the algorithms that I use to create typescales, colour palettes, etc. directly in my CSS. I don’t want to need to use static custom properties for this. I want to be able to get the fifth step of my typescale with my own custom CSS function.</p>
<figure><pre><code class="hljs language-css"><span class="hljs-keyword">@function</span> --my-typescale(--step &#x3C;integer>) returns &#x3C;length> {
	result: <span class="hljs-built_in">calc</span>(<span class="hljs-built_in">var</span>(--step) * <span class="hljs-number">0.25rem</span>);
}

<span class="hljs-keyword">@function</span> --my-palette(
	--color type(indigo | blue | green),
	--step &#x3C;integer>
) returns &#x3C;<span class="hljs-attribute">color</span>> {
	<span class="hljs-comment">/* Complex colour palette logic */</span>
}

<span class="hljs-selector-class">.component</span> {
	<span class="hljs-attribute">font-size</span>: <span class="hljs-built_in">--my-typescale</span>(<span class="hljs-number">5</span>);
	<span class="hljs-attribute">color</span>: <span class="hljs-built_in">--my-palette</span>(indigo, <span class="hljs-number">500</span>);
}
</code></pre><figcaption>Figure 2: a CSS function for getting a value in a typescale or a colour palette.</figcaption></figure>
<p>Today, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@function">CSS functions</a> are available in Chromium-based browsers.</p>
<h2 id="the-if-function">The <code>if()</code> function</h2>
<p>I want to be able to colocate all of the variations for a single property in one declaration rather than in different selectors, at-rules, etc.</p>
<figure><pre><code class="hljs language-css"><span class="hljs-keyword">@mixin</span> --component() {
	<span class="hljs-attribute">font-size</span>: <span class="hljs-built_in">--my-typescale</span>(<span class="hljs-built_in">if</span>(
		<span class="hljs-built_in">style</span>(--component-size, large): <span class="hljs-number">7</span>;
		style(<span class="hljs-attr">--component-size</span>, small): <span class="hljs-number">3</span>;
		else: <span class="hljs-number">5</span>;
	));
	<span class="hljs-attribute">color</span>: <span class="hljs-built_in">--my-palette</span>(indigo, <span class="hljs-built_in">if</span>(
		<span class="hljs-built_in">style</span>(--component-state, hover): <span class="hljs-number">300</span>;
		style(<span class="hljs-attr">--component-state</span>, active): <span class="hljs-number">700</span>;
		else: <span class="hljs-number">500</span>;
	));
}

<span class="hljs-selector-class">.component</span> {
	<span class="hljs-keyword">@apply</span> --component();

	&#x26;<span class="hljs-selector-pseudo">:hover</span> {
		<span class="hljs-attr">--component-state</span>: hover;
	}

	&#x26;<span class="hljs-selector-pseudo">:active</span> {
		<span class="hljs-attr">--component-state</span>: active;
	}
}
</code></pre><figcaption>Figure 3: a mixin where the <code>if()</code> function is used to set a parameter of the CSS functions mentioned in Figure 2 for the <code>font-size</code> and <code>color</code> properties based on the value of a <code>--component-state</code> property. This latter custom property is set when the mixin is applied to a concrete selector.</figcaption></figure>
<p>Today, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/if">the <code>if()</code> function</a> is available in Chromium-based browsers.</p>
<h2 id="a-css-powered-dsl-for-design-systems">A CSS-powered DSL for design systems</h2>
<p>Features like these allow us to effectively create our own DSL (domain-specific language) for a design system.</p>
<p><strong>Functions</strong> allow us to encode some of the algorithms behind our design decisions into our stylesheets. They ultimately can reduce the amount of custom properties that are necessary since they allow us to generate values using parameters.</p>
<p><strong>Mixins</strong> allow us to create presentational abstractions that can be flexibly applied to many framework implementations and can even be used by consumers in <i>ad hoc</i> way where they previously might have either misapplied a component in a semantically inappropriate scenario or even fork styles to work for their own non-matching markup.</p>
<p>The <strong>if()</strong> function allows us to more powerfully leverage an element’s own custom properties and colocate similar rules, avoiding needing to deal with various selectors with differing specificity along the way.</p>
<h2 id="a-renewed-separation-of-concerns">A renewed separation of concerns</h2>
<p>Empowering CSS in these ways makes it easier to keep markup concerns separate from presentational concerns. There will always need to be an implemenetation layer where presentation is matched to markup using selectors, but with these features, that layer will become much clearer and descriptive.</p>
<figure><pre><code class="hljs language-css"><span class="hljs-selector-tag">button</span> {
	<span class="hljs-keyword">@apply</span> --button();

	&#x26;<span class="hljs-selector-attr">[aria-expanded=<span class="hljs-string">"true"</span>]</span> {
		<span class="hljs-attr">--button-state</span>: open;
	}
}

cta-<span class="hljs-selector-tag">button</span> {
	<span class="hljs-keyword">@apply</span> --button(large);

	&#x26;:<span class="hljs-built_in">state</span>(open) {
		<span class="hljs-attr">--button-state</span>: open;
	}
}
</code></pre><figcaption>Figure 4: applying a button mixin to two elements that use different attributes for different presentational states. The latter element uses a different default size for the mixin.</figcaption></figure>
<p>Features that we have today, such as <code>@scope</code> reduce the need for complicated selector naming conventions or even highly specific complex/compound selectors. Paired with these new features, we can easily avoid needing to touch our markup to apply the styles, yet have maintainable styles that are descriptive.</p>
<figure><pre><code class="hljs language-css"><span class="hljs-keyword">@scope</span> (body) to (main) {
	<span class="hljs-selector-tag">header</span> <span class="hljs-selector-pseudo">:any-link</span> {
		<span class="hljs-keyword">@apply</span> --pill();
	}
}
</code></pre><figcaption>Figure 5: applying a “pill” mixin to any link within the semantic banner of the document (i.e. the <code>header</code> element that’s scoped to the body and not a sectioning element like <code>main</code>).</figcaption></figure>
<p>With features like these, separation of concerns becomes much easier to achieve. Markup can describe our content’s semantics and CSS can describe presentation. We don’t need to endlessly come up with new class name conventions (whether human or machine readable) or tweak our favourite utility class generator with every new CSS feature that’s introduced.</p>
<h2 id="optimism-and-gratitude">Optimism and gratitude</h2>
<p>It’s an exciting time to be writing CSS. It’s come a very long way from when I made my first web page twenty years ago. I’m grateful for those behind the scenes doing the hard work of both slugging away at bugs, optimizing performance, and coming up with new features. Cheers!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[2025 in music]]></title>
            <link>https://knowler.dev/blog/2025-in-music</link>
            <guid isPermaLink="false">2025-in-music</guid>
            <pubDate>Wed, 31 Dec 2025 07:35:09 GMT</pubDate>
            <description><![CDATA[My favourite new music in 2025 as well as the live shows I saw.]]></description>
            <content:encoded><![CDATA[<p>It has been a while since I’ve done a year-end music-in-review here on the blog.
Apparently, <a href="https://knowler.dev/blog/2020-in-music">2020</a> was the last time.
I’ve definitely done it on the socials, but those get lost in the noise and the
blog is a much nicer place to record it, even though composing a thread
elsewhere definitely helps me get my thoughts going. This year I’ve got my
favourite albums of the year and a record of all of the shows I went to this
year.</p>
<h2 id="top-five-albums">Top five albums</h2>
<p>This year, I’ll just go through my top five, counting down. Beyond that is
really hard to make up my mind.</p>
<h3 id="5-life-is-violence-by-idle-heirs">5. <a href="https://album.link/ca/i/1780850732"><em>Life is Violence</em> by Idle Heirs</a></h3>
<p>I love the dynamics and textures of this record. From the walls of sound to the
slow western-y guitar bits—it’s a musical sweet spot for me. I returned to it a
lot and I’m glad they released <a href="https://album.link/ca/i/1843946092">an instrumental
version</a> which was a staple for my writing
sessions.</p>
<p>Favourite track: <a href="https://song.link/ca/i/1780850741">“Lemonade Stands”</a></p>
<h3 id="4-i-dont-want-to-see-you-in-heaven-by-the-callous-daoboys">4. <a href="https://album.link/ca/i/1794618631"><em>I Don’t Want to See You in Heaven</em> by The Callous Daoboys</a></h3>
<p>What can I say? Ugga-ugga-boo, ugga-boo-boo-ugga?</p>
<p>Favourite track: <a href="https://song.link/ca/i/1794619258">“Idiot Temptation Force”</a></p>
<h3 id="3-are-we-all-angels-by-scowl">3. <a href="https://album.link/ca/i/1785686511"><em>Are We All Angels</em> by Scowl</a></h3>
<p>This would be number one by a longshot if it were up to playtime alone. Indigo
(6 years old) and I had this album on repeat constantly, singing along. It’s one
of those special albums that will always remind me of this time in her life.</p>
<p>Favourite track: <a href="https://song.link/ca/i/1785686514">“B.A.B.E.”</a></p>
<h3 id="2-spinner-by-eyes">2. <a href="https://album.link/i/1780425831"><em>SPINNER</em> by EYES</a></h3>
<p>I knew this album would be my favourite hardcore album of the year as soon as I
heard it. Definitely darker than their previous two albums. I love how the album
is both driving, but makes room to breathe. A certified <em>spinner</em>.</p>
<p>Favourite track: <a href="https://song.link/ca/i/1780425857">“Beelzebub, the Hypocrite”</a></p>
<h3 id="1-lonely-people-with-power-by-deafheaven">1. <a href="https://album.link/ca/i/1792077173"><em>Lonely People With Power</em> by Deafheaven</a></h3>
<p>For record that runs just over an hour, this one has me captivated the entire
time and gives me chills throughout.</p>
<p>Favourite track: <a href="https://song.link/ca/i/1792077185">“Winona”</a></p>
<h3 id="honourable-mentions">Honourable mentions</h3>
<ul>
<li>Stray from the Path’s <a href="https://album.link/ca/i/1816670397"><em>Clockworked</em></a></li>
<li>Dying Wish’s <a href="https://album.link/ca/i/1827219997"><em>Flesh Stays Together</em></a></li>
<li>Rolo Tomassi’s <a href="https://album.link/ca/i/1845299841"><em>In The Echoes Of All Dreams</em></a> (EP)</li>
<li>Counterparts’ <a href="https://album.link/ca/i/1778052244"><em>Heaven Let Them Die</em></a> (EP)</li>
<li>Agriculture’s <a href="https://album.link/ca/i/1821247551"><em>The Spiritual Sound</em></a></li>
<li>Turnstile’s <a href="https://album.link/ca/i/1805820903"><em>NEVER ENOUGH</em></a></li>
<li>La Dispute’s <a href="https://album.link/ca/i/1811680398"><em>No One Was Driving the Car</em></a></li>
<li>Pile’s <a href="https://album.link/ca/i/1807843726"><em>Sunshine and Balance Beams</em></a></li>
<li>The Armed’s <a href="https://album.link/ca/i/1809236820"><em>THE FUTURE IS HERE AND EVERYTHING NEEDS TO BE DESTROYED</em></a></li>
<li>Between the Buried and Me’s <a href="https://album.link/ca/i/1816935990"><em>The Blue Nowhere</em></a></li>
</ul>
<h2 id="live-shows">Live shows</h2>
<p>In February, I saw Meantime, Cancer Bats, Orthodox, and Comeback Kid for the
latter’s 25th anniversary show in Winnipeg. This was Indigo’s first show. She
had a blast, but we were playing Echoes of Wisdom on the Switch for all of the
openers which was a very strange experience for me—also, sitting in a balcony at
a hardcore show was weird, but comfortable. Then, shortly after, at the start of
March, I travelled to Calgary to see Gouge Away and Chat Pile. I’m so happy
Gouge Away played <a href="https://song.link/ca/i/1163416546">“Uproar.”</a>.</p>
<p>In May, I saw Juice, Trench, Whispers, and SPEED in Winnipeg.</p>
<p>In October, as a way to celebrate my 30th birthday, I travelled to Birmingham,
Alabama, to Furnace Fest where I saw Gideon, Pig Pen, Madball, Microwave, SPEED
(again), Comeback Kid (again), DRAIN, Mom Jeans, Kublai Khan, Jimmy Eat World,
Meadows, BIG ASS TRUCK I.E., As Cities Burn, Anberlin, Rolo Tomassi, Zao,
Counterparts, Johnny Booth, Boundaries, XweaponX, Stray from the Path, Many
Eyes, The Tony Danza Tapdance Extravaganza, Terror, Norma Jean, Converge, Emery,
The Dillinger Escape Plan, and Knocked Loose. So… pretty much all of my
favourite bands. It was amazing. Then a week later I saw 156/Silence, thrown,
and Counterparts (again) in Winnipeg.</p>
<h2 id="2026">2026</h2>
<p>I’m really looking forward to Converge’s new album, <a href="https://album.link/ca/i/1848413292"><em>Love Is Not
Enough</em></a>. After seeing them at Furnace Fest
and since they announced the new album, I’ve been listening to them non-stop.
Outside of that, I’m actually not super aware of what else is coming in 2026.
Maybe new Silent Planet? Norma Jean?</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Searchable, collapsible sections with hidden=until-found]]></title>
            <link>https://knowler.dev/blog/searchable-collapsible-sections-with-hidden-until-found</link>
            <guid isPermaLink="false">searchable-collapsible-sections-with-hidden-until-found</guid>
            <pubDate>Tue, 16 Sep 2025 14:17:10 GMT</pubDate>
            <description><![CDATA[Using the new hidden=until-found attribute-value combo as a progressive enhancement to make your existing collapsible sections searchable.]]></description>
            <content:encoded><![CDATA[<aside role="note" aria-labelledby="caution"><p><strong id="caution">Caution</strong>: there is a significant bug in Safari and VoiceOver which make this pattern worth avoiding until it’s fixed. See the <a href="/blog/searchable-collapsible-sections-with-hidden-until-found#update-june-26-2026">“Update: June 26, 2026”</a> for more details.</p></aside>
<p>Recently, I found myself needing to implement collapsible sections. For this particular use case, using <code>&#x3C;details></code> and <code>&#x3C;summary></code> elements was too much of a hassle because the CMS did not recognize them as elements and I don’t particularly like nesting headings within a <code>&#x3C;summary></code> element, since it makes discovering the expand and collapse trigger more difficult for those <a href="https://tetralogical.com/blog/2022/02/28/headings/">navigating by headings</a>. So, I’m turning to the tried and true <a href="https://inclusive-components.design/collapsible-sections/">“Collapsible Sections” pattern</a>.</p>
<p>I’ve used this pattern for accordion components in design systems, built custom elements with it, and used it for cases like this. Here’s what the markup generally looks like:</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">section</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">h2</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"section-0-trigger"</span> <span class="hljs-attr">aria-expanded</span>=<span class="hljs-string">"false"</span> <span class="hljs-attr">aria-controls</span>=<span class="hljs-string">"section-0-content"</span>></span>
			Hello, World!
		<span class="hljs-tag">&#x3C;/<span class="hljs-name">button</span>></span>
	<span class="hljs-tag">&#x3C;/<span class="hljs-name">h2</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"section-0-content"</span> <span class="hljs-attr">hidden</span>></span>Hideable content…<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">section</span>></span>
</code></pre>
<p>The JavaScript for it is pretty simple as well:</p>
<pre><code class="hljs language-javascript"><span class="hljs-comment">// Simplified</span>
button.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">"click"</span>, <span class="hljs-function">() =></span> {
	content.<span class="hljs-property">hidden</span> = !content.<span class="hljs-property">hidden</span>;
	button.<span class="hljs-property">ariaExpanded</span> = content.<span class="hljs-property">hidden</span> ? <span class="hljs-string">"false"</span> : <span class="hljs-string">"true"</span>;
});
</code></pre>
<p>While implementing the pattern, I realized that rather than just using <code>hidden</code> as a boolean attribute on its own, I can use the newer <code>until-found</code> attribute-value, so that I could allow the contents of the disclosures to be <em>searchable</em>.</p>
<p>As of writing this, <code>hidden=until-found</code> is only available in Chrome and Firefox. Safari will likely include it in an upcoming release as it is currently available in their latest Technology Preview.</p>
<p>I’ve written on <a href="https://knowler.dev/blog/polyfilling-hidden-until-found">how to polyfill <code>hidden=until-found</code></a> earlier this year, but unfortunately that doesn’t work in Safari since there’s no good way to observe its “find-in-page” feature. I can still use <code>hidden=until-found</code> though, because it was designed with <strong>progressive enhancement</strong> in mind.</p>
<p>Unsupporting browsers will hide content visibly and accessibly for any value provided to the <code>hidden</code> attribute (bear in mind that the element <em>property</em> works differently when given <em>boolean</em> values). For those cases, <code>hidden=until-found</code> will work like <code>hidden</code> on its own, since the attribute is present.</p>
<p>Here’s our updated markup:</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">section</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">h2</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"section-0-trigger"</span> <span class="hljs-attr">aria-expanded</span>=<span class="hljs-string">"false"</span> <span class="hljs-attr">aria-controls</span>=<span class="hljs-string">"section-0-content"</span>></span>
			Hello, World!
		<span class="hljs-tag">&#x3C;/<span class="hljs-name">button</span>></span>
	<span class="hljs-tag">&#x3C;/<span class="hljs-name">h2</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"section-0-content"</span> <span class="hljs-attr">hidden</span>=<span class="hljs-string">"until-found"</span>></span>Hideable, but searchable content…<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">section</span>></span>
</code></pre>
<p>We’ll need to slightly adjust the JS to use <code>until-found</code> as the value for the attribute when collapsing. Since <code>"until-found"</code> is a truthy value in JavaScript, we’re relying on that both in the condition and when setting the property which sets a boolean in unsupported browsers.</p>
<pre><code class="hljs language-javascript">button.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">"click"</span>, <span class="hljs-function">() =></span> {
	content.<span class="hljs-property">hidden</span> = content.<span class="hljs-property">hidden</span> ? <span class="hljs-literal">false</span> : <span class="hljs-string">"until-found"</span>;
	button.<span class="hljs-property">ariaExpanded</span> = content.<span class="hljs-property">hidden</span> ? <span class="hljs-string">"false"</span> : <span class="hljs-string">"true"</span>;
});
</code></pre>
<p>Since the content can be expanded when it contains a match for a find-in-page query, I’ll need to listen for the <code>beforematch</code> event for the content, so that I can update the <code>&#x3C;button></code> element’s <code>aria-expanded</code> attribute value to reflect when the content is no longer hidden.</p>
<pre><code class="hljs language-javascript">content.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">"beforematch"</span>, <span class="hljs-function">() =></span> {
	button.<span class="hljs-property">ariaExpanded</span> = <span class="hljs-string">"true"</span>;
});
</code></pre>
<p>So, there you go: with not a whole lot of changes your collapsible sections are now searchable as a progressive enhancement.</p>
<p>Again, if <code>&#x3C;details></code> and <code>&#x3C;summary></code> elements are suitable use those instead — this is <a href="https://www.w3.org/TR/using-aria/#firstrule">the first rule of ARIA usage</a>. They have the same searchable benefit as <code>hidden=until-found</code> and you don’t need any extra HTML or JavaScript to get them working.</p>
<h2 id="demo">Demo</h2>
<p>You can check out a live demo of a searchable, collapsible section below, but it likely will work better <a href="https://knowler.dev/demos/EgY5CWL?codepen">on a standalone page</a> to avoid searching this parent document.</p>
<iframe src="https://knowler.dev/demos/EgY5CWL?codepen" width="100%" height="400px"></iframe>
<h2 id="update-june-26-2026">Update: June 26, 2026</h2>
<p>I’ve become aware of a Safari/VoiceOver bug (<a href="https://bugs.webkit.org/show_bug.cgi?id=310623">Bug 310623</a>) where if you programmatically remove <code>hidden=until-found</code> from an element the accessibility tree doesn’t seem to update to include the content. When content is matched with find-in-page, this doesn’t seem to be an issue. I don’t believe this would have been an issue when I wrote this article as I usually test pretty thoroughly with screen readers. Anyway, either avoid this pattern until that’s fixed or hopefully pressure WebKit to fix it, as the bug was opened three months ago and still doesn’t have a fix.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[An intro to manual slot assignment]]></title>
            <link>https://knowler.dev/blog/an-intro-to-manual-slot-assignment</link>
            <guid isPermaLink="false">an-intro-to-manual-slot-assignment</guid>
            <pubDate>Wed, 03 Sep 2025 19:54:58 GMT</pubDate>
            <description><![CDATA[Exploring more powerful Shadow DOM composition using manual slot assignment.]]></description>
            <content:encoded><![CDATA[<p>Composition may be the killer feature of the Shadow DOM. By “composition” we mean composing an element’s light DOM children within its encapsulated shadow tree using <code>&#x3C;slot></code> elements.</p>
<p>This reduces the boilerplate required by those using a custom element and it helps reduce its API which makes it more maintainable since the entire DOM tree isn’t public by default.</p>
<p><a href="https://knowler.dev/blog/on-slot-elements">I’ve written about <code>&#x3C;slot></code> elements before</a> focusing how the Web platform’s slotting works differently from other forms of slotting and pointing out that the <code>&#x3C;slot></code> element itself can be styled despite its user agent styles (i.e. <code>display: contents</code>). In this post, I’m going to talk about slot assignment and explore a lesser known feature of it.</p>
<h2 id="the-default-named-slot-assignment">The default named slot assignment</h2>
<p>The default named slot assignment behaviour of a shadow tree is to assign an element’s child nodes (i.e. elements and text nodes) to a corresponding <code>&#x3C;slot></code> element.</p>
<p>Text nodes and element nodes without a <code>slot</code> attribute are assigned to the first unnamed slot.</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span>></span>
	<span class="hljs-comment">&#x3C;!-- Declarative shadow DOM for visualization purposes only. Often this is set inside of the custom element definition. --></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">template</span> <span class="hljs-attr">shadowrootmode</span>=<span class="hljs-string">open</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span>
	<span class="hljs-tag">&#x3C;/<span class="hljs-name">template</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>I appear within the unnamed slot<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
	I also appear within the unnamed slot.
<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
</code></pre>
<p>Element nodes with a <code>slot</code> attribute are assigned to the corresponding named slots (i.e. <code>&#x3C;slot></code> elements with a <code>name</code> attribute).</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">my-article</span>></span>
	<span class="hljs-comment">&#x3C;!-- Declarative shadow DOM for visualization purposes only. Often this is set inside of the custom element definition. --></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">template</span> <span class="hljs-attr">shadowrootmode</span>=<span class="hljs-string">open</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">hgroup</span>></span>
			<span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span> <span class="hljs-attr">name</span>=<span class="hljs-string">title</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span>
			<span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span> <span class="hljs-attr">name</span>=<span class="hljs-string">subtitle</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span>
		<span class="hljs-tag">&#x3C;/<span class="hljs-name">hgroup</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">article-content</span>></span>
			<span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span>
		<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
	<span class="hljs-tag">&#x3C;/<span class="hljs-name">template</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">h1</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">title</span>></span>Article title appears within the title slot<span class="hljs-tag">&#x3C;/<span class="hljs-name">h1</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">p</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">subtitle</span>></span>Article subtitle appears within the subtitle slot<span class="hljs-tag">&#x3C;/<span class="hljs-name">p</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">p</span>></span>Article content appears in the unnamed slot.<span class="hljs-tag">&#x3C;/<span class="hljs-name">p</span>></span>
	I also appear within the unnamed slot.
<span class="hljs-tag">&#x3C;/<span class="hljs-name">my-article</span>></span>
</code></pre>
<p>Nodes that do not have a corresponding slot whether named or unnamed are ignored.</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span>></span>
	<span class="hljs-comment">&#x3C;!-- Declarative shadow DOM for visualization purposes only. Often this is set inside of the custom element definition. --></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">template</span> <span class="hljs-attr">shadowrootmode</span>=<span class="hljs-string">open</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span> <span class="hljs-attr">name</span>=<span class="hljs-string">content</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span>
	<span class="hljs-tag">&#x3C;/<span class="hljs-name">template</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">span</span> <span class="hljs-attr">slot</span>=<span class="hljs-string">content</span>></span>I appear within the named slot<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
	I don’t appear anywhere.
<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
</code></pre>
<p>As you can see, slots are very powerful. They enable composition. They allow for mixed-order rendering which is incredibly useful for HTML streaming (e.g. for an app use case you could send the “app shell” in the first chunk and then chunk over content as it becomes available).</p>
<p>Named slot assignment works well for many simple use cases.</p>
<p>What if you want to wrap each individual slotted node with an element within the shadow tree? What if you only want to accept certain types of nodes or elements? You might think this is impossible or maybe even a design flaw of the Shadow DOM, but it does provide a method to do this.</p>
<h2 id="enter-manual-slot-assignment">Enter manual slot assignment</h2>
<p>Manual slot assignment allows you to decide how an element’s child nodes get slotted.</p>
<p>You enable this when first attaching the shadow tree:</p>
<pre><code class="hljs language-javascript">element.<span class="hljs-title function_">attachShadow</span>({
	<span class="hljs-attr">mode</span>: <span class="hljs-string">"open"</span>,
	<span class="hljs-attr">slotAssignment</span>: <span class="hljs-string">"manual"</span>,
});
</code></pre>
<p>Once enabled, child nodes will not be auto-assigned to any <code>&#x3C;slot></code> element inside of the the shadow tree. That responsibility is all yours.</p>
<aside role="note" aria-labelledby="no-declarative-equivalent"><p>Note that there is <strong id="no-declarative-equivalent">no declarative equivalent</strong> to this which makes sense since all of the slot assignment code is going to need to be in JavaScript anyway. I have thoughts on how this might be done declaratively in the future, but for now, we’ll just explore it imperatively.</p></aside>
<h3 id="assigning-to-slots">Assigning to slots</h3>
<p>Nodes are assigned to slot elements using <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assign">the <code>assign()</code> method of the <code>HTMLSlotElement</code> interface</a>. Only nodes that are direct children of a shadow host, can be assigned to slots within its shadow tree.</p>
<p>The <code>assign()</code> method takes an <em>ordered</em> list of nodes and will unassign any node previously assigned to the slot when called.</p>
<pre><code class="hljs language-javascript"><span class="hljs-comment">// 0. Attach the open shadow root with manual slot assignment enabled</span>
element.<span class="hljs-title function_">attachShadow</span>({ <span class="hljs-attr">mode</span>: <span class="hljs-string">"open"</span>, <span class="hljs-attr">slotAssignment</span>: <span class="hljs-string">"manual"</span> });

<span class="hljs-comment">// 1. Create a slot element and append it to the shadow root. </span>
<span class="hljs-keyword">const</span> slot = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">"slot"</span>);
element.<span class="hljs-property">shadowRoot</span>.<span class="hljs-title function_">append</span>(slot);

<span class="hljs-comment">// 2. Find slottable nodes — must be either text or element nodes.</span>
<span class="hljs-keyword">const</span> slottables = <span class="hljs-title class_">Array</span>.<span class="hljs-title function_">from</span>(element.<span class="hljs-property">childNodes</span>).<span class="hljs-title function_">filter</span>(
	<span class="hljs-function"><span class="hljs-params">node</span> =></span> node.<span class="hljs-property">nodeType</span> === <span class="hljs-title class_">Node</span>.<span class="hljs-property">ELEMENT_NODE</span> || node.<span class="hljs-property">nodeType</span> === <span class="hljs-title class_">Node</span>.<span class="hljs-property">TEXT_NODE</span>
);

<span class="hljs-comment">// 3. Assign the slottable nodes to the slot.</span>
slot.<span class="hljs-title function_">assign</span>(...slottables);
</code></pre>
<p>That’s the gist of how to assign nodes to a slot manually.</p>
<h3 id="accessing-slotted-nodes">Accessing slotted nodes</h3>
<p>There are two methods to accessing what’s assigned to a slot:</p>
<ol>
<li>The <code>assignedNodes()</code> method which includes all assignable nodes (i.e. text and element nodes).</li>
<li>The <code>assignedElements()</code> method which only includes the element nodes.</li>
</ol>
<p>Both of these methods also take an optional object of options with <code>flatten</code> as the only option currently available. This option defaults to <code>false</code>, but when set to <code>true</code> it will return the slot element’s children (i.e. the fallback content of the slot) if there is nothing assigned to it.</p>
<pre><code class="hljs language-javascript">slot.<span class="hljs-title function_">assignedNodes</span>({ <span class="hljs-attr">flatten</span>: <span class="hljs-literal">true</span> });
</code></pre>
<h2 id="practical-use-case-wrapping-slotted-elements">Practical use case: wrapping slotted elements</h2>
<p>Say you have an element called <code>&#x3C;fruit-bowl></code>. This takes a list of fruit as direct child elements.</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">fruit-bowl</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>Apple<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>Cherry<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>Apricot<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>Banana<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">fruit-bowl</span>></span>
</code></pre>
<p>In the shadow tree, we have an <code>&#x3C;ul></code> element:</p>
<pre><code class="hljs language-html"><span class="hljs-comment">&#x3C;!-- shadow tree content --></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">ul</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">ul</span>></span>
</code></pre>
<p>Within that, each slotted fruit should be wrapped with an <code>&#x3C;li></code> element:</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">li</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">li</span>></span>
</code></pre>
<p>The resulting “composed tree” should look like this:</p>
<pre><code class="hljs language-html"><span class="hljs-comment">&#x3C;!-- This is a composed tree: it does not represent the real DOM structure --></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">fruit-bowl</span>></span>
	<span class="hljs-tag">&#x3C;<span class="hljs-name">ul</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">li</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>Apple<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">li</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">li</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>Cherry<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">li</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">li</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>Apricot<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">li</span>></span>
		<span class="hljs-tag">&#x3C;<span class="hljs-name">li</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>Banana<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">li</span>></span>
	<span class="hljs-tag">&#x3C;/<span class="hljs-name">ul</span>></span>
<span class="hljs-tag">&#x3C;/<span class="hljs-name">fruit-bowl</span>></span>
</code></pre>
<p>When using manual slot assignment, <code>MutationObserver</code>s are generally what you use to watch for changes of what’s slottable.</p>
<p>Generally, you’ll at least want a mutation observer that’s observing the shadow host’s light DOM children. In some cases, you might want an observer for the shadow tree as well.</p>
<p>It’s also important to be aware of at what point in the lifecycle of the document that the mutation observer has started. If it’s guaranteed that the observer has started early enough either as soon as the element is created or before its contents can be parsed, then there will be no need to check for existing slottable nodes. In other cases, it is necessary to check for existing slottable nodes when the element is connected (i.e. in custom elements check in the <code>connectedCallback()</code>).</p>
<p>To keep things simple, let’s assume the <code>&#x3C;fruit-bowl></code>’s custom element registration took place in a parser blocking way within the document’s <code>&#x3C;head></code>, so we don’t need to deal with any existing children. We’ll also only assume that the mutation observer is handling one entry at a time.</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> slottableObserver = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MutationObserver</span>(<span class="hljs-function">(<span class="hljs-params">entries</span>) =></span> {
	<span class="hljs-keyword">const</span> { childNodes, shadowRoot } = entries[<span class="hljs-number">0</span>].<span class="hljs-property">target</span>;

	<span class="hljs-comment">// Reset the shadow root with an unordered list element.</span>
	shadowRoot.<span class="hljs-property">innerHTML</span> = <span class="hljs-string">"&#x3C;ul>"</span>;
	<span class="hljs-keyword">const</span> list = shadowRoot.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">"ul"</span>);

	<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> node <span class="hljs-keyword">of</span> childNodes) {
		<span class="hljs-comment">// Filter out non-elements</span>
		<span class="hljs-keyword">if</span> (node.<span class="hljs-property">nodeType</span> !== <span class="hljs-title class_">Node</span>.<span class="hljs-property">ELEMENT_NODE</span>) <span class="hljs-keyword">continue</span>;
		<span class="hljs-comment">// Stub out the list item.</span>
		<span class="hljs-keyword">const</span> listItem = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">"li"</span>);
		<span class="hljs-comment">// Create and append a slot element within the list item.</span>
		<span class="hljs-keyword">const</span> slot = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">"slot"</span>);
		listItem.<span class="hljs-title function_">append</span>(slot);
		<span class="hljs-comment">// Append to the connected list, so we can assign to it.</span>
		list.<span class="hljs-title function_">append</span>(listItem);
		<span class="hljs-comment">// Assign the corresponding light DOM child element.</span>
		slot.<span class="hljs-title function_">assign</span>(node);
	}
});

customElements.<span class="hljs-title function_">define</span>(<span class="hljs-string">"fruit-bowl"</span>, <span class="hljs-keyword">class</span> <span class="hljs-title class_">extends</span> <span class="hljs-title class_">HTMLElement</span> {
	<span class="hljs-title function_">constructor</span>(<span class="hljs-params"></span>) {
		<span class="hljs-variable language_">super</span>();
		<span class="hljs-variable language_">this</span>.<span class="hljs-title function_">attachShadow</span>({
			<span class="hljs-attr">mode</span>: <span class="hljs-string">"open"</span>,
			<span class="hljs-attr">slotAssignment</span>: <span class="hljs-string">"manual"</span>,
		});
		slottableObserver.<span class="hljs-title function_">observe</span>(<span class="hljs-variable language_">this</span>, { <span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span> });
	}
});
</code></pre>
<p>Now, whenever the element’s light DOM children change, our custom element will wrap any child elements within a list item within the shadow root — neat! Give it a try:</p>
<iframe src="https://knowler.dev/demos/xNjDYa2?codepen" width="100%" height="300px"></iframe>
<p>Obviously, this is incredibly unoptimized — and we’re not going to get into improving it — but it demonstrates the kind of composition that manual slot assignment allows.</p>
<p>Beyond simply wrapping nodes, manual slot assignment can be a great way to further reduce boilerplate in your custom element APIs, especially when dealing with sub-component relationships (e.g. consumers don’t need to explicitly assign elements with the <code>slot</code> attribute; optional wrapping elements can be omitted). You can implicitly assign elements to slots based on their DOM position or even use DOM position to provide different templates. You can filter out unsupported content.</p>
<p>Manual slot assignment is just another way that Web Components APIs and the Shadow DOM allow you to create custom elements that are like those already included in the platform.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Polyfilling hidden=until-found]]></title>
            <link>https://knowler.dev/blog/polyfilling-hidden-until-found</link>
            <guid isPermaLink="false">polyfilling-hidden-until-found</guid>
            <pubDate>Sat, 04 Jan 2025 05:15:31 GMT</pubDate>
            <description><![CDATA[Exploring what it takes to polyfill the until-found value of the hidden attribute.]]></description>
            <content:encoded><![CDATA[<p>I recently wrote <a href="https://12daysofweb.dev/2024/css-content-visibility/">an article for 12 Days of Web on CSS <code>content-visibility</code></a>. Something I wanted to highlight in writing that article is that while <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden#the_hidden_until_found_state">the new HTML feature <code>hidden=until-found</code></a> uses <a href="https://12daysofweb.dev/2024/css-content-visibility/#manually-hiding-content-with-hidden"><code>content-visibility: hidden</code></a> underneath, it works a little differently than how that property works on its own.</p>
<h2 id="content-visibility-hidden-vs-hiddenuntil-found"><code>content-visibility: hidden</code> vs. <code>hidden=until-found</code></h2>
<p>On its own, <code>content-visibility: hidden</code> will cause an element’s content to be <em>completely hidden</em>. This means it’s skipped for rendering, ignored by assistive technologies, and <em>not findable</em> using “find-in-page” browser features. The <code>until-found</code> value of the <code>hidden</code> attribute keeps the content findable which makes sense given its name, but otherwise it works the same.</p>
<p>Something I’d like to underscore is that it’s the <code>hidden=until-found</code> attribute-value pairing that makes content findable — not <code>content-visibility: hidden</code>. This means that in browsers that don’t support <code>hidden=until-found</code> yet, we can’t use <code>content-visibility: hidden</code> as a part of a polyfill. So, how would one polyfill this feature?</p>
<h2 id="hiding-content-while-remaining-findable">Hiding content while remaining findable</h2>
<p>Now, while <code>content-visibility: hidden</code> does not keep content findable, <a href="https://12daysofweb.dev/2024/css-content-visibility/#letting-the-browser-decide-using-auto"><code>content-visibility: auto</code></a> does and reflecting on this got me thinking: could it be possible to polyfill <code>hidden=until-found</code> using <code>content-visibility: auto</code>?</p>
<p>Some differences between the <code>auto</code> and <code>hidden</code> values for the <code>content-visibility</code> property:</p>
<ol>
<li><code>auto</code> keeps content accessible whether or not the content is skipped for rendering. <code>hidden</code> always ignores the content in the accessibility tree.</li>
<li><code>auto</code> skips rendering while <a href="https://12daysofweb.dev/2024/css-content-visibility/#what-makes-content-relevant">the content isn’t relevant</a>. This means that if the content is in the viewport, it will be visible.</li>
<li>Both <code>auto</code> and <code>hidden</code> apply size, layout, paint, and style <a href="https://12daysofweb.dev/2024/css-content-visibility/#css-containment">containment</a> when the content is hidden. <code>auto</code> drops <em>size</em> containment when the content is visible.</li>
<li><code>auto</code> keeps content selectable.</li>
</ol>
<p>So, we’ve got our grocery list of differences to address by using <code>content-visibility: auto</code>:</p>
<ol>
<li>We need the content ignored in the accessibility tree, but the element itself should still be accessible. This means we can’t set <code>aria-hidden=true</code> directly on the element itself.</li>
<li>We need to visually hide both elements and text nodes.</li>
<li>We need to ensure size containment is applied while the content is hidden.</li>
<li>We need to make the content unselectable.</li>
<li>We need to ensure elements cannot be clicked or focused.</li>
</ol>
<p>We can address almost all but the first of these concerns with the following CSS:</p>
<pre><code class="hljs language-css"><span class="hljs-selector-attr">[hidden=<span class="hljs-string">"until-found"</span>]</span> {
	<span class="hljs-attribute">display</span>: revert;
	<span class="hljs-attribute">content-visibility</span>: auto;
	<span class="hljs-attribute">contain</span>: size;
	user-select: none;
	<span class="hljs-attribute">color</span>: transparent;

	> * {
		<span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
		<span class="hljs-attribute">pointer-events</span>: none;
	}
}
</code></pre>
<iframe src="https://knowler.dev/demos/v9-pZyS?codepen" width="100%" height="300px"></iframe>
<p>You’ll notice we’re setting <code>display: revert</code>. Wouldn’t that revert back to the user agent style for the <code>hidden</code> attribute which would be <code>display: none</code>? Nope — the <code>hidden</code> attribute is what’s called a presentational hint and the <code>revert</code> keyword reverts presentational hints (as well as non-user agent styles). While this is usually why you’d need to <a href="https://mayank.co/blog/revert-layer/#the-nuclear-reset">exercise caution when using the <code>revert</code> keyword</a>, perhaps even opt-ing for <code>revert-layer</code> instead, this is the perfect use case: we want to revert back to the <em>element’s</em> user agent styles.</p>
<p>Now, we’re not done yet. And there are improvements to be made.</p>
<p>Setting <code>color: transparent</code> on the element itself is problematic as it affects <code>currentColor</code> which could be used by other properties of the element (e.g. like its border). Instead it would be great if we could set it on an inner element that wrapped the contents.</p>
<p>Finally, we want to make some of these styles easy to override. Others should not be an easy override. With the current styles, we need to be very specific about where and when they needed to be loaded to avoid cascading issues.</p>
<p>To make these improvements and to address the issue of accessibility, we’ll be going into the shadows…</p>
<h2 id="into-the-shadow-dom">Into the Shadow DOM</h2>
<p>We can use a shadow root to address both our styling and accessibility concerns — not a phrase you might have expected someone to say about the Shadow DOM.</p>
<p>First, inside the shadow tree we’ll use a <code>&#x3C;slot></code> element to hide any slotted content from the accessibility tree and we’ll take that content out of the tab order, since <code>&#x3C;slot></code> elements are what’s called a <em>focus navigation scope owner</em>.</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&#x3C;<span class="hljs-name">slot</span> <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">true</span> <span class="hljs-attr">tabindex</span>=<span class="hljs-string">-1</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">slot</span>></span>
</code></pre>
<p>Why <code>tabindex=-1</code> and <code>aria-hidden=true</code> instead of the <code>inert</code> attribute? The <code>inert</code> attribute does do everything we need: it makes an element and all of its content hidden from the accessibility tree, makes the content unselectable, removes content from the tab order, and prevents content from being clicked or focused. It also does something undesirable for our purposes: it makes content hidden for “find-in-page” features. So, unfortunately, we’ll need to settle with a deconstructed version of it.</p>
<p>Now, another great advantage of using a <code>&#x3C;slot></code> element is that it’s an encapsulated element inside the tree that with its default styles of <code>display: contents</code> just visually displays all of the contents: text and elements. This is great because we still want the host element to be exposed to the accessibility tree and potentially a part of the tab order. We can also use it to fix some of our styling problems.</p>
<p>In a stylesheet for our shadow root, we move the <code>color: transparent</code> rule from the host element to the <code>&#x3C;slot></code> element. This means that <code>color</code> and <code>currentColor</code> can be used on the host element, but all slotted content will be transparent.</p>
<p>Here’s what the stylesheet looks like once we introduce the <code>&#x3C;slot></code> element and adapt it for the shadow DOM:</p>
<pre><code class="hljs language-css"><span class="hljs-selector-pseudo">:host</span>(<span class="hljs-selector-attr">[hidden=<span class="hljs-string">"until-found"</span>]</span>) {
	<span class="hljs-attribute">display</span>: revert;
	<span class="hljs-attribute">content-visibility</span>: auto;
	<span class="hljs-attribute">contain</span>: size;
	user-select: none;
	<span class="hljs-attribute">background-clip</span>: initial <span class="hljs-meta">!important</span>;

	slot {
		<span class="hljs-attribute">color</span>: transparent;
		<span class="hljs-attribute">pointer-events</span>: none;
	}

	<span class="hljs-selector-pseudo">::slotted</span>(*) {
		<span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span> <span class="hljs-meta">!important</span>;
	}
}
</code></pre>
<p>A few notes:</p>
<ul>
<li>In order to use the attribute selector for the host element, we need to wrap with the <code>:host()</code> functional pseudo-class. This selector allows us to select the <em>shadow host</em> which in our case will be the element with <code>hidden=until-found</code> set on it. This element is not in the shadow tree, it’s the light DOM element that the shadow tree is attached to. That’s why we need a special selector to target it.</li>
<li>We’ve moved both the <code>color</code> and <code>pointer-events</code> properties to the <code>&#x3C;slot></code> element. This reduces the restrictions we have on the host element.</li>
<li>We’re preventing the transparent text from being clipped by setting <code>background-clip: initial !important</code> on the host. This likely isn’t common, but since we’re using <code>color: transparent</code> on a descendant using <code>background-clip: text</code> on the parent with a <code>background-color</code> could expose the content visually.</li>
<li>Since the slotted content is in the light DOM, we need to use the <code>::slotted()</code> pseudo-element function with the universal selector to apply to all of the slotted elements. We’ve also made the <code>opacity: 0</code> rule important as shadow DOM styles can be too weak to ensure this applies otherwise, since they’re cascaded before the document styles.</li>
</ul>
<p>Now that we have our styles finalized, we can start implementing the polyfilling logic to apply this to any element.</p>
<h2 id="revealing-the-content-when-found">Revealing the content when found</h2>
<p>Unfortunately, this is where we have to leave Safari by the wayside, as there’s no good way to observe its “find-in-page” feature. This means that we’re effectively only targeting Firefox.</p>
<p>While we’re preventing <em>user selection</em> with <code>user-select: none</code>, the “find-in-page” feature in Firefox selects the found text. This means that we can observe if our hidden content has been found with the <code>selectionchange</code> event on the <code>document</code> (or <code>window</code>).</p>
<pre><code class="hljs language-javascript"><span class="hljs-variable language_">document</span>.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">"selectionchange"</span>, <span class="hljs-function"><span class="hljs-params">event</span> =></span> {
	<span class="hljs-keyword">const</span> selection = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getSelection</span>();
	<span class="hljs-keyword">const</span> foundElement = selection.<span class="hljs-property">anchorNode</span>?.<span class="hljs-property">parentElement</span>.<span class="hljs-title function_">closest</span>(<span class="hljs-string">"[hidden=until-found]"</span>);

	<span class="hljs-keyword">if</span> (foundElement) {
		<span class="hljs-keyword">const</span> cancelled = !foundElement.<span class="hljs-title function_">dispatchEvent</span>(
			<span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<span class="hljs-string">"beforematch"</span>, { <span class="hljs-attr">bubbles</span>: <span class="hljs-literal">true</span> });
		);
		<span class="hljs-keyword">if</span> (cancelled) <span class="hljs-keyword">return</span>;

		foundElement.<span class="hljs-title function_">removeAttribute</span>(<span class="hljs-string">"hidden"</span>);
		<span class="hljs-keyword">const</span> slot = foundElement.<span class="hljs-property">shadowRoot</span>.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">"slot"</span>);
		slot.<span class="hljs-title function_">removeAttribute</span>(<span class="hljs-string">"tabindex"</span>);
		slot.<span class="hljs-title function_">removeAttribute</span>(<span class="hljs-string">"aria-hidden"</span>);
	}
});
</code></pre>
<p>When the selection changes, we check if the selected content is within an element with <code>hidden=until-found</code>, the <code>beforematch</code> event isn’t cancelled, and if we’re clear, then we start its teardown:</p>
<ul>
<li>Removing the <code>hidden</code> attribute (like the actual <code>hidden=until-found</code> feature works), removes all of our styles.</li>
<li>Then we need to tear down the <code>tabindex</code> and <code>aria-hidden</code> attributes.</li>
</ul>
<p>Afterward there is the shadow root leftover and while we can remove that if we replace the host with a shadow-less clone, we’ll lose the selection, which isn’t terribly hard to resolve, but it causes issues with the “find-in-page” feature which put me off from that approach.</p>
<h2 id="applying-the-polyfill">Applying the polyfill</h2>
<p>If we start a mutation observer early enough, we can both catch elements as they initially are parsed and later if there are any programmatic changes to the document. We can use this to polyfill the value for the global <code>hidden</code> attribute.</p>
<p>Here’s the configuration of our observer:</p>
<pre><code class="hljs language-javascript"><span class="hljs-keyword">const</span> hiddenUntilFoundObserver = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MutationObserver</span>(callback);

hiddenUntilFoundObserver.<span class="hljs-title function_">observe</span>(<span class="hljs-variable language_">document</span>, {
	<span class="hljs-attr">subtree</span>: tree,
	<span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span>,
	<span class="hljs-attr">attributeFilter</span>: [<span class="hljs-string">"hidden"</span>],
});
</code></pre>
<p>Since we’re observing prior to the <code>&#x3C;body></code> element being parsed or constructed, we’ll just observe the <code>document</code> itself. We want to both observe every element mutation and <code>hidden</code> attribute mutations.</p>
<aside role="note" aria-label="Performance"><p>For the purposes of this post, I’m not interested optimizing the performance of this polyfill. If you have any ideas, I’d be happy to hear them though.</p></aside>
<p>Before we get into the the observer callback, since we’re covering two types of mutations, we’ll need to abstract our shadow root setup code:</p>
<pre><code class="hljs language-javascript"><span class="hljs-keyword">const</span> sheet = <span class="hljs-keyword">new</span> <span class="hljs-title class_">CSSStyleSheet</span>();
sheet.<span class="hljs-title function_">replaceSync</span>(<span class="hljs-string">`
	:host([hidden="until-found"]) {
		display: revert;
		content-visibility: auto;
		contain: size;
		user-select: none;
		background-clip: initial !important;
	
		slot {
			color: transparent;
			pointer-events: none;
		}
	
		::slotted(*) {
			opacity: 0 !important;
		}
	}
`</span>);

<span class="hljs-keyword">function</span> <span class="hljs-title function_">addShadow</span>(<span class="hljs-params">element</span>) {
	<span class="hljs-keyword">if</span> (element.<span class="hljs-property">shadowRoot</span>) <span class="hljs-keyword">return</span>;
	element.<span class="hljs-title function_">attachShadow</span>({ <span class="hljs-attr">mode</span>: <span class="hljs-string">"open"</span> });
	element.<span class="hljs-property">shadowRoot</span>.<span class="hljs-property">innerHTML</span> = <span class="hljs-string">"&#x3C;slot aria-hidden=true tabindex=-1>&#x3C;/slot>"</span>;
	element.<span class="hljs-property">shadowRoot</span>.<span class="hljs-property">adoptedStyleSheets</span> = [sheet];
}
</code></pre>
<p>If the element already has a shadow root attached this bail early. This is a limitation of making this a generalized approach. It’s not an unsurmountable problem.</p>
<p>We’re also using the <em>same</em> constructed stylesheet between elements. This is fine since we’re not modifying the stylesheet.</p>
<p>Now for our callback…</p>
<pre><code class="hljs language-javascript"><span class="hljs-keyword">const</span> hiddenUntilFoundObserver = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MutationObserver</span>(<span class="hljs-function"><span class="hljs-params">entries</span> =></span> {
	<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> entries) {
		<span class="hljs-keyword">if</span> (entry.<span class="hljs-property">type</span> === <span class="hljs-string">"attributes"</span> &#x26;&#x26; entry.<span class="hljs-property">target</span>.<span class="hljs-title function_">getAttribute</span>(<span class="hljs-string">"hidden"</span>) === <span class="hljs-string">"until-found"</span>) <span class="hljs-title function_">addShadow</span>(entry.<span class="hljs-property">target</span>);
		<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (entry.<span class="hljs-property">type</span> === <span class="hljs-string">"childList"</span>)
			<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> node <span class="hljs-keyword">of</span> entry.<span class="hljs-property">addedNodes</span>)
				<span class="hljs-keyword">if</span> (node.<span class="hljs-property">getAttribute</span>?.(<span class="hljs-string">"hidden"</span>) === <span class="hljs-string">"until-found"</span>) <span class="hljs-title function_">addShadow</span>(node);
	}
});
</code></pre>
<p>For <code>attributes</code> mutation records, we only need to worry about one element which makes it straightforward to check the value of the <code>hidden</code> attribute. For <code>childList</code> mutation records, the <code>addedNodes</code> could be text nodes, so we need to optionally call the <code>getAttribute()</code> in our check.</p>
<p>Now, all of the polyfill’s code is written and we just need to feature check if we need the polyfill. In browsers that support the feature we can check if <code>onbeforematch</code> is in the <code>document</code>. We’ll also want to filter out Safari which we’ll just have to do a bit inelgantly just using a supports-check for <code>user-select: none</code> since there’s no way to feature-check “find-in-page.”</p>
<pre><code class="hljs language-javascript"><span class="hljs-keyword">if</span> (<span class="hljs-string">"onbeforematch"</span> <span class="hljs-keyword">in</span> <span class="hljs-variable language_">document</span> || !<span class="hljs-variable constant_">CSS</span>.<span class="hljs-title function_">supports</span>(<span class="hljs-string">"(user-select: none)"</span>)) <span class="hljs-keyword">return</span>;
</code></pre>
<p>Once that’s in place we’ve got our entire polyfill:</p>
<pre><code class="hljs language-javascript">(<span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) {
	<span class="hljs-keyword">if</span> (<span class="hljs-string">"onbeforematch"</span> <span class="hljs-keyword">in</span> <span class="hljs-variable language_">document</span> || !<span class="hljs-variable constant_">CSS</span>.<span class="hljs-title function_">supports</span>(<span class="hljs-string">"(user-select: none)"</span>)) <span class="hljs-keyword">return</span>;

	<span class="hljs-keyword">const</span> sheet = <span class="hljs-keyword">new</span> <span class="hljs-title class_">CSSStyleSheet</span>();
	sheet.<span class="hljs-title function_">replaceSync</span>(<span class="hljs-string">`
		:host([hidden="until-found"]) {
			display: revert;
			content-visibility: auto;
			contain: size;
			user-select: none;
			background-clip: initial !important;
		
			slot {
				color: transparent;
				pointer-events: none;
			}
		
			::slotted(*) {
				opacity: 0 !important;
			}
		}
	`</span>);

	<span class="hljs-keyword">const</span> hiddenUntilFoundObserver = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MutationObserver</span>(<span class="hljs-function"><span class="hljs-params">entries</span> =></span> {
		<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entry <span class="hljs-keyword">of</span> entries) {
			<span class="hljs-keyword">if</span> (entry.<span class="hljs-property">type</span> === <span class="hljs-string">"attributes"</span> &#x26;&#x26; entry.<span class="hljs-property">target</span>.<span class="hljs-title function_">getAttribute</span>(<span class="hljs-string">"hidden"</span>) === <span class="hljs-string">"until-found"</span>) <span class="hljs-title function_">addShadow</span>(entry.<span class="hljs-property">target</span>);
			<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (entry.<span class="hljs-property">type</span> === <span class="hljs-string">"childList"</span>)
				<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> node <span class="hljs-keyword">of</span> entry.<span class="hljs-property">addedNodes</span>)
					<span class="hljs-keyword">if</span> (node.<span class="hljs-property">getAttribute</span>?.(<span class="hljs-string">"hidden"</span>) === <span class="hljs-string">"until-found"</span>) <span class="hljs-title function_">addShadow</span>(node);
		}
	});
	
	hiddenUntilFoundObserver.<span class="hljs-title function_">observe</span>(<span class="hljs-variable language_">document</span>, {
		<span class="hljs-attr">subtree</span>: tree,
		<span class="hljs-attr">childList</span>: <span class="hljs-literal">true</span>,
		<span class="hljs-attr">attributeFilter</span>: [<span class="hljs-string">"hidden"</span>],
	});

	<span class="hljs-variable language_">document</span>.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">"selectionchange"</span>, <span class="hljs-function"><span class="hljs-params">event</span> =></span> {
		<span class="hljs-keyword">const</span> selection = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getSelection</span>();
		<span class="hljs-keyword">const</span> foundElement = selection.<span class="hljs-property">anchorNode</span>?.<span class="hljs-property">parentElement</span>.<span class="hljs-title function_">closest</span>(<span class="hljs-string">"[hidden=until-found]"</span>);
	
		<span class="hljs-keyword">if</span> (foundElement) {
			<span class="hljs-keyword">const</span> cancelled = !foundElement.<span class="hljs-title function_">dispatchEvent</span>(
				<span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<span class="hljs-string">"beforematch"</span>, { <span class="hljs-attr">bubbles</span>: <span class="hljs-literal">true</span> });
			);
			<span class="hljs-keyword">if</span> (cancelled) <span class="hljs-keyword">return</span>;
	
			foundElement.<span class="hljs-title function_">removeAttribute</span>(<span class="hljs-string">"hidden"</span>);
			<span class="hljs-keyword">const</span> slot = foundElement.<span class="hljs-property">shadowRoot</span>.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">"slot"</span>);
			slot.<span class="hljs-title function_">removeAttribute</span>(<span class="hljs-string">"tabindex"</span>);
			slot.<span class="hljs-title function_">removeAttribute</span>(<span class="hljs-string">"aria-hidden"</span>);
		}
	});

	<span class="hljs-keyword">function</span> <span class="hljs-title function_">addShadow</span>(<span class="hljs-params">element</span>) {
		<span class="hljs-keyword">if</span> (element.<span class="hljs-property">shadowRoot</span>) <span class="hljs-keyword">return</span>;
		element.<span class="hljs-title function_">attachShadow</span>({ <span class="hljs-attr">mode</span>: <span class="hljs-string">"open"</span> });
		element.<span class="hljs-property">shadowRoot</span>.<span class="hljs-property">innerHTML</span> = <span class="hljs-string">"&#x3C;slot aria-hidden=true tabindex=-1>&#x3C;/slot>"</span>;
		element.<span class="hljs-property">shadowRoot</span>.<span class="hljs-property">adoptedStyleSheets</span> = [sheet];
	}
})();
</code></pre>
<p>And here it is in action:</p>
<iframe src="https://knowler.dev/demos/5gLfaym?codepen" width="100%" height="300"></iframe>
<h2 id="summary-of-limitations">Summary of limitations</h2>
<p>As mentioned, this does not work in Safari and you don’t need it in Chrome.</p>
<p>Due to issues with observing selection within shadow roots in Firefox, there’s no way we can make the polyfill work inside shadow roots.</p>
<p>And again, this also doesn’t work for elements that already have a shadow root.</p>
<h2 id="why-even">Why even?</h2>
<p>Given the limitations and especially the fact that this polyfill only applies to one browser, why bother polyfilling this? Well, to be completely honest I don’t find it worth polyfilling — especially since it would be quite easy to progressively enhance <code>hidden=until-found</code> usage. This was an interesting exercise in exploring <em>findability</em> in the browser. It’s something that authors don’t have <em>direct</em> control over. It takes a very thought out concoction of CSS and shadow DOM to even mimic something that is based on it. Perhaps this could be something that authors had greater control over in the future. Another part of browsers to unearth in making the platform extensible?</p>
<h2 id="contribute">Contribute</h2>
<p>If you want to use the final polyfill or contribute, you can do so on GitHub at <a href="https://github.com/knowler/hidden-until-found-polyfill">knowler/hidden-until-found-polyfill</a>.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I’m switching back to Arc Browser (for now)]]></title>
            <link>https://knowler.dev/blog/im-switching-back-to-arc-browser-for-now</link>
            <guid isPermaLink="false">im-switching-back-to-arc-browser-for-now</guid>
            <pubDate>Fri, 22 Nov 2024 06:00:00 GMT</pubDate>
            <description><![CDATA[There and back again. Are forever browsers a thing?]]></description>
            <content:encoded><![CDATA[<p>I adopted Arc Browser as soon as I could get my hands on an invite. It was definitely weird at first, but it had what I considered the holy grail of features in a web browser: separate profiles with their own spaces all in the same window. I cannot stress how helpful that has been for my workflow and it’s something I was searching for long before I started using Arc.</p>
<h2 id="why-i-need-a-non-traditional-browser">Why I need a non-traditional browser</h2>
<p>I imagine that I have some kind of neurodivergence. I haven’t formally explored it and I don’t really know enough to put a label on it. Context switching is <em>very</em> difficult for me and I get distracted so easily, which isn’t helpful when my mind is constantly drawing connections between seemingly disparate things. I can become very obsessive on topics and often lose track of time when I get lost diving into details and making a terrible mess of my workspace making it unapproachable for myself if I return to it.</p>
<p>Anyway, I should probably talk to a mental health specialist about all of that, the point I’m trying to make is that Arc is shaped in a way that has been tremendously helpful for my ability to work and focus. Being able to eliminate distractions, separate extremely focused contexts, switch between contexts in a non-overwhelming way, tame my tab hoarding tendencies or at least create meaningful structures for those tabs, all have been incredible for me.</p>
<p>Before Arc, I was jumping between browsers because I couldn’t use Firefox, my previous daily driver, for work since the proprietary SSO extension required for that job didn’t support it — <em>IYKYK</em>. What I loved about Firefox was its container tabs. They allowed me to have separate isolated sessions in the same window. They weren’t perfect as all of those container tabs were still mixed together in the tab list, so I had joked that I wanted Firefox’s container tabs and Chrome’s tab groups to have a baby.</p>
<p>Arc was even better than that.</p>
<h2 id="my-forever-browser">My forever browser?</h2>
<p>In Arc, there are profiles which is very similar to profiles in other Chromium browsers: they have their own session and extensions. A major difference is that in Arc profiles all live in the same window.</p>
<p>A core feature of Arc is <em>spaces</em> which are effectively separate tab lists (almost like tab groups) which are visually isolated from each other. Multiple spaces can use the same profile and when they do, they can share some favourites which are like a super pinned tab. Within spaces, regular pinned tabs can be organized and renamed like bookmarks inside folders. Using the same hotkey to close a tab on a pinned tab will reset it to its initial URL which is helpful when I make a mess of my pins.</p>
<p>A very important feature for a browser like this is being able to route incoming URLs into the correct space. For this, Arc has a feature called “Air Traffic Control” which allows you to set up those routing rules. I found it works exceptionally well.</p>
<p>A more controversial feature that I’ve come to really love is the default to clear tabs after a certain period of time. The default of 24 hours might be too short for some, but that’s why it’s configurable. Left unchecked, I am a major tab hoarder. Sometimes I’d have hundreds of tabs hoarded on my phone, but I’ve since taken advantage of a feature like this in Safari. Since Arc makes it pretty easy to meaningfully organize tabs, I’d treat the regular tabs like an inbox and the pinned tabs would become longer-term storage.</p>
<p>Now, don’t get me wrong, there are features of Arc that I really do not like — their quote feature which I think is a recipe for widespread link rot and the Arc Max AI bullshit — but Arc worked overwhelming well for me and I used it almost exclusively for two years.</p>
<h2 id="getting-of-the-arc">Getting off the Arc</h2>
<p>I made plans to stop using Arc, when I realized the team was more interested in AI and “disrupting” traditional web search than iterating on the user experience of the browser. Sadly, that probably hasn’t changed and I’ll address that later.</p>
<p>But it took months to stop using Arc because I had really leaned into many features like folders and renaming tabs. Whenever I started a new project at work, I’d scaffold out a new folder with renamed tabs for each environment of the site I was working on, sometimes even tabs for my own PRs or the ones I was reviewing. I’d collect important links which I’d give more relevant names.</p>
<h2 id="first-stop-on-the-safari">First stop on the <em>Safari</em></h2>
<p>Initially, I jumped to Safari and made use of its profile feature and tab groups. Tab groups were the closest thing I could get to renaming tabs, so sometimes I’d just have a single tab in a group or I’d get creative with how I organized groups. Safari’s Reading List feature replaced how I sometimes used pinned tabs in Arc — almost in a better way, since I use the Reading List on my iPhone a fair bit either to queue up articles to listen to while I wash dishes or to read if I’m ever offline. Ultimately, I still struggled with funnelling tabs into the correct contexts, keeping those contexts focused and in tact, and dealing with some frustrating bugs in Web Inspector.</p>
<p>I evaluated a number of other browsers, like Vivaldi and Orion, and though they had many features that I liked, I just couldn’t find one that worked with how I now worked.</p>
<p>Once my previous job ended, I sought to make the return to Firefox or a Firefox-based browser. I tried a few, even Firefox Nightly which has an experimental vertical tabs features, but I ultimately landed on Zen browser for a few months.</p>
<h2 id="zen-the-closest-browser-to-arc">Zen: the closest browser to Arc</h2>
<p>Zen was the closest to Arc that I’ve found. Zen not only has container tabs since it’s Firefox based, it has its own Space-like feature called Workspaces which effectively works the same. You can even set a default container tab to use for a space. Container tabs already do have a routing feature to open matching URLs in the correct container, so it was very close to Arc’s “Air Traffic Control.”</p>
<p>Now while Zen is really shaping up to be effectively an Arc-clone in Firefox, I found it to be too unstable and slow for my day-to-day work. Sometimes an update would completely break my workspaces. There were times when I’d need to research a topic and I just knew that I could do it more effectively in Arc itself.</p>
<p>Firefox’s Web Developer Tools also has a terrible bug where constructed stylesheets sometimes won’t display at all in the style panel which would require a full reload and re-opening of the dev tools panel. It was oddly reminiscent of the bug I was dealing with in Safari’s Web Inspector which required the same solution.</p>
<p>Despite this, I am optimistic for Zen browser. I truly loved being back in a Firefox-based browser. I also loved having my vertical tabs on the right side instead of the left — am I the only person in the world that’s bothered when you toggle a left sidebar and my left-to-right content shifts substantially? I digress.</p>
<p>Once I realized that Zen could not sustain my day to day work, I knew I had to go back to what I knew worked: Arc.</p>
<h2 id="there-and-back-again">There and back again</h2>
<p>So, I am somewhat reluctantly back using Arc and it’s made a noticeable difference already. I’m disheartened that the company is still obsessed with AI. I have mixed feelings about them deciding to not work on another major version of Arc: they are leaving Arc alone to channel that AI-fixation elsewhere, but at the same time, despite what they said, I hope this isn’t Arc’s death-knell.</p>
<p>I thought Arc could be my <em>forever browser</em>, but I’m really not sure that’s a thing. I expect that I’ll adventure elsewhere at some point. If I could steal an ounce of the Browser Company team’s AI-consumed attention for Arc to ask one selfish thing:</p>
<p>Can I please but my sidebar on the right-hand side? That would truly make Arc complete to me.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[To define custom elements or not when distributing them]]></title>
            <link>https://knowler.dev/blog/to-define-custom-elements-or-not-when-distributing-them</link>
            <guid isPermaLink="false">to-define-custom-elements-or-not-when-distributing-them</guid>
            <pubDate>Tue, 08 Oct 2024 04:12:54 GMT</pubDate>
            <description><![CDATA[Why not both?]]></description>
            <content:encoded><![CDATA[<p>A few days ago, <a href="https://front-end.social/@rem/113248748478433268">Remy Sharp was wondering about name collisions for custom elements</a> and <a href="https://sunny.garden/@knowler/113249247334407076">I replied</a> with my opinion that custom element authors should allow their consumers to register the custom elements themselves and just export the class. This morning both <a href="https://gomakethings.com/user-defined-web-components/">Chris Ferdinandi</a> and <a href="https://mastodon.gal/@misteroom/113266920063195955">Óscar Otero</a> made posts about the convenience of a single script which had me rethinking my earlier suggestion.</p>
<p>Personally, I still think that as a custom element author, I want to give my consumers the most flexibility for how to use a custom element I’ve created:</p>
<ul>
<li>I want to do what I can to help them avoid name collisions.</li>
<li>I don’t want to assume their conditions for defining a custom element.</li>
<li>I want to support consumers who want to use <a href="https://wicg.github.io/webcomponents/proposals/Scoped-Custom-Element-Registries.html">scoped custom element registeries</a> in the future.</li>
</ul>
<p>However, I began to wonder how I could add the convenience offered by a single script solution.</p>
<p>I could just include an additional file that handles the definition:</p>
<pre><code class="hljs language-javascript"><span class="hljs-keyword">import</span> { <span class="hljs-title class_">BlehBlehBlehElement</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"./bleh-bleh-bleh-element.js"</span>;

customElements.<span class="hljs-title function_">define</span>(<span class="hljs-string">"bleh-bleh-bleh"</span>, <span class="hljs-title class_">BlehBlehBlehElement</span>);
</code></pre>
<p>But this really is such an insignificant amount of code <em>and</em> it introduces nested imports. The latter is resolvable with a <code>preload</code>, but it still creates complexity and a potential footgun for consumers.</p>
<p>I also could create a bundle for that file… but I like to not use build tools whenever I can since I don’t want the complexity myself.</p>
<p>This got me thinking about <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta"><code>import.meta</code></a>. For modules, <code>import.meta</code> includes the URL used to load the module. This means we can create an additional API for the module using its URL’s search params.</p>
<p>So in our case, we can just conditionally define the element when the <code>?define</code> search param is set:</p>
<pre><code class="hljs language-javascript"><span class="hljs-comment">// Export the class so the consumer can register the element with greater flexibility</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">BlehBlehBlehElement</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">HTMLElement</span> {
	<span class="hljs-title function_">constructor</span>(<span class="hljs-params"></span>) {
		<span class="hljs-variable language_">super</span>();
		<span class="hljs-keyword">if</span> (!<span class="hljs-variable language_">this</span>.<span class="hljs-property">shadowRoot</span>) <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">attachShadow</span>({ <span class="hljs-attr">mode</span>: <span class="hljs-string">"open"</span> });
		<span class="hljs-variable language_">this</span>.<span class="hljs-property">shadowRoot</span>.<span class="hljs-property">innerHTML</span> = <span class="hljs-string">"🧛"</span>;
	}
}

<span class="hljs-comment">// Check if `?define` is set and if so, define it in the default registry</span>
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(<span class="hljs-keyword">import</span>.<span class="hljs-property">meta</span>.<span class="hljs-property">url</span>).<span class="hljs-property">searchParams</span>.<span class="hljs-title function_">has</span>(<span class="hljs-string">"define"</span>)) {
	customElements.<span class="hljs-title function_">define</span>(<span class="hljs-string">"bleh-bleh-bleh"</span>, <span class="hljs-title class_">BlehBlehBlehElement</span>);
}
</code></pre>
<p>Here’s how a consumer of the custom element would use it:</p>
<pre><code class="hljs language-html"><span class="hljs-meta">&#x3C;!doctype <span class="hljs-keyword">html</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">title</span>></span>Bleh bleh bleh – Drac<span class="hljs-tag">&#x3C;/<span class="hljs-name">title</span>></span>
<span class="hljs-tag">&#x3C;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">module</span> <span class="hljs-attr">src</span>=<span class="hljs-string">./bleh-bleh-bleh-element.js?define</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">script</span>></span>

<span class="hljs-tag">&#x3C;<span class="hljs-name">bleh-bleh-bleh</span>></span><span class="hljs-tag">&#x3C;/<span class="hljs-name">bleh-bleh-bleh</span>></span>
</code></pre>
<p>Is this a bad idea? Sure, the script must be a module now (i.e. you must set <code>type=module</code> on its <code>&#x3C;script></code> element), but that’s likely a good thing.</p>
<p>Anyway, I thought this was a pretty neat way to keep things simple for both a custom element author and consumer. Let me know what you think.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A custom element smoke test]]></title>
            <link>https://knowler.dev/blog/a-custom-element-smoke-test</link>
            <guid isPermaLink="false">a-custom-element-smoke-test</guid>
            <pubDate>Thu, 11 Jul 2024 19:31:25 GMT</pubDate>
            <description><![CDATA[A step many custom elements miss before shipping.]]></description>
            <content:encoded><![CDATA[<p>It’s easy to create a custom element and forget that HTML elements can
be created programmatically with the DOM’s <code>createElement()</code> method.
Consumers of your custom elements might do this or they might use a
framework that will.</p>
<p>So, a great <a href="https://en.wikipedia.org/wiki/Smoke_testing_(software)"><i>smoke test</i></a> for custom elements is to try to
create it programmatically:</p>
<pre><code class="hljs language-js"><span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">"my-element"</span>);
</code></pre>
<p>There’s a couple of exceptions this might throw:</p>
<pre><code>DOMException: Failed to construct 'CustomElement': The result must not have children
</code></pre>
<p>In this case, you’re adding children to your custom element inside its
constructor.</p>
<pre><code>DOMException: Failed to construct 'CustomElement': The result must not have attributes
</code></pre>
<p>In this case, you’re setting attributes on your custom element inside
its constructor.</p>
<p>The HTML parser isn’t going to surface these issues for you and so if
that’s all your testing with (i.e. you’re just writing plain text HTML),
then you could end up shipping something that’s broken.</p>
<p>An important principle emerges from these cases: <strong>don’t do light DOM
operations inside of the constructor.</strong></p>
<h2 id="a-digression-on-sciopobia">A digression on sciopobia</h2>
<p>If you write “HTML Web Components” — <em>sciophobic</em> custom elements that
avoid using the Shadow DOM — you are likely going to be more prone to
this sort of issue as <a href="https://knowler.dev/blog/html-web-components-are-an-antipattern">you’re creating elements against the grain of
HTML</a>. It might seem easy enough to just not do stuff in the
constructor, however, using the light DOM to express the make up of a
custom element is naturally going to be more prone to these issues in
general which is why <a href="https://knowler.dev/blog/please-keep-your-hands-arms-and-legs-inside-the-custom-element">I avoid it if I can</a>. When you ditch
encapsulation this is what you get. The promise of “framework agnostic”
components does not look very promising with such an approach, since
frameworks expect custom elements to behave as HTML elements do.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Read the Platform]]></title>
            <link>https://knowler.dev/blog/read-the-platform</link>
            <guid isPermaLink="false">read-the-platform</guid>
            <pubDate>Sat, 29 Jun 2024 00:55:18 GMT</pubDate>
            <description><![CDATA[A Web Platform reading club.]]></description>
            <content:encoded><![CDATA[<p>This week <a href="https://sunny.garden/@knowler/112684253934419619">I suggested the idea of a “Web Standards Book Club” on
Mastodon</a> and there was a bit of interest. After thinking about how
such a club would work, I’ve decided that since I suggested the idea of
the club to the fediverse, the club will take place in the fediverse.</p>
<h2 id="how-to-participate">How to participate</h2>
<ol>
<li>Follow the hashtag <code>#ReadThePlatform</code> in the fediverse. Feel free to
lurk or to secondhand read.</li>
<li>Read books or content about the Web Platform at your own pace. Sync
up with others if you’d like.</li>
<li>Share about that content using the hashtag. Feel free to share
anything you write about it on your website too.</li>
</ol>
<p>There’s no formal meet up times, but don’t let that stop you from
organizing meetups to discuss with others.</p>
<h2 id="my-own-reading-list">My own reading list</h2>
<p>I’m no authority here — you can read whatever you’d like, but here is my
own initial, tentative reading plan if you’d like to follow along with
me:</p>
<ol>
<li><a href="https://htmlparser.info"><em>Idiosyncrasies of the HTML parser</em></a> by <a href="https://mastodon.social/@zcorpan">Simon Pieters</a></li>
<li><a href="https://browser.engineering"><em>Web Browser Engineering</em></a> by <a href="https://pavpanchekha.com">Pavel Panchekha</a> &#x26; <a href="https://mastodon.social/@chrishtr">Chris Harrelson</a></li>
<li><a href="https://wpc.guide"><em>Web Platform Contribution Guide</em></a> by various authors</li>
</ol>
<p>All of these books are free to read online, open source, and in the case
of the first two, you can support the writers as they continue to write
their respective books. Those details can be found in the links above.</p>
<h2 id="why-im-doing-this">Why I’m doing this?</h2>
<p>My own personal motivation for reading books and other content about the
Web Platform is not only to deepen my understanding of the medium as it
affects my work, but so that I can contribute in more ways. I’ve been
inspired by individuals involved in moving the Web forward and I believe
that the Web is ours and not just the product of large companies. I
believe in the Web and so I am reading to ensure the Web continues to
have a future, because <a href="https://knowler.dev/blog/the-web-is-not-inevitable">the Web is not inevitable</a>.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Maintaining dotfiles]]></title>
            <link>https://knowler.dev/blog/maintaining-dotfiles</link>
            <guid isPermaLink="false">maintaining-dotfiles</guid>
            <pubDate>Sat, 15 Jun 2024 22:39:22 GMT</pubDate>
            <description><![CDATA[A dotfiles maintenance technique that has worked for me.]]></description>
            <content:encoded><![CDATA[<aside role="note" aria-label="Republishing"><p><strong>Note</strong>: I wrote this a number of years ago and had it on my site at a different path. I noticed it was linked from another one of my pages, so I figured I’d re-publish it here on my blog. Ironically, I haven’t touched my dotfiles for a few years, but that’s because I had to fork them for work to store them privately and that’s what I now maintain.</p></aside>
<p>When it comes to maintaining anything that serves a sort of background function for more important things, it is vital to ensure that the maintainence process is simple, otherwise, there will be stagnation and that thing won’t be serving its purpose. When it comes to dotfiles, I don’t believe there is necessarily a best way to maintain them, however, your maintainence methods should suit your strengths.</p>
<p>Mine is <strong>Git</strong>.</p>
<p>I’ve used Git for a number of years and it is a tool that I use every day. For many developers, I am sure this is also the case, though I expect that many might not feel adequately strong at using Git. I still think that a maintainence workflow that favours Git is the best route for most of these developers, because the main alternative is a workflow that, while it does use Git, primarily depends on a shell’s linking capabilities (i.e. <code>ln</code>), and from my observation and experience, most developers are much less confident here.</p>
<p>That was the case with myself. I found myself spending a long time writing shell scripts for linking files or using scripts that required a tendious degree of maintainence in order to add new files or simply to move files, along with a lot of time spent on trial and error — which is scary to do when you just need your system to work for more important tasks. This usually resulted a stale dotfiles repository and a lot less care for my local dotfiles or even fear towards modifying (or fixing) different configurations.</p>
<p>In 2020, after a number of issues with my computer hardware which required me to setup at least 3 different fresh OS installs for my work, I decided that I needed to find a solution for my dotfiles so that I could reduce how much time it was taking me to get to work.</p>
<h2 id="the-bare-repository-method">The Bare Repository Method</h2>
<p>I stumbled upon an article by Atlassian called <a href="https://www.atlassian.com/git/tutorials/dotfiles">“The best way to store your dotfiles: A bare Git repository.”</a> I had never heard of a bare Git respository and since I had been digging deep into Git, I was curious. After reading the article, I gave the setup a try in a new respository on my GitHub and very quickly realized that this was a much easier way to manage dotfiles, and I converted my main repo over.</p>
<p>A bare repository is essentially just the <code>.git</code> directory without a checked out worktree. A setup looks like cloning your dotfiles as a bare repo. Usually it is advisable to use something other than <code>.git</code>, I use <code>.dotfiles</code> in my home directory. Then you create checkout a worktree in your home directory. Now, you might wonder, <em>wouldn’t that display everything as potential files to track when I run <code>git status</code>?</em> Yes, it does, however, this is easily solvable, by turning off untracked files in your status config for the repository.</p>
<h3 id="clone-the-repo">Clone the repo</h3>
<pre><code class="hljs language-shell">git clone &#x3C;repo> &#x3C;git-dir-path> --bare
</code></pre>
<h3 id="do-not-show-untracked-files-in-git-status-for-the-repo">Do not show untracked files in git status for the repo</h3>
<pre><code class="hljs language-shell">git --git-dir=&#x3C;git-dir-path> config status.showUntrackedFiles no
</code></pre>
<h3 id="checkout-the-worktree-in-your-home">Checkout the worktree in your <code>$HOME</code></h3>
<pre><code class="hljs language-shell">git --git-dir=&#x3C;git-dir-path> --work-tree=$HOME checkout
</code></pre>
<h3 id="manage-dotfiles-using-git">Manage dotfiles using Git</h3>
<p>You will need to prefix your Git commands with the path to the Git directory and the worktree path:</p>
<pre><code class="hljs language-shell">git --git-dir=&#x3C;git-dir-path> --work-tree=$HOME &#x3C;command>
</code></pre>
<p>It is best to create an alias for this.</p>
<pre><code class="hljs language-shell">alias dots="git --git-dir=&#x3C;git-dir-path> --work-tree=$HOME"
</code></pre>
<p>Now, you can use this alias however you would use Git.</p>
<pre><code class="hljs language-shell">dots add ~/.config/nvim/init.vim

dots commit -m "Add Neovim config"

dots push
</code></pre>]]></content:encoded>
        </item>
    </channel>
</rss>