<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/rss/style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>
    TIL | Stefan Judis Web Development
  </title>
  <subtitle>
    Today I learned posts
  </subtitle>
  <link href="https://www.stefanjudis.com/feeds/til.xml" rel="self"/>
  <link href="https://www.stefanjudis.com/"/>
  <updated>2026-04-05T22:00:00+00:00</updated>
  <id>
    https://www.stefanjudis.com/
  </id>
  <author>
    <name>
      Stefan Judis
    </name>
    <email>
      stefanjudis@gmail.com
    </email>
  </author>
    
    <entry>
      <title>
        Intl can localize units, too!
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/intl-can-localize-units-too/"/>
      <published>2026-04-05T22:00:00+00:00</published>
      <updated>2026-04-05T22:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/intl-can-localize-units-too/
      </id>
      <category term="tilPost"></category>
        <category term="JavaScript"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>Today I learned that <code>Intl</code> allows you to format numbers with currencies or units!</p>
<pre class="language-javascript"><code class="language-javascript">console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
  <span class="token keyword">new</span> <span class="token class-name">Intl<span class="token punctuation">.</span>NumberFormat</span><span class="token punctuation">(</span><span class="token string">"de-DE"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> 
    <span class="token literal-property property">style</span><span class="token operator">:</span> <span class="token string">"currency"</span><span class="token punctuation">,</span> 
    <span class="token literal-property property">currency</span><span class="token operator">:</span> <span class="token string">"EUR"</span> 
  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span><span class="token number">123456789</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// "123.456.789,00 €"</span>

console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
  <span class="token keyword">new</span> <span class="token class-name">Intl<span class="token punctuation">.</span>NumberFormat</span><span class="token punctuation">(</span><span class="token string">"ja-JP"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> 
    <span class="token literal-property property">style</span><span class="token operator">:</span> <span class="token string">"currency"</span><span class="token punctuation">,</span> 
    <span class="token literal-property property">currency</span><span class="token operator">:</span> <span class="token string">"JPY"</span> 
  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span><span class="token number">123456789</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// "￥123,456,789"</span>

console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
  <span class="token keyword">new</span> <span class="token class-name">Intl<span class="token punctuation">.</span>NumberFormat</span><span class="token punctuation">(</span><span class="token string">"pt-PT"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
    <span class="token literal-property property">style</span><span class="token operator">:</span> <span class="token string">"unit"</span><span class="token punctuation">,</span>
    <span class="token literal-property property">unit</span><span class="token operator">:</span> <span class="token string">"kilometer-per-hour"</span><span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span><span class="token number">50</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// "50 km/h"</span>

console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
  <span class="token keyword">new</span> <span class="token class-name">Intl<span class="token punctuation">.</span>NumberFormat</span><span class="token punctuation">(</span><span class="token string">'fr-FR'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
    <span class="token literal-property property">style</span><span class="token operator">:</span> <span class="token string">'unit'</span><span class="token punctuation">,</span>
    <span class="token literal-property property">unit</span><span class="token operator">:</span> <span class="token string">'kilobyte'</span><span class="token punctuation">,</span>
    <span class="token literal-property property">unitDisplay</span><span class="token operator">:</span> <span class="token string">'long'</span><span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span><span class="token number">123456</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// "123 456 kilooctets"</span>
</code></pre>
<p>Did you notice how the Japanese Yen symbol (￥) moved in front of the numbers? Or that, apparently, French folks translate the unit kilobyte? Fun!</p>
<p>Whenever you put currency/unit logic into userland JavaScript, don't. It's all baked into the language these days.</p>
<p><em>Tip: here's <a href="https://tc39.es/ecma402/#sec-issanctionedsimpleunitidentifier" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Ftc39.es%2Fecma402%2F%23sec-issanctionedsimpleunitidentifier')">the list of supported units</a>.</em></p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20Intl can localize units, too!">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        The scope of type guards and assertion functions
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/the-scope-of-type-guards-and-assertion-functions/"/>
      <published>2026-04-03T22:00:00+00:00</published>
      <updated>2026-04-03T22:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/the-scope-of-type-guards-and-assertion-functions/
      </id>
      <category term="tilPost"></category>
        <category term="TypeScript"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>I've just read <a href="https://www.solberg.is/unknown-to-typed" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.solberg.is%2Funknown-to-typed')">Absorbing unknown Into the Type Realm</a>. It's a good read, check it out.</p>
<p>However I also learned an important difference between TypeScript type guards and assertion functions. And to be fair, I didn't even know assertion functions were a thing in TypeScript.</p>
<p>Let's look at a standard type guard.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #569CD6">interface</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='interface User' >User</data-lsp></span><span style="color: #D4D4D4"> {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE"><data-lsp lsp='(property) User.name: string' >name</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0">string</span><span style="color: #D4D4D4">;</span></div><div class='line'><span style="color: #D4D4D4">}</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// standard type guard</span></div><div class='line'><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA"><data-lsp lsp='function isUser(value: unknown): value is User' >isUser</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: unknown' >value</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0">unknown</span><span style="color: #D4D4D4">): </span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: unknown' >value</data-lsp></span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">is</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='interface User' >User</data-lsp></span><span style="color: #D4D4D4"> {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">typeof</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: unknown' >value</data-lsp></span><span style="color: #D4D4D4"> === </span><span style="color: #CE9178">"object"</span><span style="color: #D4D4D4"> && </span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: object | null' >value</data-lsp></span><span style="color: #D4D4D4"> !== </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4"> && </span><span style="color: #CE9178">"name"</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">in</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: object' >value</data-lsp></span><span style="color: #D4D4D4">;</span></div><div class='line'><span style="color: #D4D4D4">}</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// let&apos;s pretend JSON.parse is an unknown data source</span></div><div class='line'><span style="color: #569CD6">const</span><span style="color: #D4D4D4"> </span><span style="color: #4FC1FF"><data-lsp lsp='const data: any' >data</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0"><data-lsp lsp='var JSON: JSON' >JSON</data-lsp></span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA"><data-lsp lsp='(method) JSON.parse(text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any' >parse</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&apos;{"name": "Stefan"}&apos;</span><span style="color: #D4D4D4">);</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #DCDCAA"><data-lsp lsp='function isUser(value: unknown): value is User' >isUser</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE"><data-lsp lsp='const data: any' >data</data-lsp></span><span style="color: #D4D4D4">)) {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE"><data-lsp lsp='const data: User' style='border-bottom: solid 2px lightgrey;'>data</data-lsp></span><span style="color: #D4D4D4">.</span><span style="color: #9CDCFE"><data-lsp lsp='(property) User.name: string' >name</data-lsp></span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA"><data-lsp lsp='(method) String.toUpperCase(): string' >toUpperCase</data-lsp></span><span style="color: #D4D4D4">(); </span><span style="color: #6A9955">// narrowed to User inside of condition scope</span></div><div class='meta-line'><span class='popover-prefix'>   </span><span class='popover'><div class='arrow'></div>const data: User</span></div><div class='line'><span style="color: #D4D4D4">}</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #9CDCFE"><data-lsp lsp='const data: any' style='border-bottom: solid 2px lightgrey;'>data</data-lsp></span><span style="color: #D4D4D4">;</span></div><div class='meta-line'><span class='popover-prefix'> </span><span class='popover'><div class='arrow'></div>const data: any</span></div></code></div></pre>
<p>If you receive data from an unknown source (most likely via <code>fetch</code> or HTTP), TypeScript doesn't know the types. To be on the safe side, you should bring in a validation library like <a href="https://zod.dev/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fzod.dev%2F')">Zod</a> but if you only care about the types, a classic type guard might do the trick.</p>
<p>When you check the types above you see that thanks to the <code>isUser</code> type guard, <code>data</code> is of type <code>User</code> inside the condition scope. Great! But if you leave the scope you'll see that <code>data</code> is again back at <code>any</code>. Booh!</p>
<p>Now check this out!</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #569CD6">interface</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='interface User' >User</data-lsp></span><span style="color: #D4D4D4"> {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE"><data-lsp lsp='(property) User.name: string' >name</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0">string</span><span style="color: #D4D4D4">;</span></div><div class='line'><span style="color: #D4D4D4">}</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// assertion function</span></div><div class='line'><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA"><data-lsp lsp='function assertIsUser(value: unknown): asserts value is User' >assertIsUser</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: unknown' >value</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0">unknown</span><span style="color: #D4D4D4">): asserts </span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: unknown' >value</data-lsp></span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">is</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='interface User' >User</data-lsp></span><span style="color: #D4D4D4"> {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #C586C0">if</span><span style="color: #D4D4D4"> (</span><span style="color: #569CD6">typeof</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: unknown' >value</data-lsp></span><span style="color: #D4D4D4"> !== </span><span style="color: #CE9178">"object"</span><span style="color: #D4D4D4"> || </span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: object | null' >value</data-lsp></span><span style="color: #D4D4D4"> === </span><span style="color: #569CD6">null</span><span style="color: #D4D4D4"> || !(</span><span style="color: #CE9178">"name"</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">in</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) value: object' >value</data-lsp></span><span style="color: #D4D4D4">)) {</span></div><div class='line'><span style="color: #D4D4D4">    </span><span style="color: #C586C0">throw</span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">new</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='var Error: ErrorConstructor&#10;new (message?: string | undefined) => Error' >Error</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">"Expected a User"</span><span style="color: #D4D4D4">);</span></div><div class='line'><span style="color: #D4D4D4">  }</span></div><div class='line'><span style="color: #D4D4D4">}</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// let&apos;s pretend JSON.parse is an unknown data source</span></div><div class='line'><span style="color: #569CD6">const</span><span style="color: #D4D4D4"> </span><span style="color: #4FC1FF"><data-lsp lsp='const data: any' >data</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0"><data-lsp lsp='var JSON: JSON' >JSON</data-lsp></span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA"><data-lsp lsp='(method) JSON.parse(text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any' >parse</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&apos;{"name": "Stefan"}&apos;</span><span style="color: #D4D4D4">);</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #DCDCAA"><data-lsp lsp='function assertIsUser(value: unknown): asserts value is User' >assertIsUser</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE"><data-lsp lsp='const data: any' >data</data-lsp></span><span style="color: #D4D4D4">); </span><span style="color: #6A9955">// throws if not a User</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #9CDCFE"><data-lsp lsp='const data: User' style='border-bottom: solid 2px lightgrey;'>data</data-lsp></span><span style="color: #D4D4D4">.</span><span style="color: #9CDCFE"><data-lsp lsp='(property) User.name: string' >name</data-lsp></span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA"><data-lsp lsp='(method) String.toUpperCase(): string' >toUpperCase</data-lsp></span><span style="color: #D4D4D4">(); </span><span style="color: #6A9955">// narrowed to User after assertion</span></div><div class='meta-line'><span class='popover-prefix'> </span><span class='popover'><div class='arrow'></div>const data: User</span></div></code></div></pre>
<p>If you now change the type guard to be an assertion function with <code>asserts value is User</code> the type will be adjusted for the rest of <strong>the current scope</strong>. The contract is: if this function doesn't throw, apply the type to the current scope.</p>
<p>Good stuff!</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20The scope of type guards and assertion functions">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        New lines are removed from WHATWG URLs
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/new-lines-are-removed-from-whatwg-urls/"/>
      <published>2026-03-02T23:00:00+00:00</published>
      <updated>2026-03-02T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/new-lines-are-removed-from-whatwg-urls/
      </id>
      <category term="tilPost"></category>
        <category term="HTML"></category>
      
        <category term="JavaScript"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>Today I learned something groundbreaking about URLs!</p>
<p>You know that when you're templating HTML and you're dealing with attribute values, it might make sense to break things into pieces. I like to have one attribute per line and sometimes even break the attribute values into multiple lines.</p>
<p>Long URLs are always tricky, because you can't break these apart...</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>something something_else 
          something_very_long something_even_longer<span class="token punctuation">"</span></span>
   <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://www.example-blog-about-everything-under-the-sun.com/2026/03/03/the-ultimate-comprehensive-and-absolutely-definitive-guide-to-understanding-why-your-css-layout-breaks-at-3am-when-you-least-expect-it-and-how-to-fix-it-once-and-for-all-without-losing-your-sanity-or-your-will-to-live-as-a-frontend-developer-in-the-modern-web-ecosystem-featuring-flexbox-grid-and-container-queries<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
  Long link
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span>
</code></pre>
<p>Or can you?</p>
<p><a href="https://lemire.me/blog/2026/02/28/you-can-use-newline-characters-in-urls/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Flemire.me%2Fblog%2F2026%2F02%2F28%2Fyou-can-use-newline-characters-in-urls%2F')">Daniel shared that line breaks and tabs will actually be removed from URLs</a>. Excuse me?!</p>
<p>Here's <a href="https://url.spec.whatwg.org/#url-parsing" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Furl.spec.whatwg.org%2F%23url-parsing')">the URL parsing WHATWG spec</a>:</p>
<blockquote>
<ol start="2">
<li>If input contains any ASCII tab or newline, invalid-URL-unit validation error.</li>
<li>Remove all ASCII tab or newline from input.</li>
</ol>
</blockquote>
<p>So, browsers discover newlines or tabs in URLs, recognize them as &quot;invalid-URL-unit&quot; errors, and then remove the invalid characters to get the job done. Nice job, browsers — I love it!</p>
<p>Here's a rendred link including a URL with tabs and newlines for you to inspect.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>Tabs and newslines are removed and things just work and because we're looking at the WHATWG spec in action here, the characters will be remove in JavaScript, too!</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> urlWithTabsAndNewlines <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://www.	
stefanjudis.
com</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>

<span class="token keyword">const</span> normalizedUrl <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span>urlWithTabsAndNewlines<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
  normalizedUrl<span class="token punctuation">.</span>href <span class="token operator">===</span> urlWithTabsAndNewlines
<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// false</span>
</code></pre>
<p>That's pretty wild, isn't it?</p>
<p><a href="https://lemire.me/blog/2026/02/28/you-can-use-newline-characters-in-urls/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Flemire.me%2Fblog%2F2026%2F02%2F28%2Fyou-can-use-newline-characters-in-urls%2F')">Check Daniel's post if you want to learn more</a>!</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20New lines are removed from WHATWG URLs">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        How to scale elements and their layout with CSS &quot;zoom&quot;
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/css-zoom-to-scale-elements/"/>
      <published>2026-02-23T23:00:00+00:00</published>
      <updated>2026-02-23T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/css-zoom-to-scale-elements/
      </id>
      <category term="tilPost"></category>
        <category term="CSS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>You probably know that you can use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/scale" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FCSS%2FReference%2FValues%2Ftransform-function%2Fscale')">the scale function <code>scale()</code></a> or even just <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/scale" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FCSS%2FReference%2FProperties%2Fscale')"><code>scale</code> property</a> to transform and change an element's size.</p>
<p>The thing with <code>scale</code> is that it only changes visual appearance and the layout size of the target element remains the same.</p>
<p>Today I learned there's also <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/zoom" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FCSS%2FReference%2FProperties%2Fzoom')">the <code>zoom</code> property</a>. <code>zoom</code> &quot;really&quot; scales the element and its layout. Check this out!</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>What's the browser support?</p>
<div class="highlightBox mdn margin-top-xl margin-bottom-xl">
        <div class="cornerBubble">
          <svg aria-hidden="true"><use xlink:href="/sprite.svg#icon-mdn"></use></svg>
        </div>
        <div class="highlightBox__header">MDN Compat Data (<a href="https://raw.githubusercontent.com/mdn/browser-compat-data/refs/heads/main/css/properties/zoom.json">source</a>)</div>
        <div class="highlightBox__body">
          <div class="highlightBox__overflow">
            <table class="highlightBox__compat">
              <caption>Browser support info for <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/zoom">zoom</a> </caption>
              <thead>
                <tr>
                  <td>
                        <img src="/assets/browsers/chrome.webp"
                              srcset="/assets/browsers/chrome.webp, /assets/browsers/chrome@2.webp 2x"
                              width="48" height="51" alt="chrome">
                      </td><td>
                        <img src="/assets/browsers/chrome_android.webp"
                              srcset="/assets/browsers/chrome_android.webp, /assets/browsers/chrome_android@2.webp 2x"
                              width="48" height="51" alt="chrome_android">
                      </td><td>
                        <img src="/assets/browsers/edge.webp"
                              srcset="/assets/browsers/edge.webp, /assets/browsers/edge@2.webp 2x"
                              width="48" height="51" alt="edge">
                      </td><td>
                        <img src="/assets/browsers/firefox.webp"
                              srcset="/assets/browsers/firefox.webp, /assets/browsers/firefox@2.webp 2x"
                              width="48" height="51" alt="firefox">
                      </td><td>
                        <img src="/assets/browsers/firefox_android.webp"
                              srcset="/assets/browsers/firefox_android.webp, /assets/browsers/firefox_android@2.webp 2x"
                              width="48" height="51" alt="firefox_android">
                      </td><td>
                        <img src="/assets/browsers/safari.webp"
                              srcset="/assets/browsers/safari.webp, /assets/browsers/safari@2.webp 2x"
                              width="48" height="51" alt="safari">
                      </td><td>
                        <img src="/assets/browsers/safari_ios.webp"
                              srcset="/assets/browsers/safari_ios.webp, /assets/browsers/safari_ios@2.webp 2x"
                              width="48" height="51" alt="safari_ios">
                      </td><td>
                        <img src="/assets/browsers/samsunginternet_android.webp"
                              srcset="/assets/browsers/samsunginternet_android.webp, /assets/browsers/samsunginternet_android@2.webp 2x"
                              width="48" height="51" alt="samsunginternet_android">
                      </td><td>
                        <img src="/assets/browsers/webview_android.webp"
                              srcset="/assets/browsers/webview_android.webp, /assets/browsers/webview_android@2.webp 2x"
                              width="48" height="51" alt="webview_android">
                      </td>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td>
                        <span class="highlightBox__pill success margin-top-s">
                        1
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        1
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        12
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        126
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        126
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        3.1
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        3
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        1.5
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        1
                        </span>
                      </td>
                </tr>
              </tbody>
            </table>
          </div>
<p></div>
</div></p>
<p>It's pretty green!</p>
<p>I can totally see constrained situations where this can come in handy, but you should avoid animating it because browsers probably don't appreciate the layout shift.</p>
<div class="highlightBox attention margin-top-xl margin-bottom-xl">
      
          <div class="cornerBubble">
            <svg aria-hidden="true">
              <use xlink:href="/sprite.svg#icon-attention"/>
            </svg>
          </div>
        

      
      <p>Someone pointed out that there are still browser inconsistencies for zoomed elements. Boooh!</p>
<p>Apparently, Safari doesn't return the updated element layout size if you use <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FElement%2FgetBoundingClientRect')"><code>getBoundingClientRect()</code></a>. <code>width</code> and <code>height</code> remain the initial size of 120px time 120px (tested in Safari 26.3).</p>
<p>Chrome and Firefox work as expected.</p>
</div></div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20How to scale elements and their layout with CSS &quot;zoom&quot;">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        How to style the found search / &quot;find in page&quot; substrings
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/how-to-style-find-in-page-substrings/"/>
      <published>2026-02-01T23:00:00+00:00</published>
      <updated>2026-02-01T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/how-to-style-find-in-page-substrings/
      </id>
      <category term="tilPost"></category>
        <category term="CSS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>Chrome 144 shipped new CSS pseudo-elements!</p>
<pre class="language-css"><code class="language-css"><span class="token selector">::search-text</span> <span class="token punctuation">{</span>
  <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--blue<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token selector">::search-text:current</span> <span class="token punctuation">{</span>
  <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--blue-darker<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span>
  <span class="token property">text-decoration</span><span class="token punctuation">:</span> currentColor solid underline<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>The classes are so new that there aren't MDN pages yet, but I'm super excited about this tiny search improvement.</p>
<p>I'm pretty quick with hitting <code>CMD + F</code> because often I can't be bothered with using the provided site search (if there is one). However, sometimes it's hard to understand what's currently selected if you're looking at multiple matching substrings.</p>
<p>Today I learned, this is a Chrome/Firefox problem because Safari's &quot;find in page&quot; feature adds an overlay while highlighting the current substring.</p>
<p>
      <div class="sqip-container margin-top-l margin-bottom-l">
        <figure class="sqip-image" style="
          --color1: rgb(250,250,4); --color2: rgb(113,123,130); --color3: rgb(243,244,251); --color4: rgb(188,188,196); --color5: rgb(52,68,84);
          --sqip-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTM1Ij48cGF0aCBmaWxsPSIjMzQ0NDU0IiBkPSJNMCAwaDMwMHYxMzVIMHoiLz48ZyBmaWxsLW9wYWNpdHk9Ii41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSguNiAuNikgc2NhbGUoMS4xNzE4OCkiPjxjaXJjbGUgY3g9IjE0OCIgY3k9Ijg0IiByPSIxODMiIGZpbGw9IiNmZmYiLz48Y2lyY2xlIGN4PSIxOTYiIGN5PSI3MyIgcj0iMjEyIiBmaWxsPSIjYzJjM2JmIi8+PHBhdGggZmlsbD0iI2Q1ZDIwMCIgZD0iTTExIDEzaDI3djEwSDExeiIvPjxjaXJjbGUgcj0iMSIgZmlsbD0iIzlkYTJhNyIgdHJhbnNmb3JtPSJtYXRyaXgoLTkuNTQ2NSAzNi44MzM1IC04MS42NDMzNSAtMjEuMTYwMzEgMTA4LjYgNTYuOCkiLz48ZWxsaXBzZSBjeD0iNTkiIGN5PSI4NiIgZmlsbD0iIzY4NzE3YSIgcng9IjIwNSIgcnk9IjIiLz48ZWxsaXBzZSBjeD0iMTI2IiBjeT0iNjMiIGZpbGw9IiM2NjcxN2EiIHJ4PSIxMjIiIHJ5PSIyIi8+PGVsbGlwc2UgY3g9IjE0OSIgY3k9IjE4IiBmaWxsPSIjNzM3Yzg0IiByeD0iODQiIHJ5PSIzIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTI5IDY3aDIxdjZIMjl6Ii8+PC9nPjwvc3ZnPg==');
        ">
          <a href="//images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png">
            <picture>
              <source type="image/avif"
                srcset="//images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=avif&fit=scale&q=75&w=300&h=135 300w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=avif&fit=scale&q=75&w=500&h=226 500w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=avif&fit=scale&q=75&w=700&h=317 700w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=avif&fit=scale&q=75&w=900&h=407 900w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=avif&fit=scale&q=75&w=1100&h=498 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <source type="image/webp"
                srcset="//images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=webp&fit=scale&q=75&w=300&h=135 300w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=webp&fit=scale&q=75&w=500&h=226 500w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=webp&fit=scale&q=75&w=700&h=317 700w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=webp&fit=scale&q=75&w=900&h=407 900w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=webp&fit=scale&q=75&w=1100&h=498 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <img width="1000" height="453"
                srcset="//images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=jpg&fit=scale&q=75&w=300&h=135 300w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=jpg&fit=scale&q=75&w=500&h=226 500w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=jpg&fit=scale&q=75&w=700&h=317 700w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=jpg&fit=scale&q=75&w=900&h=407 900w, //images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png?fm=jpg&fit=scale&q=75&w=1100&h=498 1100w"
                sizes="(max-width: 50em) 98vw, 700px"
                src="//images.ctfassets.net/f20lfrunubsq/2pdfeK6acwfOnBDZFysTL1/13e62f9d90f52bde778376f0720a3089/Screenshot_2026-02-02_at_14.21.53.png"
                alt="Find in page in Safari showing highlights for all matching strings while marking the currently selected one."
                loading="lazy"
                onload="this.classList.add('kf-fade-in')">
            </picture>
          </a>
        </figure>
      </div>
    </p>
<p>This looks great, but I won't switch browsers for a better in-page search experience. So what about Chrome and Firefox?</p>
<p><code>::search-text</code> allows you to style the found strings to follow your site's style. This is good stuff but not really helping with understanding the currently selected search string. This is where <code>::search-text:current</code> comes into play. Check this out!</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>If you're on Chrome 144 right now, search the page for &quot;doggo&quot;! The matching strings in the component above are matching my site's colors and you can now see which dog is the selected search entry. It doesn't look as fancy as the Safari overlay but I'll buy it!</p>
<p>
      <div class="sqip-container margin-top-l margin-bottom-l">
        <figure class="sqip-image" style="
          --color1: rgb(28,124,188); --color2: rgb(112,128,140); --color3: rgb(150,194,224); --color4: rgb(162,180,198); --color5: rgb(60,76,92);
          --sqip-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTMxIj48cGF0aCBmaWxsPSIjM2M0YzVjIiBkPSJNMCAwaDMwMHYxMzFIMHoiLz48ZyBmaWxsLW9wYWNpdHk9Ii41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSguNiAuNikgc2NhbGUoMS4xNzE4OCkiPjxjaXJjbGUgY3g9IjEyNiIgY3k9Ijg5IiByPSIxNzAiIGZpbGw9IiNmZmYiLz48ZWxsaXBzZSBjeD0iMTQ4IiBjeT0iNzQiIGZpbGw9IiNmZmYiIHJ4PSIyMDAiIHJ5PSIxMTEiLz48Y2lyY2xlIGN4PSIxMzYiIGN5PSI5NCIgcj0iMTY4IiBmaWxsPSIjZWFmM2ZiIi8+PGNpcmNsZSByPSIxIiBmaWxsPSIjMTIxZDIyIiB0cmFuc2Zvcm09Im1hdHJpeCgtMTIuMDMzIC4wNjMgLS4wMjE2OSAtNC4xNDIgMTM0LjUgMzcuNikiLz48ZWxsaXBzZSBjeD0iMTIzIiBjeT0iMTA1IiBmaWxsPSIjZmVmZmZmIiByeD0iMjU1IiByeT0iMjAiLz48cGF0aCBmaWxsPSIjMDA2OGI3IiBkPSJNMTIgMTNoMjF2OEgxMnoiLz48cGF0aCBmaWxsPSIjMGY3YmMxIiBkPSJNMTcyIDU2aDE4djEwaC0xOHoiLz48ZWxsaXBzZSBjeD0iMTI5IiBjeT0iNTAiIGZpbGw9IiM4YTk2YTAiIHJ4PSIxMTQiIHJ5PSIzIi8+PC9nPjwvc3ZnPg==');
        ">
          <a href="//images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png">
            <picture>
              <source type="image/avif"
                srcset="//images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=avif&fit=scale&q=75&w=300&h=132 300w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=avif&fit=scale&q=75&w=500&h=220 500w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=avif&fit=scale&q=75&w=700&h=308 700w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=avif&fit=scale&q=75&w=900&h=396 900w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=avif&fit=scale&q=75&w=1100&h=484 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <source type="image/webp"
                srcset="//images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=webp&fit=scale&q=75&w=300&h=132 300w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=webp&fit=scale&q=75&w=500&h=220 500w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=webp&fit=scale&q=75&w=700&h=308 700w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=webp&fit=scale&q=75&w=900&h=396 900w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=webp&fit=scale&q=75&w=1100&h=484 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <img width="1000" height="440"
                srcset="//images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=jpg&fit=scale&q=75&w=300&h=132 300w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=jpg&fit=scale&q=75&w=500&h=220 500w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=jpg&fit=scale&q=75&w=700&h=308 700w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=jpg&fit=scale&q=75&w=900&h=396 900w, //images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png?fm=jpg&fit=scale&q=75&w=1100&h=484 1100w"
                sizes="(max-width: 50em) 98vw, 700px"
                src="//images.ctfassets.net/f20lfrunubsq/1XHhS3BCUgvMZqzGKqOang/18a455b844f0ee896002ed3f8d11611a/Screenshot_2026-02-02_at_14.27.02.png"
                alt="Find in page highlighting all strings in blue and the currently matching one in black."
                loading="lazy"
                onload="this.classList.add('kf-fade-in')">
            </picture>
          </a>
        </figure>
      </div>
    </p>
<p>Highlighting &quot;find in page&quot; entries isn't the most groundbreaking feature in the world, but I'm excited because I think it'll make my search experience way better! Apparently, <a href="https://github.com/mozilla/standards-positions/issues/1103" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fgithub.com%2Fmozilla%2Fstandards-positions%2Fissues%2F1103')">there's no clear thumbs up from Mozilla yet</a>, but let's hope that we'll get this improvement on the web.</p>
<p>If you want to dive deeper, here are some more resources:</p>
<ul>
<li>From Igalia: <a href="https://blogs.igalia.com/schenney/find-in-page-highlight-styling/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fblogs.igalia.com%2Fschenney%2Ffind-in-page-highlight-styling%2F')">Find-in-Page Highlight Styling</a></li>
<li><a href="https://drafts.csswg.org/css-pseudo-4/#selectordef-search-text" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdrafts.csswg.org%2Fcss-pseudo-4%2F%23selectordef-search-text')">The <code>search-text</code> spec</a></li>
</ul>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20How to style the found search / &quot;find in page&quot; substrings">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        ARIA roles can remove their children’s semantics
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/aria-roles-can-remove-their-childrens-semantics/"/>
      <published>2026-01-12T23:00:00+00:00</published>
      <updated>2026-01-12T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/aria-roles-can-remove-their-childrens-semantics/
      </id>
      <category term="tilPost"></category>
        <category term="Accessibility"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>I've learned something new about ARIA today!</p>
<p>You probably know the first rule of ARIA:</p>
<blockquote>
<p>If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of repurposing an element and adding an ARIA role, state, or property to make it accessible, then do so.</p>
</blockquote>
<p>Unfortunately, many people slap ARIA attributes on elements without really understanding what they're doing. Adding <code>role=&quot;menu&quot;</code> to navigation lists is one example that I see very often.</p>
<p>What's supposed to be a <code>menu</code>? Here's <a href="https://w3c.github.io/aria/#menu" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fw3c.github.io%2Faria%2F%23menu')">the ARIA spec</a>:</p>
<blockquote>
<p>A <code>menu</code> is a container, generally rendered as a popup or overlay, for a set of menu items that can be invoked to perform an action or function.</p>
</blockquote>
<p>Making a list of navigation entries a <code>menu</code> isn't a good idea because <strong>a menu is supposed to include menu items that invoke actions or functions</strong>. However, that's not all.</p>
<p>Today I learned that adding ARIA attributes to elements can also change the semantics of the included elements. Here's the <code>role=&quot;menu&quot;</code> example for a <code>ul</code> case.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>A &quot;normal&quot; list might be announced by a screen reader as &quot;List (3 items)&quot;. It's helpful to know how many elements are in the list, right? A <code>menu</code>, though, must include menu items. And if you're not planning to also add elements with <code>role=&quot;menuitem&quot;</code> to the menu, it'll be empty.</p>
<p>I quickly tested Chrome with VoiceOver <a href="https://codepen.io/stefanjudis/pen/ZYOBKQY" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fcodepen.io%2Fstefanjudis%2Fpen%2FZYOBKQY')">on this CodePen</a>:</p>
<ul>
<li>A normal list is announced as <em>&quot;List (3 items)&quot;</em>.</li>
<li>A list with <code>role=&quot;menu&quot;</code> without included <code>menuitems</code> isn't announced at all. The list entries are read out loud.</li>
<li>A list with <code>role=&quot;menu&quot;</code> with included <code>menuitems</code> is announced as <em>&quot;Menu (3 items)&quot;</em>.</li>
</ul>
<p>Incorrect <code>role=&quot;menu&quot;</code> usage can not only be confusing but might remove the list item count and nuke the item semantics.</p>
<p>Don't mess with ARIA unless you know what you're doing.</p>
<p><a href="https://www.maxdesign.com.au/articles/aria-roles-semantics.html" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.maxdesign.com.au%2Farticles%2Faria-roles-semantics.html')">Check this post</a> if you want to dive deeper into ARIA and what other roles affect their children semantics.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20ARIA roles can remove their children’s semantics">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        Automatically load .env files in Node.js scripts
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/load-env-files-in-node-js-scripts/"/>
      <published>2026-01-05T23:00:00+00:00</published>
      <updated>2026-01-05T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/load-env-files-in-node-js-scripts/
      </id>
      <category term="tilPost"></category>
        <category term="NodeJS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>Node.js continues to follow the lead of competing runtimes and implements more and more userland features. Loading <code>.env</code> files was one of these features that's been solved in userland (<a href="https://www.npmjs.com/package/dotenv" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fdotenv')">dotenv has 46m weekly downloads</a>) since I started writing Node.js code almost 15 years ago.</p>
<p>Today I learned that Node.js not only added support for loading environment files from the command line with the <a href="https://nodejs.org/api/cli.html#--env-filefile" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fnodejs.org%2Fapi%2Fcli.html%23--env-filefile')"><code>--env-file</code></a> and <a href="https://nodejs.org/api/cli.html#--env-file-if-existsfile" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fnodejs.org%2Fapi%2Fcli.html%23--env-file-if-existsfile')"><code>--env-file-if-exists</code></a> flags, but also added a <a href="https://nodejs.org/api/process.html#processloadenvfilepath" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fnodejs.org%2Fapi%2Fprocess.html%23processloadenvfilepath')">new <code>loadEnvFile()</code> method</a> to load <code>.env</code> files right in your scripts, similar to how <code>dotenv</code> does it. The new method is marked stable since Node.js v24.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> loadEnvFile <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'node:process'</span><span class="token punctuation">;</span>

<span class="token comment">// load .env file with default path ('./.env')</span>
<span class="token function">loadEnvFile</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// load .env file with a custom path</span>
<span class="token function">loadEnvFile</span><span class="token punctuation">(</span><span class="token string">'../../.env'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>One thing to watch out for, though, is that <code>loadEnvFile()</code> throws if the <code>.env</code> file doesn't exist, which is usually the case in production environments. But that's no problem, and I wrapped it into a small utility when I was removing <code>dotenv</code> from one of my projects.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> loadEnvFile <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'node:process'</span><span class="token punctuation">;</span>

<span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">safeLoadEnvFile</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span>
  <span class="token keyword">try</span> <span class="token punctuation">{</span>
    <span class="token function">loadEnvFile</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token comment">// In prod environments there's usually no `.env` file.</span>
    <span class="token comment">// Ignore the error thrown if the file doesn't exist.</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p>I love seeing Node.js moving forward!</p>
<p>Edit: If you're looking for a more advanced snippet to bring into your projects, <a href="https://gist.github.com/franky47/9ce6eaf80b4c74b63988370b7c39fd23" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fgist.github.com%2Ffranky47%2F9ce6eaf80b4c74b63988370b7c39fd23')">François shared an environment-aware way to load <code>.env</code> files</a>.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20Automatically load .env files in Node.js scripts">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        light-dark() isn&#39;t always the same as prefers-color-scheme
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/light-dark-isnt-the-same-as-prefers-color-scheme/"/>
      <published>2025-11-23T23:00:00+00:00</published>
      <updated>2025-11-23T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/light-dark-isnt-the-same-as-prefers-color-scheme/
      </id>
      <category term="tilPost"></category>
        <category term="CSS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>I've thought that the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/light-dark" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FCSS%2FReference%2FValues%2Fcolor_value%2Flight-dark')">new <code>light-dark()</code> CSS function</a> can be used as a drop-in replacement for <code>prefers-color-scheme</code> media queries. Today I learned that they don't always behave the same.</p>
<p>Here's how MDN describes the new color function:</p>
<blockquote>
<p>The <code>light-dark()</code> CSS <code>&lt;color&gt;</code> function enables setting two colors for a property [...] without needing to encase the theme colors within a prefers-color-scheme media feature query.</p>
</blockquote>
<p>If you wonder about <code>light-dark()</code> browser support, here we go.</p>
<div class="highlightBox mdn margin-top-xl margin-bottom-xl">
        <div class="cornerBubble">
          <svg aria-hidden="true"><use xlink:href="/sprite.svg#icon-mdn"></use></svg>
        </div>
        <div class="highlightBox__header">MDN Compat Data (<a href="https://raw.githubusercontent.com/mdn/browser-compat-data/refs/heads/main/css/types/color.json">source</a>)</div>
        <div class="highlightBox__body">
          <div class="highlightBox__overflow">
            <table class="highlightBox__compat">
              <caption>Browser support info for <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Values/color_value/light-dark">`light-dark()`</a> </caption>
              <thead>
                <tr>
                  <td>
                        <img src="/assets/browsers/chrome.webp"
                              srcset="/assets/browsers/chrome.webp, /assets/browsers/chrome@2.webp 2x"
                              width="48" height="51" alt="chrome">
                      </td><td>
                        <img src="/assets/browsers/chrome_android.webp"
                              srcset="/assets/browsers/chrome_android.webp, /assets/browsers/chrome_android@2.webp 2x"
                              width="48" height="51" alt="chrome_android">
                      </td><td>
                        <img src="/assets/browsers/edge.webp"
                              srcset="/assets/browsers/edge.webp, /assets/browsers/edge@2.webp 2x"
                              width="48" height="51" alt="edge">
                      </td><td>
                        <img src="/assets/browsers/firefox.webp"
                              srcset="/assets/browsers/firefox.webp, /assets/browsers/firefox@2.webp 2x"
                              width="48" height="51" alt="firefox">
                      </td><td>
                        <img src="/assets/browsers/firefox_android.webp"
                              srcset="/assets/browsers/firefox_android.webp, /assets/browsers/firefox_android@2.webp 2x"
                              width="48" height="51" alt="firefox_android">
                      </td><td>
                        <img src="/assets/browsers/safari.webp"
                              srcset="/assets/browsers/safari.webp, /assets/browsers/safari@2.webp 2x"
                              width="48" height="51" alt="safari">
                      </td><td>
                        <img src="/assets/browsers/safari_ios.webp"
                              srcset="/assets/browsers/safari_ios.webp, /assets/browsers/safari_ios@2.webp 2x"
                              width="48" height="51" alt="safari_ios">
                      </td><td>
                        <img src="/assets/browsers/samsunginternet_android.webp"
                              srcset="/assets/browsers/samsunginternet_android.webp, /assets/browsers/samsunginternet_android@2.webp 2x"
                              width="48" height="51" alt="samsunginternet_android">
                      </td><td>
                        <img src="/assets/browsers/webview_android.webp"
                              srcset="/assets/browsers/webview_android.webp, /assets/browsers/webview_android@2.webp 2x"
                              width="48" height="51" alt="webview_android">
                      </td>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td>
                        <span class="highlightBox__pill success margin-top-s">
                        123
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        123
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        123
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        120
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        120
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        17.5
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        17.5
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        27.0
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        123
                        </span>
                      </td>
                </tr>
              </tbody>
            </table>
          </div>
<p></div>
</div></p>
<p>Now, I thought you could replace all your verbose color scheme queries with short <code>light-dark()</code> one-liners.</p>
<pre class="language-css"><code class="language-css"><span class="token comment">/* The old way of flipping dark mode styles */</span>
<span class="token selector">.someElement</span> <span class="token punctuation">{</span>
  <span class="token property">background</span><span class="token punctuation">:</span> #eee<span class="token punctuation">;</span>
  <span class="token property">color</span><span class="token punctuation">:</span> #333<span class="token punctuation">;</span>
  <span class="token property">border-color</span><span class="token punctuation">:</span> #ddd<span class="token punctuation">;</span>

  <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-color-scheme</span><span class="token punctuation">:</span> dark<span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
    <span class="token property">background</span><span class="token punctuation">:</span> #333<span class="token punctuation">;</span>
    <span class="token property">color</span><span class="token punctuation">:</span> #eee<span class="token punctuation">;</span>
    <span class="token property">border-color</span><span class="token punctuation">:</span> #444<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token comment">/* The new way of flipping dark mode styles */</span>
<span class="token selector">.someElement</span> <span class="token punctuation">{</span>
  <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>#eee<span class="token punctuation">,</span> #333<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>#333<span class="token punctuation">,</span> #eee<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">border-color</span><span class="token punctuation">:</span> <span class="token function">light-dark</span><span class="token punctuation">(</span>#ddd<span class="token punctuation">,</span> #444<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Unfortunately, things aren't that easy.</p>
<div class="heading-wrapper">
      <a class="margin-left-s text-no-underline" aria-label="Link to heading" aria-describedby="%60light-dark()%60-requires-a-%60color-scheme%60" href="#%60light-dark()%60-requires-a-%60color-scheme%60">
        <svg class="icon" aria-hidden="true">
          <use xlink:href="/sprite.svg#icon-link"/>
        </svg>
      </a>
      <h2 id="%60light-dark()%60-requires-a-%60color-scheme%60"><code>light-dark()</code> requires a <code>color-scheme</code></h2></div><p>The first thing to realize is that <code>light-dark()</code> requires <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/color-scheme" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FCSS%2FReference%2FProperties%2Fcolor-scheme')">a properly set <code>color-scheme</code></a>. Here's MDN again:</p>
<blockquote>
<p>To enable support for the <code>light-dark()</code> color function, the <code>color-scheme</code> must have a value of <code>light dark</code>, usually set on the <code>:root</code> pseudo-class.</p>
</blockquote>
<p>Without setting <code>color-scheme: light dark;</code> on <code>:root</code> or a surrounding container, the <code>light-dark()</code> function won't work. Flip your operating system's color theme and you'll see that <code>light-dark()</code> doesn't work below.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>The <code>prefers-color-scheme</code> query, on the other hand, will react to your operating system's preferences no matter what. This behavior seems strange, but I assume that <code>prefers-color-scheme</code> was introduced way before the <code>color-scheme</code> property was a thing, so that considering <code>color-scheme</code> would have been a breaking change.</p>
<p>However, adding <code>color-scheme: light dark;</code> is quickly done, right?</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>While working on the new <a href="https://webweekly.email/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwebweekly.email%2F')">Web Weekly</a> site, I implemented a color theme toggle and things got interesting.</p>
<p>
      <div class="sqip-container margin-top-l margin-bottom-l">
        <figure class="sqip-image" style="
          --color1: rgb(52,154,211); --color2: rgb(82,115,166); --color3: rgb(250,226,196); --color4: rgb(213,186,169); --color5: rgb(49,61,82);
          --sqip-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTYxIj48cGF0aCBmaWxsPSIjMzEzZDUyIiBkPSJNMCAwaDMwMHYxNjFIMHoiLz48ZyBmaWxsLW9wYWNpdHk9Ii41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSguNiAuNikgc2NhbGUoMS4xNzE4OCkiPjxjaXJjbGUgcj0iMSIgZmlsbD0iI2ZmZiIgdHJhbnNmb3JtPSJyb3RhdGUoLTI3LjUgOTguMSAtMTU5LjYpIHNjYWxlKDI1NSA2MS4wMTc5NSkiLz48Y2lyY2xlIHI9IjEiIGZpbGw9IiNmZmYiIHRyYW5zZm9ybT0ibWF0cml4KC0zMS44ODQ1MiAtLjI1MjU2IC4xNjc5IC0yMS4xOTU3MSAyMDUuMiA5My40KSIvPjxjaXJjbGUgcj0iMSIgZmlsbD0iI2ZmZiIgdHJhbnNmb3JtPSJtYXRyaXgoLTI0LjgyMzg3IC00NS4xMDM4MiAyMjAuMjI3ODQgLTEyMS4yMDcyMSA3OS4yIDM4KSIvPjxwYXRoIGZpbGw9IiNmZmZmZjEiIGQ9Ik0yMDMuOSAxMTcuOCAxODEuNy0xNC4xbDMxLjEgNTcuNiAxNS4xIDU5LjV6Ii8+PGNpcmNsZSByPSIxIiBmaWxsPSIjZWZmIiB0cmFuc2Zvcm09Im1hdHJpeCgyMTEuMDg3MyAtNDIuMTc5NCAxMS43ODgyOCA1OC45OTQ1NyA2Ny44IDUpIi8+PGNpcmNsZSBjeD0iMTI3IiBjeT0iMTE5IiByPSI0NiIgZmlsbD0iIzAwMDIxNyIvPjxjaXJjbGUgcj0iMSIgZmlsbD0iI2ZmZiIgdHJhbnNmb3JtPSJyb3RhdGUoMTQwLjEgNzguMyA4Mikgc2NhbGUoMTIuMTE5MDQgMjQuODU1NTEpIi8+PGVsbGlwc2UgY3g9IjExMCIgY3k9IjEwOSIgZmlsbD0iIzdiZWZmZiIgcng9IjE4IiByeT0iNiIvPjwvZz48L3N2Zz4=');
        ">
          <a href="https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg">
            <picture>
              <source type="image/avif"
                srcset="https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=avif&fit=scale&q=75&w=300&h=162 300w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=avif&fit=scale&q=75&w=500&h=270 500w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=avif&fit=scale&q=75&w=700&h=378 700w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=avif&fit=scale&q=75&w=900&h=486 900w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=avif&fit=scale&q=75&w=1100&h=594 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <source type="image/webp"
                srcset="https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=webp&fit=scale&q=75&w=300&h=162 300w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=webp&fit=scale&q=75&w=500&h=270 500w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=webp&fit=scale&q=75&w=700&h=378 700w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=webp&fit=scale&q=75&w=900&h=486 900w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=webp&fit=scale&q=75&w=1100&h=594 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <img width="1000" height="540"
                srcset="https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=jpg&fit=scale&q=75&w=300&h=162 300w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=jpg&fit=scale&q=75&w=500&h=270 500w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=jpg&fit=scale&q=75&w=700&h=378 700w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=jpg&fit=scale&q=75&w=900&h=486 900w, https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg?fm=jpg&fit=scale&q=75&w=1100&h=594 1100w"
                sizes="(max-width: 50em) 98vw, 700px"
                src="https://images.ctfassets.net/f20lfrunubsq/Gq6a32irjovMj6sWWP9Qy/c893e563e214281bf854c6a285c463ef/ww-light-dark.jpg"
                alt="Web Weekly redesign showing the light and dark mode in a split view"
                loading="lazy"
                onload="this.classList.add('kf-fade-in')">
            </picture>
          </a>
        </figure>
      </div>
    </p>
<div class="heading-wrapper">
      <a class="margin-left-s text-no-underline" aria-label="Link to heading" aria-describedby="%60prefers-color-scheme%60-doesn't-consider-the-%60color-scheme%60" href="#%60prefers-color-scheme%60-doesn't-consider-the-%60color-scheme%60">
        <svg class="icon" aria-hidden="true">
          <use xlink:href="/sprite.svg#icon-link"/>
        </svg>
      </a>
      <h2 id="%60prefers-color-scheme%60-doesn't-consider-the-%60color-scheme%60"><code>prefers-color-scheme</code> doesn't consider the <code>color-scheme</code></h2></div><p>The Web Weekly redesign is Tailwind-based and to implement a light and dark mode I added custom color variables to my <code>tailwind<wbr>.config<wbr>.ts</code> to avoid placing <code>dark:*</code> classes everywhere.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #C586C0">export</span><span style="color: #D4D4D4"> </span><span style="color: #C586C0">default</span><span style="color: #D4D4D4"> {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE"><data-lsp lsp='(property) theme: {&#10;    extend: {&#10;        colors: {&#10;            "ld-slate-50": string;&#10;            "ld-slate-100": string;&#10;            "ld-slate-100/75": string;&#10;        };&#10;    };&#10;}' >theme</data-lsp>:</span><span style="color: #D4D4D4"> {</span></div><div class='line'><span style="color: #D4D4D4">    </span><span style="color: #9CDCFE"><data-lsp lsp='(property) extend: {&#10;    colors: {&#10;        "ld-slate-50": string;&#10;        "ld-slate-100": string;&#10;        "ld-slate-100/75": string;&#10;    };&#10;}' >extend</data-lsp>:</span><span style="color: #D4D4D4"> {</span></div><div class='line'><span style="color: #D4D4D4">      </span><span style="color: #9CDCFE"><data-lsp lsp='(property) colors: {&#10;    "ld-slate-50": string;&#10;    "ld-slate-100": string;&#10;    "ld-slate-100/75": string;&#10;}' >colors</data-lsp>:</span><span style="color: #D4D4D4"> {</span></div><div class='line'><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// light / dark styles</span></div><div class='line'><span style="color: #D4D4D4">        </span><span style="color: #CE9178">"ld-slate-50"</span><span style="color: #9CDCFE">:</span></div><div class='line'><span style="color: #D4D4D4">          </span><span style="color: #CE9178">"light-dark(oklch(98.4% 0.003 247.858), oklch(12.9% 0.042 264.695))"</span><span style="color: #D4D4D4">,</span></div><div class='line'><span style="color: #D4D4D4">        </span><span style="color: #CE9178">"ld-slate-100"</span><span style="color: #9CDCFE">:</span></div><div class='line'><span style="color: #D4D4D4">          </span><span style="color: #CE9178">"light-dark(oklch(96.8% 0.007 247.896), oklch(20.8% 0.042 265.755))"</span><span style="color: #D4D4D4">,</span></div><div class='line'><span style="color: #D4D4D4">        </span><span style="color: #CE9178">"ld-slate-100/75"</span><span style="color: #9CDCFE">:</span></div><div class='line'><span style="color: #D4D4D4">          </span><span style="color: #CE9178">"light-dark(oklch(96.8% 0.007 247.896 / 0.75), oklch(20.8% 0.042 265.755 / 0.75))"</span><span style="color: #D4D4D4">,</span></div><div class='line'><span style="color: #D4D4D4">        </span><span style="color: #6A9955">// ...</span></div><div class='line'><span style="color: #D4D4D4">      },</span></div><div class='line'><span style="color: #D4D4D4">    },</span></div><div class='line'><span style="color: #D4D4D4">  },</span></div><div class='line'><span style="color: #D4D4D4">};</span></div></code></div></pre>
<p>This new color palette implements the standard Tailwind colors but uses <code>light-dark()</code> to automatically flip them. <code>ld-slate-100</code> would be <code>slate-100</code> in light mode and <code>slate-800</code> in dark mode. Not gonna lie, I felt very smart when coming up with this idea.</p>
<p>The <code>ld-*</code> colors worked okay-ish until I had to fine-tune my dark mode styles and mix my automatic <code>light-dark()</code> color with <code>dark:*</code> colors using <code>prefers-color-scheme</code>.</p>
<p>My plan was to implement a dark/light mode toggle that changes the general <code>color-scheme</code> value and the rest should &quot;just work&quot;, right? <code>light dark</code> would be the default, <code>light</code> should be set in light mode and <code>dark</code> in dark mode.</p>
<p>Turns out, this approach works great for <code>light-dark()</code> however <code>prefers-color-scheme</code> doesn't seem to care about <code>color-scheme</code> values and always relied on the OS settings.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p><code>light-dark()</code> always reflects the currently set <code>color-scheme</code>. For <code>color-scheme: light dark;</code> it falls back to the operating system settings, it returns the light variant for <code>color-scheme: light;</code> and the dark one for <code>color-scheme: dark;</code>. It does exactly what I expected.</p>
<p><code>prefers-color-scheme</code> isn't affected by the <code>color-scheme</code> value and always falls back to the operating system's color scheme. Isn't this surprising?</p>
<p>I don't know the exact reasoning here, as mentioned above, it surely must be some backwards compatibility thing. If you know more, <a href="mailto:stefanjudis@gmail.com">reach out and let me know</a>. I'd love to extend this post later.</p>
<p>So, whenever you read somewhere that <code>light-dark()</code> is just a shorter <code>prefers-color-scheme</code> query, be aware this isn't true! If you're dealing with <code>color-scheme</code>-based theming, this statement is fake news...</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20light-dark() isn&#39;t always the same as prefers-color-scheme">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        How to &quot;officially&quot; deprecate methods with Node.js utilities
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/deprecate-method-in-node-js/"/>
      <published>2025-11-14T23:00:00+00:00</published>
      <updated>2025-11-14T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/deprecate-method-in-node-js/
      </id>
      <category term="tilPost"></category>
        <category term="NodeJS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>If you maintain a Node.js library or framework that's consumed by people downstream, you know the pain of introducing breaking changes and deprecating existing functionality. People get mad very quickly and the only solution is to communicate very early that things might change or will disappear in a future version.</p>
<p>I've thought that the only way to communicate future deprecations is to mark methods as deprecated via TSDoc / JSDoc.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token comment">/**
 * Returns a string value.
 * @deprecated This function is deprecated and will be removed in a future version.
 * @returns {string} The string 'something'
 */</span>
<span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">getSomething</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">return</span> <span class="token string">'something'</span>
<span class="token punctuation">}</span>
</code></pre>
<p>In this case, if someone imports the deprecated <code>getSomething</code> method, the editor (in my case it's VS Code) will make it very clear that they should look into not using the method.</p>
<p>
      <div class="sqip-container margin-top-l margin-bottom-l">
        <figure class="sqip-image" style="
          --color1: rgb(108,161,235); --color2: rgb(80,112,172); --color3: rgb(183,122,209); --color4: rgb(188,189,196); --color5: rgb(44,53,76);
          --sqip-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTQ0Ij48cGF0aCBmaWxsPSIjMmMzNTRjIiBkPSJNMCAwaDMwMHYxNDRIMHoiLz48ZyBmaWxsLW9wYWNpdHk9Ii41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSguNiAuNikgc2NhbGUoMS4xNzE4OCkiPjxjaXJjbGUgY3g9IjEzOCIgY3k9IjczIiByPSIxNzMiIGZpbGw9IiMyOTI4MjEiLz48ZWxsaXBzZSBjeD0iODQiIGN5PSI2NCIgZmlsbD0iIzc4ODA4ZCIgcng9IjM5IiByeT0iMiIvPjxlbGxpcHNlIGN4PSIxNTIiIGN5PSIxMDQiIGZpbGw9IiM2NTZiNzYiIHJ4PSIxMDciIHJ5PSIyIi8+PGVsbGlwc2UgY3g9IjYwIiBjeT0iNyIgZmlsbD0iIzVkNDk1ZSIgcng9IjU3IiByeT0iNCIvPjxwYXRoIGZpbGw9IiM1NzgwYTMiIGQ9Ik0zNiAyMWg0OXY0SDM2eiIvPjxwYXRoIGZpbGw9IiM1MzViNmEiIGQ9Ik0xODYgMTVoNDB2MTFoLTQweiIvPjxlbGxpcHNlIGN4PSI5MyIgY3k9IjExNCIgZmlsbD0iIzY1NmM3NyIgcng9IjQ1IiByeT0iMiIvPjxlbGxpcHNlIGN4PSIyMTAiIGN5PSI0MiIgZmlsbD0iIzQ0NGQ1YiIgcng9IjE3MiIgcnk9IjMiLz48L2c+PC9zdmc+');
        ">
          <a href="//images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png">
            <picture>
              <source type="image/avif"
                srcset="//images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=avif&fit=scale&q=75&w=300&h=144 300w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=avif&fit=scale&q=75&w=500&h=241 500w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=avif&fit=scale&q=75&w=700&h=338 700w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=avif&fit=scale&q=75&w=900&h=434 900w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=avif&fit=scale&q=75&w=1100&h=531 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <source type="image/webp"
                srcset="//images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=webp&fit=scale&q=75&w=300&h=144 300w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=webp&fit=scale&q=75&w=500&h=241 500w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=webp&fit=scale&q=75&w=700&h=338 700w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=webp&fit=scale&q=75&w=900&h=434 900w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=webp&fit=scale&q=75&w=1100&h=531 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <img width="1000" height="483"
                srcset="//images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=jpg&fit=scale&q=75&w=300&h=144 300w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=jpg&fit=scale&q=75&w=500&h=241 500w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=jpg&fit=scale&q=75&w=700&h=338 700w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=jpg&fit=scale&q=75&w=900&h=434 900w, //images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png?fm=jpg&fit=scale&q=75&w=1100&h=531 1100w"
                sizes="(max-width: 50em) 98vw, 700px"
                src="//images.ctfassets.net/f20lfrunubsq/7fMzcyxzJZBWMMVMPun4oH/a73a353520485d9c5b7f212fc8ec1a28/Screenshot_2025-11-15_at_17.23.12.png"
                alt="VS Code interface showing a deprecation warning and strike-through text."
                loading="lazy"
                onload="this.classList.add('kf-fade-in')">
            </picture>
          </a>
        </figure>
      </div>
    </p>
<p>VS Code's crossed out text is very convincing if you ask me but I don't know if every editor displays deprecation warnings as prominently.</p>
<p>If you really want to make sure people see your Node.js deprecation notice, today, I learned that Node.js provides <a href="https://nodejs.org/docs/latest/api/util.html#utildeprecatefn-msg-code-options" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fnodejs.org%2Fdocs%2Flatest%2Fapi%2Futil.html%23utildeprecatefn-msg-code-options')">a native <code>deprecate</code> method</a>.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> deprecate <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node:util"</span><span class="token punctuation">;</span>

<span class="token comment">/**
 * Returns a string value.
 * @deprecated This function is deprecated and will be removed in a future version.
 * @returns {string} The string 'something'
 */</span>
<span class="token keyword">export</span> <span class="token keyword">const</span> getSomething <span class="token operator">=</span> <span class="token function">deprecate</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">return</span> <span class="token string">'something'</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token string">'watch out! something is deprecated!'</span><span class="token punctuation">)</span>
</code></pre>
<p>And if someone now uses deprecated methods, they will also get notified when they run the code via Node.js.</p>
<pre><code>$ node index.ts

something
(node:21682) DeprecationWarning: watch out! something is deprecated!
(Use `node --trace-deprecation ...` to show where the warning was created)
</code></pre>
<p>Sweet!</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20How to &quot;officially&quot; deprecate methods with Node.js utilities">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        pathLength makes makes SVG path animations easier to manage
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/pathlength-makes-makes-svg-path-animations-easier-to-manage/"/>
      <published>2025-11-02T23:00:00+00:00</published>
      <updated>2025-11-02T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/pathlength-makes-makes-svg-path-animations-easier-to-manage/
      </id>
      <category term="tilPost"></category>
        <category term="CSS"></category>
      
        <category term="SVG"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>You've probably seen this SVG effect that people use to animate signatures. If you have an SVG path you can combine <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FSVG%2FReference%2FAttribute%2Fstroke-dasharray')"><code>stroke-dasharray</code></a>, <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dashoffset" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FSVG%2FReference%2FAttribute%2Fstroke-dashoffset')"><code>stroke-dashoffset</code></a> and CSS animations/transitions to animate a given SVG path. Here's an example of the effect.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>The SVG includes a single SVG path that expands when you hover over the SVG. How does this work in detail?</p>
<p>As a first step, you can cut SVG paths into visual pieces with <code>stroke-dasharray</code>. It's important to know that the &quot;drawn pieces&quot; and in-between gaps will have the same length.</p>
<p>Then, after you've chopped your path into pieces, you can move these pieces around the SVG path with <code>stroke-dashoffset</code>.</p>
<p>So, the overall trick is to make the pieces as big as the entire path length to transition from showing a gap (which spans the entire path) to showing a drawn piece next to it.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>If you played with the demo above, you might have wondered where this magic number (<code>14302</code>) is coming from. <code>14302</code> is the overall path length, and you can access it with some JavaScript.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> path <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'path'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> pathLength <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">getTotalLength</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p>With the path length available, you can then write some quick CSS to achieve this effect.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">path</span> <span class="token punctuation">{</span>
  <span class="token comment">/* Cut the path into pieces being as long as the path itself */</span>
  <span class="token property">stroke-dasharray</span><span class="token punctuation">:</span> 14301<span class="token punctuation">;</span>
  <span class="token comment">/* Move the rendering to the gap */</span>
  <span class="token property">stroke-dashoffset</span><span class="token punctuation">:</span> 14301<span class="token punctuation">;</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> stroke-dashoffset 0.75s ease-in-out<span class="token punctuation">;</span>
  
  <span class="token selector">&amp;:hover</span> <span class="token punctuation">{</span>
    <span class="token comment">/* Transition the offset to show the rendered path piece */</span>
    <span class="token property">stroke-dashoffset</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p>And while this approach works just fine, it's a bit cumbersome to always figure out the path length. After reading Josh's wonderful article <a href="https://www.joshwcomeau.com/svg/friendly-introduction-to-svg/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.joshwcomeau.com%2Fsvg%2Ffriendly-introduction-to-svg%2F')">A Friendly Introduction to SVG</a>, I learned that you can set the path length manually in your SVG using <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/pathLength" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FSVG%2FReference%2FAttribute%2FpathLength')">the <code>pathLength</code> attribute</a>!</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>7149<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1392<span class="token punctuation">"</span></span> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 7149 1400<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>path</span> <span class="token attr-name">d</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>M0.640625 ...<span class="token punctuation">"</span></span> <span class="token attr-name">pathLength</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>100<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">></span></span>
</code></pre>
<p>And with this tiny adjustment, the CSS animation becomes way easier to handle because you can base your animations on the path length of <code>100</code>.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">path</span> <span class="token punctuation">{</span>
  <span class="token comment">/* Cut the path into pieces being as long as the path itself */</span>
  <span class="token property">stroke-dasharray</span><span class="token punctuation">:</span> 100<span class="token punctuation">;</span>
  <span class="token comment">/* Move the rendering to the gap */</span>
  <span class="token property">stroke-dashoffset</span><span class="token punctuation">:</span> 100<span class="token punctuation">;</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> stroke-dashoffset 0.75s ease-in-out<span class="token punctuation">;</span>
  
  <span class="token selector">&amp;:hover</span> <span class="token punctuation">{</span>
    <span class="token comment">/* Transition the offset to show the rendered path piece */</span>
    <span class="token property">stroke-dashoffset</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Isn't this way nicer? I'm a fan!</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20pathLength makes makes SVG path animations easier to manage">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        field-sizing isn&#39;t only about growing textareas
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/field-sizing-is-about-more-than-textareas/"/>
      <published>2025-10-20T06:00:00+00:00</published>
      <updated>2025-10-20T06:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/field-sizing-is-about-more-than-textareas/
      </id>
      <category term="tilPost"></category>
        <category term="CSS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>There's a new CSS property in town: <code>field-sizing</code>. <code>field-sizing: content</code> allows form controls to adjust in size to fit their contents. At the time of writing this property is still in flux in terms of baseline support.</p>
<div class="highlightBox mdn margin-top-xl margin-bottom-xl">
        <div class="cornerBubble">
          <svg aria-hidden="true"><use xlink:href="/sprite.svg#icon-mdn"></use></svg>
        </div>
        <div class="highlightBox__header">MDN Compat Data (<a href="https://raw.githubusercontent.com/mdn/browser-compat-data/refs/heads/main/css/properties/field-sizing.json">source</a>)</div>
        <div class="highlightBox__body">
          <div class="highlightBox__overflow">
            <table class="highlightBox__compat">
              <caption>Browser support info for <a href="https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/field-sizing">field-sizing</a> </caption>
              <thead>
                <tr>
                  <td>
                        <img src="/assets/browsers/chrome.webp"
                              srcset="/assets/browsers/chrome.webp, /assets/browsers/chrome@2.webp 2x"
                              width="48" height="51" alt="chrome">
                      </td><td>
                        <img src="/assets/browsers/chrome_android.webp"
                              srcset="/assets/browsers/chrome_android.webp, /assets/browsers/chrome_android@2.webp 2x"
                              width="48" height="51" alt="chrome_android">
                      </td><td>
                        <img src="/assets/browsers/edge.webp"
                              srcset="/assets/browsers/edge.webp, /assets/browsers/edge@2.webp 2x"
                              width="48" height="51" alt="edge">
                      </td><td>
                        <img src="/assets/browsers/firefox.webp"
                              srcset="/assets/browsers/firefox.webp, /assets/browsers/firefox@2.webp 2x"
                              width="48" height="51" alt="firefox">
                      </td><td>
                        <img src="/assets/browsers/firefox_android.webp"
                              srcset="/assets/browsers/firefox_android.webp, /assets/browsers/firefox_android@2.webp 2x"
                              width="48" height="51" alt="firefox_android">
                      </td><td>
                        <img src="/assets/browsers/safari.webp"
                              srcset="/assets/browsers/safari.webp, /assets/browsers/safari@2.webp 2x"
                              width="48" height="51" alt="safari">
                      </td><td>
                        <img src="/assets/browsers/safari_ios.webp"
                              srcset="/assets/browsers/safari_ios.webp, /assets/browsers/safari_ios@2.webp 2x"
                              width="48" height="51" alt="safari_ios">
                      </td><td>
                        <img src="/assets/browsers/samsunginternet_android.webp"
                              srcset="/assets/browsers/samsunginternet_android.webp, /assets/browsers/samsunginternet_android@2.webp 2x"
                              width="48" height="51" alt="samsunginternet_android">
                      </td><td>
                        <img src="/assets/browsers/webview_android.webp"
                              srcset="/assets/browsers/webview_android.webp, /assets/browsers/webview_android@2.webp 2x"
                              width="48" height="51" alt="webview_android">
                      </td>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td>
                        <span class="highlightBox__pill success margin-top-s">
                        123
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        123
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        123
                        </span>
                      </td><td>
                        <span class="highlightBox__pill failure margin-top-s">
                        Nö
                        </span>
                      </td><td>
                        <span class="highlightBox__pill failure margin-top-s">
                        Nein
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        26.2
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        26.2
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        27.0
                        </span>
                      </td><td>
                        <span class="highlightBox__pill success margin-top-s">
                        123
                        </span>
                      </td>
                </tr>
              </tbody>
            </table>
          </div>
<p></div>
</div></p>
<p>So far, I've thought the new property is only about expanding <code>textarea</code> elements, but apparently it works for other input elements, too.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>That's good stuff!</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20field-sizing isn&#39;t only about growing textareas">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        Header &amp; footer elements change their roles when they&#39;re inside of sectioning content
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/header-and-footer-elements-lose-their-roles-in-sectioning-content/"/>
      <published>2025-10-19T22:00:00+00:00</published>
      <updated>2025-10-19T22:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/header-and-footer-elements-lose-their-roles-in-sectioning-content/
      </id>
      <category term="tilPost"></category>
        <category term="HTML"></category>
      
        <category term="Accessibility"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>Using proper ARIA landmark roles allows assistive technologies to navigate a page. You probably know about some common ones: <code>main</code> (<code>main</code>), <code>header</code> (<code>banner</code>), <code>footer</code> (<code>contentinfo</code>). But do you know that <code>header</code> and <code>footer</code> don't always get assigned their initial ARIA roles?</p>
<p>Here's an example:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>header</span><span class="token punctuation">></span></span>
  "Root" header
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>header</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>main</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>header</span><span class="token punctuation">></span></span>
    "Main" header
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>header</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>footer</span><span class="token punctuation">></span></span>
    "Main" footer
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>footer</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>main</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>footer</span><span class="token punctuation">></span></span>
  "Root" footer
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>footer</span><span class="token punctuation">></span></span>
</code></pre>
<p>This HTML snippet includes <code>header</code> and <code>footer</code> elements that are a) on the root level and b) included in a <code>main</code> element. If you check the browser's accessibility tools, these elements get different roles assigned.</p>
<p>
      <div class="sqip-container margin-top-l margin-bottom-l">
        <figure class="sqip-image" style="
          --color1: rgb(251,182,22); --color2: rgb(177,149,86); --color3: rgb(251,219,62); --color4: rgb(205,218,226); --color5: rgb(40,60,70);
          --sqip-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTUyIj48cGF0aCBmaWxsPSIjMjgzYzQ2IiBkPSJNMCAwaDMwMHYxNTJIMHoiLz48ZyBmaWxsLW9wYWNpdHk9Ii41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSguNiAuNikgc2NhbGUoMS4xNzE4OCkiPjxjaXJjbGUgY3g9IjE1OCIgY3k9IjEwMSIgcj0iMTk2IiBmaWxsPSIjZmZmIi8+PGVsbGlwc2UgY3g9IjUxIiBjeT0iNjUiIHJ4PSIzNyIgcnk9IjU2Ii8+PGNpcmNsZSByPSIxIiBmaWxsPSIjZmZmIiB0cmFuc2Zvcm09Im1hdHJpeCgtOC4yMDQxNiAtMTY1LjI1NDk0IDExNS4yNTc5MyAtNS43MjIwNCAyMDEuNiA3NS40KSIvPjxjaXJjbGUgY3g9IjIwNCIgY3k9IjcxIiByPSIxMTgiIGZpbGw9IiNmZmYiLz48ZWxsaXBzZSBjeD0iMSIgY3k9IjU2IiBmaWxsPSIjZmZmIiByeD0iMTUiIHJ5PSIxMjkiLz48cGF0aCBkPSJNMTUgMTNoNzJ2MTA2SDE1eiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0wIDExN2gyNTZ2MTNIMHoiLz48ZWxsaXBzZSBjeD0iODciIGN5PSI0IiBmaWxsPSIjZmZmIiByeD0iMjU1IiByeT0iOCIvPjwvZz48L3N2Zz4=');
        ">
          <a href="//images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg">
            <picture>
              <source type="image/avif"
                srcset="//images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=avif&fit=scale&q=75&w=300&h=153 300w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=avif&fit=scale&q=75&w=500&h=255 500w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=avif&fit=scale&q=75&w=700&h=357 700w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=avif&fit=scale&q=75&w=900&h=459 900w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=avif&fit=scale&q=75&w=1100&h=561 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <source type="image/webp"
                srcset="//images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=webp&fit=scale&q=75&w=300&h=153 300w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=webp&fit=scale&q=75&w=500&h=255 500w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=webp&fit=scale&q=75&w=700&h=357 700w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=webp&fit=scale&q=75&w=900&h=459 900w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=webp&fit=scale&q=75&w=1100&h=561 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <img width="1000" height="510"
                srcset="//images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=jpg&fit=scale&q=75&w=300&h=153 300w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=jpg&fit=scale&q=75&w=500&h=255 500w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=jpg&fit=scale&q=75&w=700&h=357 700w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=jpg&fit=scale&q=75&w=900&h=459 900w, //images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg?fm=jpg&fit=scale&q=75&w=1100&h=561 1100w"
                sizes="(max-width: 50em) 98vw, 700px"
                src="//images.ctfassets.net/f20lfrunubsq/1H8CPfkGJnjgmB6JmL299m/248fa332ced66a9eeb2c164618875a14/sectionheader.jpg"
                alt="The `header` element gets the role &quot;banner&quot; on the root level but `sectionheader` or `generic` when being nested in a `main` element."
                loading="lazy"
                onload="this.classList.add('kf-fade-in')">
            </picture>
          </a>
        </figure>
      </div>
    </p>
<p>The <code>header</code> element loses its <code>banner</code> role and depending on the browser gets <code>generic</code> or <code>sectionheader</code> assigned, which has an effect on assistive technologies. Here's Martin in his post <a href="https://www.tempertemper.net/blog/page-headings-dont-belong-in-the-header" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.tempertemper.net%2Fblog%2Fpage-headings-dont-belong-in-the-header')">&quot;Page headings don't belong in the header&quot;</a>:</p>
<blockquote>
<p>The header would carry no semantic meaning, so screen reader users would be left wondering where the main page header landmark was.</p>
</blockquote>
<p>You can find similar behavior, though not identical, with the footer element.</p>
<p>
      <div class="sqip-container margin-top-l margin-bottom-l">
        <figure class="sqip-image" style="
          --color1: rgb(251,183,22); --color2: rgb(177,148,86); --color3: rgb(251,219,63); --color4: rgb(205,218,226); --color5: rgb(39,60,72);
          --sqip-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTUyIj48cGF0aCBmaWxsPSIjMjczYzQ4IiBkPSJNMCAwaDMwMHYxNTJIMHoiLz48ZyBmaWxsLW9wYWNpdHk9Ii41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSguNiAuNikgc2NhbGUoMS4xNzE4OCkiPjxjaXJjbGUgY3g9IjEyNSIgY3k9IjU3IiByPSIxNzMiIGZpbGw9IiNmZmYiLz48Y2lyY2xlIHI9IjEiIHRyYW5zZm9ybT0ibWF0cml4KC0zNy42NTMzOCAtLjQ4MDQzIC43MDEwOCAtNTQuOTQ1NzggNTEuNiA2My43KSIvPjxjaXJjbGUgcj0iMSIgZmlsbD0iI2ZmZiIgdHJhbnNmb3JtPSJtYXRyaXgoOTcuNjExOTIgLTEwLjU1OTIyIDE5LjUyMjI0IDE4MC40NjgyMSAxODUuNiA4Mi43KSIvPjxlbGxpcHNlIGN4PSIyMDMiIGN5PSIzMyIgZmlsbD0iI2ZmZiIgcng9IjExNyIgcnk9IjEyOSIvPjxlbGxpcHNlIGN4PSIzIiBjeT0iNjMiIGZpbGw9IiNmZmYiIHJ4PSIxMyIgcnk9IjEyOSIvPjxwYXRoIGQ9Ik0yMSAxM2g2NnYxMDRIMjF6Ii8+PGVsbGlwc2UgY3g9IjEyNyIgY3k9IjQiIGZpbGw9IiNmZmYiIHJ4PSIyNTUiIHJ5PSI4Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTAgMTE4aDI1NnYxMkgweiIvPjwvZz48L3N2Zz4=');
        ">
          <a href="//images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg">
            <picture>
              <source type="image/avif"
                srcset="//images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=avif&fit=scale&q=75&w=300&h=153 300w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=avif&fit=scale&q=75&w=500&h=255 500w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=avif&fit=scale&q=75&w=700&h=357 700w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=avif&fit=scale&q=75&w=900&h=459 900w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=avif&fit=scale&q=75&w=1100&h=561 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <source type="image/webp"
                srcset="//images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=webp&fit=scale&q=75&w=300&h=153 300w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=webp&fit=scale&q=75&w=500&h=255 500w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=webp&fit=scale&q=75&w=700&h=357 700w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=webp&fit=scale&q=75&w=900&h=459 900w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=webp&fit=scale&q=75&w=1100&h=561 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <img width="1000" height="510"
                srcset="//images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=jpg&fit=scale&q=75&w=300&h=153 300w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=jpg&fit=scale&q=75&w=500&h=255 500w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=jpg&fit=scale&q=75&w=700&h=357 700w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=jpg&fit=scale&q=75&w=900&h=459 900w, //images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg?fm=jpg&fit=scale&q=75&w=1100&h=561 1100w"
                sizes="(max-width: 50em) 98vw, 700px"
                src="//images.ctfassets.net/f20lfrunubsq/1aUEzJwlTSAnYAOwbrEj9X/da7f67bbd69ee5519a20014369887901/sectionfooter.jpg"
                alt="Footer elements on the root level get `contentinfo` assigned whereas `footer` elements in a `main` element get `sectionfooter`, &quot;No matching ARIA role&quot; or `generic`."
                loading="lazy"
                onload="this.classList.add('kf-fade-in')">
            </picture>
          </a>
        </figure>
      </div>
    </p>
<p>Footer elements receive varying roles from <code>sectionfooter</code> and <code>generic</code>.</p>
<p>If you look at the &quot;ARIA in HTML&quot; spec, you'll find some answers.</p>
<p><a href="https://w3c.github.io/html-aria/#el-header" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fw3c.github.io%2Fhtml-aria%2F%23el-header')">For the <code>header</code> element</a>:</p>
<blockquote>
<p>If not a descendant of an <code>article</code>, <code>aside</code>, <code>main</code>, <code>nav</code> or <code>section</code> element, or an element with <code>role=article</code>, <code>complementary</code>, <code>main</code>, <code>navigation</code> or <code>region</code> then <code>role=banner</code>. Otherwise, <code>role=generic</code>.</p>
</blockquote>
<p><a href="https://w3c.github.io/html-aria/#el-footer" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fw3c.github.io%2Fhtml-aria%2F%23el-footer')">For the <code>footer</code> element</a></p>
<blockquote>
<p>If not a descendant of an <code>article</code>, <code>aside</code>, <code>main</code>, <code>nav</code> or <code>section</code> element, or an element with <code>role=article</code>, <code>complementary</code>, <code>main</code>, <code>navigation</code> or <code>region</code> then <code>role=contentinfo</code>. Otherwise, <code>role=generic</code>.</p>
</blockquote>
<p>But what if you can't change the document structure or really want to have the <code>header</code> element in a sectioning <code>main</code>? If you really must, you can get out the hammer and add a landmark role yourself.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>main</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>header</span> <span class="token attr-name">role</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>banner<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
    "Main" header
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>header</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>main</span><span class="token punctuation">></span></span>
</code></pre>
<p>I'm not sure how I feel about this, though.</p>
<div class="highlightBox attention margin-top-xl margin-bottom-xl">
      
          <div class="cornerBubble">
            <svg aria-hidden="true">
              <use xlink:href="/sprite.svg#icon-attention"/>
            </svg>
          </div>
        

      
      <p>I've wondered why Chromium and Safari 26+ didn't follow the spec and didn't switch to the <code>generic</code> role. It turns out, lucky me published this post right in the middle of some work-in-progress ARIA spec changes:</p>
<ul>
<li><a href="https://github.com/w3c/aria/pull/1931" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fgithub.com%2Fw3c%2Faria%2Fpull%2F1931')">Introduction of <code>sectionheader</code> and <code>sectionfooter</code></a></li>
<li><a href="https://github.com/w3c/aria/pull/2543" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fgithub.com%2Fw3c%2Faria%2Fpull%2F2543')">ARIA mappings update</a></li>
<li><a href="https://github.com/w3c/html-aam/issues/585" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fgithub.com%2Fw3c%2Fhtml-aam%2Fissues%2F585')">ARIA mappings update</a>.</li>
</ul>
<p><code>sectionheader</code> and <code>sectionfooter</code> are new roles being worked on. Chromium and Safari started to ship the new semantics already. <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1893684" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fbugzilla.mozilla.org%2Fshow_bug.cgi%3Fid%3D1893684')">Firefox still has to implement the new aria mappings</a>.</p>
<p>Thanks to <a href="https://matuzo.at/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fmatuzo.at%2F')">Manuel</a> and <a href="https://lukewarlow.dev/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Flukewarlow.dev%2F')">Luke</a> pointing me in the right direction.</p>
</div><p>Where does this lead us? You probably shouldn't rely on or use the new <code>sectionheader</code> and <code>sectionfooter</code> roles yet. Here's <a href="https://bsky.app/profile/lukewarlow.dev/post/3m3me4mmhgc2f" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fbsky.app%2Fprofile%2Flukewarlow.dev%2Fpost%2F3m3me4mmhgc2f')">Luke from Igalia giving some advice</a>:</p>
<blockquote>
<p>Browser support needs to improve but AT also won't necessarily understand these roles yet.</p>
</blockquote>
<p>It will take some time until browsers have caught up and then still screen readers need to implement the new roles, too. This won't happen until tomorrow.</p>
<p>But generally, whenever you use <code>header</code> or <code>footer</code> elements, make sure to double check the applied ARIA landmark roles and avoid placing the elements in sectioning content like the <code>main</code> element. Otherwise, you might not provide the accessibility benefits you're hoping for.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20Header &amp; footer elements change their roles when they&#39;re inside of sectioning content">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        Node.js includes a native glob utility
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/node-js-includes-a-native-glob-utility/"/>
      <published>2025-10-06T22:00:00+00:00</published>
      <updated>2025-10-06T22:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/node-js-includes-a-native-glob-utility/
      </id>
      <category term="tilPost"></category>
        <category term="NodeJS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>The two most popular glob utilities (<a href="https://www.npmjs.com/package/minimatch" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fminimatch')">minimatch</a> and <a href="https://www.npmjs.com/package/glob" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fglob')">glob</a>) are responsible for over 500 million weekly npm downloads. Something so common should be included in the Node core library, right?</p>
<p>Today, I discovered that native &quot;globbing&quot; is included in Node core since v22.17.</p>
<p>The new utility comes in three flavors.</p>
<div class="heading-wrapper">
      <a class="margin-left-s text-no-underline" aria-label="Link to heading" aria-describedby="%60fspromises.glob()%60" href="#%60fspromises.glob()%60">
        <svg class="icon" aria-hidden="true">
          <use xlink:href="/sprite.svg#icon-link"/>
        </svg>
      </a>
      <h2 id="%60fspromises.glob()%60"><code>fsPromises<wbr>.glob()</code></h2></div><p>This <code>fsPromise<wbr>.glob</code> version returns and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FJavaScript%2FReference%2FGlobal_Objects%2FAsyncIterator')">an <code>AsyncIterator</code></a> which allows to end the globbing in a <code>for await/of</code> loop early if you have found what you're looking.</p>
<p>Alternatively, use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fromAsync" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FJavaScript%2FReference%2FGlobal_Objects%2FArray%2FfromAsync')"><code>Array<wbr>.fromAsync</code></a> if you don't care about breaking out early and want to discover all the files.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> glob <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'node:fs/promises'</span><span class="token punctuation">;</span>

<span class="token comment">// Iterate over discovered file paths</span>
<span class="token keyword">for</span> <span class="token keyword">await</span> <span class="token punctuation">(</span><span class="token keyword">const</span> entry <span class="token keyword">of</span> <span class="token function">glob</span><span class="token punctuation">(</span><span class="token string">'**/*.txt'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>entry<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment">// Await all files paths</span>
<span class="token keyword">const</span> files <span class="token operator">=</span> <span class="token keyword">await</span> Array<span class="token punctuation">.</span><span class="token function">fromAsync</span><span class="token punctuation">(</span><span class="token function">glob</span><span class="token punctuation">(</span><span class="token string">'**/*.txt'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>files<span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<div class="heading-wrapper">
      <a class="margin-left-s text-no-underline" aria-label="Link to heading" aria-describedby="%60fs.glob()%60" href="#%60fs.glob()%60">
        <svg class="icon" aria-hidden="true">
          <use xlink:href="/sprite.svg#icon-link"/>
        </svg>
      </a>
      <h2 id="%60fs.glob()%60"><code>fs<wbr>.glob()</code></h2></div><p><a href="https://nodejs.org/docs/latest-v22.x/api/fs.html#fsglobpattern-options-callback" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fnodejs.org%2Fdocs%2Flatest-v22.x%2Fapi%2Ffs.html%23fsglobpattern-options-callback')"><code>fs<wbr>.glob()</code></a> is callback-based if that's your jam.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> glob <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'node:fs'</span><span class="token punctuation">;</span>

<span class="token keyword">await</span> <span class="token function">glob</span><span class="token punctuation">(</span><span class="token string">'**/*.txt'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">error<span class="token punctuation">,</span> files</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>files<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<div class="heading-wrapper">
      <a class="margin-left-s text-no-underline" aria-label="Link to heading" aria-describedby="%60fs.globsync()%60" href="#%60fs.globsync()%60">
        <svg class="icon" aria-hidden="true">
          <use xlink:href="/sprite.svg#icon-link"/>
        </svg>
      </a>
      <h2 id="%60fs.globsync()%60"><code>fs<wbr>.globSync()</code></h2></div><p>And, of course, there's a synchronous version with <a href="https://nodejs.org/docs/latest-v22.x/api/fs.html#fsglobsyncpattern-options" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fnodejs.org%2Fdocs%2Flatest-v22.x%2Fapi%2Ffs.html%23fsglobsyncpattern-options')"><code>fs<wbr>.globSync()</code></a>, too.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> globSync <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'node:fs'</span><span class="token punctuation">;</span>

console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token function">globSync</span><span class="token punctuation">(</span><span class="token string">'**/*.txt'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<hr aria-hidden="true"><p>Now, I'm sure there are some minor differences to the popular libraries, but I bet it'll be fine for some quick file-grepping in JavaScript.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20Node.js includes a native glob utility">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        Keyframe animations have a special role in the CSS cascade
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/keyframe-animations-have-a-special-role-in-the-css-cascade/"/>
      <published>2025-10-04T22:00:00+00:00</published>
      <updated>2025-10-04T22:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/keyframe-animations-have-a-special-role-in-the-css-cascade/
      </id>
      <category term="tilPost"></category>
        <category term="CSS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>I've been reading Josh's post <a href="https://www.joshwcomeau.com/css/starting-style/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.joshwcomeau.com%2Fcss%2Fstarting-style%2F')">The Big Gotcha With <code>@starting-style</code></a> and learned a new thing about <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FCSS%2F%40keyframes')">CSS <code>@keyframes</code></a>.</p>
<blockquote>
<p>Instead of doing specificity math for each built-in style, the browser treats them <em>[the animation styles]</em> as an entirely separate collection of CSS. They get applied first, and any CSS we write, no matter its specificity, will overwrite it.</p>
</blockquote>
<p>Granted, I don't use keyframe animations very often, but what CSS collection is Josh talking about? The answer can be found on <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Cascade#cascading_order" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FCSS%2FCSS_cascade%2FCascade%23cascading_order')">the MDN docs explaining the CSS cascade</a>.</p>
<p>The cascading algorithm determines what properties to apply to an element in the following order:</p>
<ol>
<li>user-agent (browser) normal</li>
<li>user normal</li>
<li>author (developer) normal</li>
<li><strong>CSS keyframe animations</strong></li>
<li>author (developer) <code>!important</code></li>
<li>user <code>!important</code></li>
<li>user-agent (browser) <code>!important</code></li>
<li>CSS transitions</li>
</ol>
<p>And keyframe animations have a higher precedence than any other &quot;non-important&quot; style. This behavior makes it quite handy to apply entry animations.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>You can apply a keyframe animation that only defines <code>from</code> styles and can then automatically transition to the other applied properties. This keyframe transition works because keyframe styles are treated higher in the cascade algorithm. Well, today I learned!</p>
<p>If you apply <code>!important</code> styles, this doesn't work for the same cascade algorithm rules.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>Keyframe animations won't overrule <code>!important</code> styles and slapping more <code>!important</code> properties into your keyframes won't work either.</p>
<p>[Interactive component: visit the article to see it...]</p>
<p>Either way, Josh argues that for the mentioned reasons, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FCSS%2F%40starting-style')"><code>@starting-style</code></a> might be a mistake and the more I think of it, I think he's right.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20Keyframe animations have a special role in the CSS cascade">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        Node.js supports import maps
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/node-js-import-maps/"/>
      <published>2025-08-06T22:00:00+00:00</published>
      <updated>2025-08-06T22:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/node-js-import-maps/
      </id>
      <category term="tilPost"></category>
        <category term="NodeJS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>In growing JavaScript projects it is very common to end up with very long import paths navigating up and down the file tree (<code>.<wbr>./<wbr>.<wbr>./<wbr>.<wbr>./src/lib/whatever/index<wbr>.js</code>). This isn't only terrible to read but can also be hard to refactor if you move files around.</p>
<p>To make things a bit easier, bundlers or the TypeScript compiler allow you to define path aliases (<code>@lib/whatever/index</code>), but I usually try to avoid bundling my Node.js applications to keep the complexity low. Today I learned that Node.js allows you to set path aliases with <a href="https://nodejs.org/api/packages.html#imports" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fnodejs.org%2Fapi%2Fpackages.html%23imports')">import maps</a>.</p>
<p>Let's say you have this Node.js code using ESM.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> getAuthenticatedUser <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"./src/lib/auth/user.js"</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> database <span class="token keyword">from</span> <span class="token string">"./src/lib/db/index.js"</span><span class="token punctuation">;</span>

<span class="token comment">// more stuff...</span>
</code></pre>
<p>While this isn't the worst, the file paths could be way cleaner. To set aliases, head into your <code>package<wbr>.json</code> and define an <code>imports</code> object.</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  <span class="token property">"imports"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"#auth/*"</span><span class="token operator">:</span> <span class="token string">"./src/lib/auth/*.js"</span><span class="token punctuation">,</span>
    <span class="token property">"#db"</span><span class="token operator">:</span> <span class="token string">"./src/lib/db/index.js"</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<p>Custom import paths need to start with a <code>#</code> and thanks to these two quick lines you can now shorten any import going into <code>./src/lib/auth</code> and also directly reference <code>./src/lib/db/index<wbr>.js</code> with three characters (<code>#db</code>).</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> getAuthenticatedUser <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"#auth/user"</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> database <span class="token keyword">from</span> <span class="token string">"#db"</span><span class="token punctuation">;</span>

<span class="token comment">// more stuff...</span>
</code></pre>
<p>Sweet! But does it work everywhere? Or is this a new Node.js thing?</p>
<p>Nope, this isn't new. Import maps are supported in Node.js since v14 (we're currently on v22 LTS), so your Node.js environment will probably support them already. Have fun shortening some paths!</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20Node.js supports import maps">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        Forbidden headers can&#39;t be set in &quot;fetch&quot; requests
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/forbidden-headers-cant-be-set-in-fetch-requests/"/>
      <published>2025-03-31T22:00:00+00:00</published>
      <updated>2025-03-31T22:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/forbidden-headers-cant-be-set-in-fetch-requests/
      </id>
      <category term="tilPost"></category>
        <category term="HTTP"></category>
      
        <category term="JavaScript"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>Here's Alex blogging <a href="https://macarthur.me/posts/forbidden-request-headers/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fmacarthur.me%2Fposts%2Fforbidden-request-headers%2F')">about forbidden request headers</a>. Forbidden what? Exactly!</p>
<p>It turns out that when using <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FAPI%2FFetch_API')">the <code>fetch</code> API</a> (or if you're old school, <code>XMLHttpRequest</code>) there's a set of headers that you can't specify or overwrite from within JavaScript.</p>
<div class="heading-wrapper">
      <a class="margin-left-s text-no-underline" aria-label="Link to heading" aria-describedby="what-are-%22forbidden-request-headers%22%3F" href="#what-are-%22forbidden-request-headers%22%3F">
        <svg class="icon" aria-hidden="true">
          <use xlink:href="/sprite.svg#icon-link"/>
        </svg>
      </a>
      <h2 id="what-are-%22forbidden-request-headers%22%3F">What are &quot;forbidden request headers&quot;?</h2></div><p><a href="https://fetch.spec.whatwg.org/#forbidden-request-header" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Ffetch.spec.whatwg.org%2F%23forbidden-request-header')">The spec defines three different types of forbidden request headers</a>.</p>
<p>First, all headers in this list are forbidden:</p>
<ul>
<li><code>Accept-Charset</code></li>
<li><code>Accept-Encoding</code></li>
<li><code>Access-Control-Request-Headers</code></li>
<li><code>Access-Control-Request-Method</code></li>
<li><code>Connection</code></li>
<li><code>Content-Length</code></li>
<li><code>Cookie</code></li>
<li><code>Cookie2</code> (This seems to be a deprecated spec that never went anywhere)</li>
<li><code>Date</code></li>
<li><code>DNT</code></li>
<li><code>Expect</code></li>
<li><code>Host</code></li>
<li><code>Keep-Alive</code></li>
<li><code>Origin</code></li>
<li><code>Referer</code></li>
<li><code>Set-Cookie</code></li>
<li><code>TE</code></li>
<li><code>Trailer</code></li>
<li><code>Transfer-Encoding</code></li>
<li><code>Upgrade</code></li>
<li><code>Via</code></li>
</ul>
<p>Second, headers starting with <code>proxy-</code> or <code>sec-</code> are also forbidden.</p>
<p>And third, if there's something going on with the parsed values of the headers <code>X-HTTP-Method</code>, <code>X-HTTP-Method-Override</code>, and <code>X-Method-Override</code> header they might be forbidden, too.</p>
<p>Apparently, you can overwrite or at least signal that the initial HTTP method should be a different one? I have so many questions, but I'll leave them for another time.</p>
<div class="heading-wrapper">
      <a class="margin-left-s text-no-underline" aria-label="Link to heading" aria-describedby="what-happens-when-you-try-to-set-a-forbidden-header%3F" href="#what-happens-when-you-try-to-set-a-forbidden-header%3F">
        <svg class="icon" aria-hidden="true">
          <use xlink:href="/sprite.svg#icon-link"/>
        </svg>
      </a>
      <h2 id="what-happens-when-you-try-to-set-a-forbidden-header%3F">What happens when you try to set a forbidden header?</h2></div><p>So, what happens if you try to set a forbidden header?</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'https://api.example.com/data'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token string-property property">'Content-Length'</span><span class="token operator">:</span> <span class="token string">'100'</span><span class="token punctuation">,</span>  <span class="token comment">// This will be ignored</span>
    <span class="token string-property property">'X-Custom-Header'</span><span class="token operator">:</span> <span class="token string">'This is fine'</span>  <span class="token comment">// This will be sent</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
</code></pre>
<p>Browsers will simply ignore them and maybe, if they're kind, log a warning.</p>
<p>This all makes sense because the spec states that there should be things the user agent remains in control of.</p>
<blockquote>
<p>These are forbidden so the user agent remains in full control over them.</p>
</blockquote>
<p>This behavior makes total sense to avoid nasty security loopholes. I can imagine that if JavaScript could overwrite every header it would open all kinds of security vulnerability doors.</p>
<p>If you want to dive deeper <a href="https://macarthur.me/posts/forbidden-request-headers/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fmacarthur.me%2Fposts%2Fforbidden-request-headers%2F')">check out Alex's post</a>, it's a nice one.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20Forbidden headers can&#39;t be set in &quot;fetch&quot; requests">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        The macOS inline dictionary has a shortcut
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/the-macos-inline-dictionary-has-a-shortcut/"/>
      <published>2025-03-13T23:00:00+00:00</published>
      <updated>2025-03-13T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/the-macos-inline-dictionary-has-a-shortcut/
      </id>
      <category term="tilPost"></category>
        <category term="macOS"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>I've been a Mac user for a very long time and I think I just learned my new favorite shortcut from <a href="https://saurabhs.org/macos-tips" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fsaurabhs.org%2Fmacos-tips')">this massive post about macOS shortcuts</a>.</p>
<p>Now, English isn't my first language and I still need to look up words daily. Usually, I use Google Translate or Raycast for this, but today I learned the macOS Dictionary app (which I barely use) provides inline help.</p>
<p>You can bring it up with <code>⌘⌃D</code> (<code>D</code> for dictionary).</p>
<p>And because this functionality comes from an official macOS app, the shortcut works in the browser, PDFs, Slack, etc.</p>
<p>
      <figure class="margin-top-l margin-bottom-l text-align-c text-style-italic">
        <video controls preload="metadata">
          <source src="//videos.ctfassets.net/f20lfrunubsq/3hdY6ulHZqW3kpjIY5mDAr/21ca6ab2bb32d2d75417805a3427c8a4/dictionary.mp4" type="video/mp4">
        </video>
        <figcaption>Example showing how to use the inline dictionary app via a shortcut.</figcaption>
      </figure>
    </p>
<p>Whoa! I'm amazed. 😱</p>
<p><em>Side note: someone pointed out that this inline help also shows up when doing a force click on the Mac trackpad, but I have most of the trackpad magic turned off because it drives me nuts.</em></p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20The macOS inline dictionary has a shortcut">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        parseInt parses anything starting with a number
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/parseint-parses-anything-starting-with-a-number/"/>
      <published>2025-03-06T23:00:00+00:00</published>
      <updated>2025-03-06T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/parseint-parses-anything-starting-with-a-number/
      </id>
      <category term="tilPost"></category>
        <category term="JavaScript"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>You probably know our good old friend <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FJavaScript%2FReference%2FGlobal_Objects%2FparseInt')"><code>parseInt</code></a> and its quirks.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token comment">// it ignores whitespace</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">"   123 "</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 123</span>

<span class="token comment">// it ignores leading zeros</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">"077"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 77</span>

<span class="token comment">// it truncates the decimal part</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">"1.9"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1</span>

<span class="token comment">// it parses numbers sometimes with a different base</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token number">015</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 13</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'0x14'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 20</span>
</code></pre>
<p>I've just read <a href="https://www.aleksandrhovhannisyan.com/blog/parseint-keycap-emoji/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.aleksandrhovhannisyan.com%2Fblog%2Fparseint-keycap-emoji%2F')">Aleksandr's post</a> and learned about a <code>parseInt</code> superpower.</p>
<p>Apparently, <code>parseInt</code> can parse Emoji numbers correctly!</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'1️⃣'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'2️⃣'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 2</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'3️⃣'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 3</span>
</code></pre>
<p>What? It doesn't make any sense that this works!</p>
<p>And, of course, <strong><code>parseInt</code> can't parse Emoji numbers</strong>, but the combination of how Emoji numbers are put together and a <code>parseInt</code> quirk makes it appear as if <code>parseInt</code> could parse Emojis.</p>
<p>I always thought <code>parseInt</code> would try to parse the entire passed-in value, and if it failed, it would return <code>NaN</code>. And this is mostly correct...</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'wat'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>     <span class="token comment">// NaN</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'wat'</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// NaN</span>
</code></pre>
<p>... unless the value you want to parse starts with a numeric value like the number Emojis (<code>1️⃣</code> = <code>1</code> + more code points).</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'1w'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>     <span class="token comment">// 1</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'1w'</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1</span>

<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'1.5w'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>     <span class="token comment">// 1</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'1.5w'</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1</span>
</code></pre>
<p>If your value starts with an unparsable character, you'll be back at <code>NaN</code>.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'a1w'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>     <span class="token comment">// NaN</span>
<span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'a1w'</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// NaN</span>
</code></pre>
<p>I must say I find this wild and confusing, but I guess this behavior was put in there to parse numbers with units more easily.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token function">parseInt</span><span class="token punctuation">(</span><span class="token string">'12€'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 12</span>
</code></pre>
<p>However, JavaScript never holds back with a twist. <a href="https://mastodon.social/@kleinfreund/114120835960538875" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fmastodon.social%2F%40kleinfreund%2F114120835960538875')">Philipp pointed out that the <code>Number()</code> constructor behaves differently</a>.</p>
<pre class="language-javascript"><code class="language-javascript"><span class="token function">Number</span><span class="token punctuation">(</span><span class="token string">'1️⃣'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token comment">// NaN</span>
<span class="token function">Number</span><span class="token punctuation">(</span><span class="token string">'1w'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token comment">// NaN</span>
<span class="token function">Number</span><span class="token punctuation">(</span><span class="token string">'1.5w'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// NaN</span>
<span class="token function">Number</span><span class="token punctuation">(</span><span class="token string">'a1w'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// NaN</span>
<span class="token function">Number</span><span class="token punctuation">(</span><span class="token string">'12€'</span><span class="token punctuation">)</span>   <span class="token comment">// NaN</span>

<span class="token function">Number</span><span class="token punctuation">(</span><span class="token string">'1.2'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1.2</span>
</code></pre>
<p>Well, it's good to know about all these quirks!</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20parseInt parses anything starting with a number">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        TypeScript &quot;strictFunctionTypes&quot; is ignored for method syntax
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/typescript-ignored-strict-function-types/"/>
      <published>2025-02-28T23:00:00+00:00</published>
      <updated>2025-02-28T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/typescript-ignored-strict-function-types/
      </id>
      <category term="tilPost"></category>
        <category term="TypeScript"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>Let's say you have this TypeScript type.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type Example = {&#10;    method(x: string | number): void;&#10;    fn: (x: string | number) => void;&#10;}' >Example</data-lsp></span><span style="color: #D4D4D4"> = {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #6A9955">// Method shorthand syntax (should be avoided)</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #DCDCAA"><data-lsp lsp='(method) method(x: string | number): void' >method</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) x: string | number' >x</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0">string</span><span style="color: #D4D4D4"> | </span><span style="color: #4EC9B0">number</span><span style="color: #D4D4D4">): </span><span style="color: #4EC9B0">void</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #6A9955">// Function (or object property) syntax</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #DCDCAA"><data-lsp lsp='(property) fn: (x: string | number) => void' >fn</data-lsp></span><span style="color: #D4D4D4">: (</span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) x: string | number' >x</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0">string</span><span style="color: #D4D4D4"> | </span><span style="color: #4EC9B0">number</span><span style="color: #D4D4D4">) </span><span style="color: #569CD6">=&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">void</span><span style="color: #D4D4D4">;</span></div><div class='line'><span style="color: #D4D4D4">};</span></div></code></div></pre>
<p>You might expect the function types <code>method</code> and <code>fn</code> to behave exactly the same, right? The only difference is that one is defined in method shorthand, whereas the other is defined in function syntax. Both accept an <code>x</code> param that could either be a <code>string</code> or a <code>number</code></p>
<p>But now check this out.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA"><data-lsp lsp='function fn(x: string): void' >fn</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) x: string' >x</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0">string</span><span style="color: #D4D4D4">) { </span><span style="color: #6A9955">/* ... */</span><span style="color: #D4D4D4"> }</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #569CD6">const</span><span style="color: #D4D4D4"> </span><span style="color: #4FC1FF"><data-lsp lsp='const example: Example' >example</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0"><data-lsp lsp='type Example = {&#10;    method(x: string | number): void;&#10;    fn: (x: string | number) => void;&#10;}' >Example</data-lsp></span><span style="color: #D4D4D4"> = {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #6A9955">// this is "fine" (but shouldn&apos;t be)...</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE"><data-lsp lsp='(method) method(x: string | number): void' >method</data-lsp>:</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE"><data-lsp lsp='function fn(x: string): void' >fn</data-lsp></span><span style="color: #D4D4D4">,</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #6A9955">// this is erroring correctly...</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE"><data-err><data-lsp lsp='(property) fn: (x: string | number) => void' >fn</data-lsp></data-err>:</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE"><data-lsp lsp='function fn(x: string): void' >fn</data-lsp></span><span style="color: #D4D4D4">,</span></div><span class="error"><span>Type '(x: string) =&gt; void' is not assignable to type '(x: string | number) =&gt; void'.
  Types of parameters 'x' and 'x' are incompatible.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.</span><span class="code">2322</span></span><span class="error-behind">Type '(x: string) =&gt; void' is not assignable to type '(x: string | number) =&gt; void'.
  Types of parameters 'x' and 'x' are incompatible.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.</span><div class='line'><span style="color: #D4D4D4">};</span></div></code></div></pre>
<p>If you create an <code>example</code> object whose properties don't match the <code>Example</code> type, only the function syntax will detect the type mismatch. What's going on?</p>
<p>Apparently, there's a <a href="https://www.typescriptlang.org/tsconfig/#strictFunctionTypes" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.typescriptlang.org%2Ftsconfig%2F%23strictFunctionTypes')"><code>strictFunctionTypes</code> TypeScript config option</a> (part of <a href="https://www.typescriptlang.org/tsconfig/#strict" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.typescriptlang.org%2Ftsconfig%2F%23strict')">TypeScript's <code>strict</code> config</a>) that only works for the function syntax for legacy reasons.</p>
<blockquote>
<p>During development of this feature, we discovered a large number of inherently unsafe class hierarchies, including some in the DOM. Because of this, the setting only applies to functions written in function syntax, not to those in method syntax:</p>
</blockquote>
<p>The TypeScript devs purposefully turned off a type check feature for legacy reasons. Wild!</p>
<p>It's good to avoid the method syntax altogether and if you use TypeScript ESLint, there's <a href="https://typescript-eslint.io/rules/method-signature-style/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Ftypescript-eslint.io%2Frules%2Fmethod-signature-style%2F')">a rule for it</a>.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20TypeScript &quot;strictFunctionTypes&quot; is ignored for method syntax">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        Rerun commands with the built-in &quot;r&quot; command
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/rerun-commands-with-the-r-command/"/>
      <published>2025-02-20T23:00:00+00:00</published>
      <updated>2025-02-20T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/rerun-commands-with-the-r-command/
      </id>
      <category term="tilPost"></category>
        <category term="Bash"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>I just learned that executing <code>r</code> in my terminal, reruns the previous command. #wat</p>
<p><code>r</code> is a built-in shell command in my zsh setup.</p>
<pre class="language-sh"><code class="language-sh">$ <span class="token function">which</span> r
r: shell built-in <span class="token builtin class-name">command</span>
</code></pre>
<p>If you check all the built-in zsh commands with <code>man zshbuiltins</code>, you'll receive the unhelpful information that <code>r</code> command is the same as <code>fc -e -</code>.</p>
<pre><code>r — Same as fc -e -
</code></pre>
<p>Alright, I'll bite, what's <code>fc</code>?</p>
<p><code>fc</code> is included in the zsh built-ins, too. <strong>The <code>fc</code> command lets you display, edit or execute commands from the shell history</strong>, but unfortunately, the zsh man pages fail to answer what <code>fc</code> actually stands for.</p>
<p>Some folks say <code>fc</code> stands for &quot;<strong>f</strong>ix <strong>c</strong>ommand&quot; whereas others advocate for &quot;<strong>f</strong>ind <strong>c</strong>ommand&quot;. Both definitions make sense, but I didn't see it written in the zsh / bash man pages.</p>
<p><code>fc</code> is handy when juggling long and complicated shell magic that requires editing and rerunning.</p>
<p>
      <figure class="margin-top-l margin-bottom-l text-align-c text-style-italic">
        <video controls preload="metadata">
          <source src="//videos.ctfassets.net/f20lfrunubsq/7E9qiCn6H1gMQM0ECzPdGJ/1b4c6568808789c27c6519c4fe91a713/fc.mp4" type="video/mp4">
        </video>
        <figcaption>Terminal session showing how to edit and rerun a command with &quot;fc&quot;.</figcaption>
      </figure>
    </p>
<div class="highlightBox info margin-top-xl margin-bottom-xl">
      
          <div class="cornerBubble">
            <svg aria-hidden="true">
              <use xlink:href="/sprite.svg#icon-info"/>
            </svg>
          </div>
        

      
      <p><code>fc</code> isn't the only way to tackle and edit long history commands. <a href="/today-i-learned/edit-long-shell-commands-in-your-usdeditor/">Modern shells also supports short cuts offering this command edit functionality</a>. But I must say, they didn't stick and <code>fc</code> seems more likely to enter my muscle memory. We'll see...</p>
</div><p>But what's up with this <code>r</code> command standing for <code>fc -e -</code>?</p>
<p>The quick answer: <code>r</code> uses <code>fc</code> to rerun the previously executed command without the option to change it.</p>
<pre class="language-sh"><code class="language-sh">$ <span class="token builtin class-name">echo</span> <span class="token string">"hello world"</span>
hello world

$ r
<span class="token builtin class-name">echo</span> <span class="token string">"hello world"</span>
hello world
</code></pre>
<p>You can also narrow and search the command to run by calling <code>r</code> with additional arguments. For example, <code>r ls</code> will rerun the previously run <code>ls</code> command.</p>
<pre class="language-sh"><code class="language-sh">$ <span class="token function">ls</span> <span class="token parameter variable">-lah</span>
<span class="token punctuation">..</span>.

$ <span class="token comment"># some more commands</span>
$ <span class="token comment"># some more commands</span>
$ <span class="token comment"># some more commands</span>

$ r <span class="token function">ls</span>
<span class="token function">ls</span> <span class="token parameter variable">-lah</span>
<span class="token punctuation">..</span>.
</code></pre>
<p>But where is this execution behavior documented? Honestly, I've no clue...</p>
<p>According to the <code>fc</code> documentation, the <code>-e</code> flag allows you to define the used editor before rerunning the command, but <code>-e</code> seems to unlock some superpowers when paired with <code>-</code>. Unfortunately, I couldn't find this behavior described anywhere.</p>
<p>However, <code>r</code> is a very convenient way to rerun a command without reaching for the arrow keys!</p>
<p>If you have more insights, <a href="mailto:stefanjudis@gmail.com">please let me know</a>.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20Rerun commands with the built-in &quot;r&quot; command">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        How to iterate over TypeScript union types
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/iterate-typescript-union-type/"/>
      <published>2025-02-16T23:00:00+00:00</published>
      <updated>2025-02-16T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/iterate-typescript-union-type/
      </id>
      <category term="tilPost"></category>
        <category term="TypeScript"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>Is it just me, or are TypeScript conditional types and the <code>extends</code> keyword kinda scary?</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #6A9955">// What is going on? 😅</span></div><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type Something&lt;T> = T extends number ? never : T' >Something</data-lsp></span><span style="color: #D4D4D4">&lt;</span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Something&lt;T>' >T</data-lsp></span><span style="color: #D4D4D4">&gt; = </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Something&lt;T>' >T</data-lsp></span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">number</span><span style="color: #D4D4D4"> ? </span><span style="color: #4EC9B0">never</span><span style="color: #D4D4D4"> : </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Something&lt;T>' >T</data-lsp></span><span style="color: #D4D4D4">;</span></div></code></div></pre>
<p>I've just read Dr. Axel's <a href="https://2ality.com/2025/02/conditional-types-typescript.html" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2F2ality.com%2F2025%2F02%2Fconditional-types-typescript.html')">&quot;Conditional types in TypeScript&quot;</a> and some things finally clicked for me.</p>
<p>Let's say we have a union type containing multiple strings.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type AllColors = "Black" | "White" | "Orange" | "Red" | "Blue" | "Yellow" | "Gray"' >AllColors</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">"Black"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"White"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Orange"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Red"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Blue"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Yellow"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Gray"</span><span style="color: #D4D4D4">;</span></div></code></div></pre>
<p>Some people don't consider black, white and gray as colors. How could you now iterate over the union type and get these &quot;noncolors&quot; out of the <code>AllColors</code> type?</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #6A9955">// Iterate of the types included in `AllColors`.</span></div><div class='line'><span style="color: #6A9955">//</span></div><div class='line'><span style="color: #6A9955">// If type `T` does not extend `R` apply the `never` type.</span></div><div class='line'><span style="color: #6A9955">// `never` will remove `T` from the resulting union type.</span></div><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type Remove&lt;T, R> = T extends R ? never : T' >Remove</data-lsp></span><span style="color: #D4D4D4">&lt;</span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Remove&lt;T, R>' >T</data-lsp></span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) R in type Remove&lt;T, R>' >R</data-lsp></span><span style="color: #D4D4D4">&gt; = </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Remove&lt;T, R>' >T</data-lsp></span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) R in type Remove&lt;T, R>' >R</data-lsp></span><span style="color: #D4D4D4"> ? </span><span style="color: #4EC9B0">never</span><span style="color: #D4D4D4"> : </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Remove&lt;T, R>' >T</data-lsp></span><span style="color: #D4D4D4">;</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// Remove "Black" and "White" from "AllColors" union type.</span></div><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type RealColors = "Orange" | "Red" | "Blue" | "Yellow"' style='border-bottom: solid 2px lightgrey;'>RealColors</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0"><data-lsp lsp='type Remove&lt;T, R> = T extends R ? never : T' >Remove</data-lsp></span><span style="color: #D4D4D4">&lt;</span><span style="color: #4EC9B0"><data-lsp lsp='type AllColors = "Black" | "White" | "Orange" | "Red" | "Blue" | "Yellow" | "Gray"' >AllColors</data-lsp></span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">"Black"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"White"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Gray"</span><span style="color: #D4D4D4">&gt;;</span></div><div class='meta-line'><span class='popover-prefix'>         </span><span class='popover'><div class='arrow'></div>type RealColors = "Orange" | "Red" | "Blue" | "Yellow"</span></div></code></div></pre>
<p>Using conditional types, you can iterate over and filter union types. If you return <code>never</code>, it will be excluded from the resulting union type.</p>
<div class="highlightBox info margin-top-xl margin-bottom-xl">
      
          <div class="cornerBubble">
            <svg aria-hidden="true">
              <use xlink:href="/sprite.svg#icon-info"/>
            </svg>
          </div>
        

      
      <p>TypeScript includes the built-in utility types <a href="https://www.typescriptlang.org/docs/handbook/utility-types.html#excludeuniontype-excludedmembers" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.typescriptlang.org%2Fdocs%2Fhandbook%2Futility-types.html%23excludeuniontype-excludedmembers')"><code>exclude</code></a> and <a href="https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.typescriptlang.org%2Fdocs%2Fhandbook%2Futility-types.html%23extracttype-union')"><code>extract</code></a> for these use cases, and I only picked this example to explain the concept.</p>
</div><p>That's pretty cool, but you can use conditional types also to iterate over a union type and map the resulting types.</p>
<p>In this example, the strings are prefixed with <code>String: </code>.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type Random = "Joo" | "Foo" | 123 | 234' >Random</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">"Joo"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Foo"</span><span style="color: #D4D4D4"> | </span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4"> | </span><span style="color: #B5CEA8">234</span><span style="color: #D4D4D4">;</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// Iterate of the types included in `Random`.</span></div><div class='line'><span style="color: #6A9955">//</span></div><div class='line'><span style="color: #6A9955">// If type `T` is of type `string` prefix it.</span></div><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type Prefix&lt;T> = T extends string ? `String: ${T}` : T' >Prefix</data-lsp></span><span style="color: #D4D4D4">&lt;</span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Prefix&lt;T>' >T</data-lsp></span><span style="color: #D4D4D4">&gt; = </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Prefix&lt;T>' >T</data-lsp></span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">string</span><span style="color: #D4D4D4"> ? </span><span style="color: #CE9178">`String: </span><span style="color: #569CD6">${</span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Prefix&lt;T>' >T</data-lsp></span><span style="color: #569CD6">}</span><span style="color: #CE9178">`</span><span style="color: #D4D4D4"> : </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Prefix&lt;T>' >T</data-lsp></span><span style="color: #D4D4D4">;</span></div><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type MappedTypes = 123 | 234 | "String: Joo" | "String: Foo"' style='border-bottom: solid 2px lightgrey;'>MappedTypes</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0"><data-lsp lsp='type Prefix&lt;T> = T extends string ? `String: ${T}` : T' >Prefix</data-lsp></span><span style="color: #D4D4D4">&lt;</span><span style="color: #4EC9B0"><data-lsp lsp='type Random = "Joo" | "Foo" | 123 | 234' >Random</data-lsp></span><span style="color: #D4D4D4">&gt;;</span></div><div class='meta-line'><span class='popover-prefix'>         </span><span class='popover'><div class='arrow'></div>type MappedTypes = 123 | 234 | "String: Joo" | "String: Foo"</span></div></code></div></pre>
<p>Or, if we take the color example, we could also iterate and append <code>(noColor)</code> to <code>Black</code>, <code>White</code> and <code>Gray</code>.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type AllColors = "Black" | "White" | "Orange" | "Red" | "Blue" | "Yellow" | "Gray"' >AllColors</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">"Black"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"White"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Orange"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Red"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Blue"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Yellow"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Gray"</span><span style="color: #D4D4D4">;</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// Iterate of the types included in `AllColors`.</span></div><div class='line'><span style="color: #6A9955">//</span></div><div class='line'><span style="color: #6A9955">// If type `T` is of type `string` and `T` is of type `R`</span></div><div class='line'><span style="color: #6A9955">// append parentheses to the string type.</span></div><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type Suffix&lt;T, R> = T extends string ? T extends R ? `${T} (no color)` : T : T' >Suffix</data-lsp></span><span style="color: #D4D4D4">&lt;</span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Suffix&lt;T, R>' >T</data-lsp></span><span style="color: #D4D4D4">, </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) R in type Suffix&lt;T, R>' >R</data-lsp></span><span style="color: #D4D4D4">&gt; = </span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Suffix&lt;T, R>' >T</data-lsp></span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">string</span><span style="color: #D4D4D4"> </span></div><div class='line'><span style="color: #D4D4D4">    ? </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Suffix&lt;T, R>' >T</data-lsp></span><span style="color: #D4D4D4"> </span><span style="color: #569CD6">extends</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) R in type Suffix&lt;T, R>' >R</data-lsp></span><span style="color: #D4D4D4"> ? </span><span style="color: #CE9178">`</span><span style="color: #569CD6">${</span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Suffix&lt;T, R>' >T</data-lsp></span><span style="color: #569CD6">}</span><span style="color: #CE9178"> (no color)`</span><span style="color: #D4D4D4"> : </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Suffix&lt;T, R>' >T</data-lsp></span></div><div class='line'><span style="color: #D4D4D4">    : </span><span style="color: #4EC9B0"><data-lsp lsp='(type parameter) T in type Suffix&lt;T, R>' >T</data-lsp></span><span style="color: #D4D4D4">;</span></div><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type MappedColors = "Orange" | "Red" | "Blue" | "Yellow" | "Black (no color)" | "White (no color)" | "Gray (no color)"' style='border-bottom: solid 2px lightgrey;'>MappedColors</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #4EC9B0"><data-lsp lsp='type Suffix&lt;T, R> = T extends string ? T extends R ? `${T} (no color)` : T : T' >Suffix</data-lsp></span><span style="color: #D4D4D4">&lt;</span><span style="color: #4EC9B0"><data-lsp lsp='type AllColors = "Black" | "White" | "Orange" | "Red" | "Blue" | "Yellow" | "Gray"' >AllColors</data-lsp></span><span style="color: #D4D4D4">, </span><span style="color: #CE9178">"Black"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"White"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Gray"</span><span style="color: #D4D4D4">&gt;;</span></div><div class='meta-line'><span class='popover-prefix'>          </span><span class='popover'><div class='arrow'></div>type MappedColors = "Orange" | "Red" | "Blue" | "Yellow" | "Black (no color)" | "White (no color)" | "Gray (no color)"</span></div></code></div></pre>
<p>Granted, the nested ternary isn't pretty, but it seems to be the only way to include two conditions to TypeScript's conditional types.</p>
<p>And as a last trick: if your union type only includes strings, you can spare all this <code>extends</code> dance and use <a href="https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fwww.typescriptlang.org%2Fdocs%2Fhandbook%2F2%2Ftemplate-literal-types.html')">template literal types</a>.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type AllColors = "Black" | "White" | "Orange" | "Red" | "Blue" | "Yellow" | "Gray"' >AllColors</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">"Black"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"White"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Orange"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Red"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Blue"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Yellow"</span><span style="color: #D4D4D4"> | </span><span style="color: #CE9178">"Gray"</span><span style="color: #D4D4D4">;</span></div><div class='line'><span style="color: #569CD6">type</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0"><data-lsp lsp='type AllPrefixedColors = "Color: Black" | "Color: White" | "Color: Orange" | "Color: Red" | "Color: Blue" | "Color: Yellow" | "Color: Gray"' style='border-bottom: solid 2px lightgrey;'>AllPrefixedColors</data-lsp></span><span style="color: #D4D4D4"> = </span><span style="color: #CE9178">`Color: </span><span style="color: #569CD6">${</span><span style="color: #4EC9B0"><data-lsp lsp='type AllColors = "Black" | "White" | "Orange" | "Red" | "Blue" | "Yellow" | "Gray"' >AllColors</data-lsp></span><span style="color: #569CD6">}</span><span style="color: #CE9178">`</span><span style="color: #D4D4D4">;</span></div><div class='meta-line'><span class='popover-prefix'>            </span><span class='popover'><div class='arrow'></div>type AllPrefixedColors = "Color: Black" | "Color: White" | "Color: Orange" | "Color: Red" | "Color: Blue" | "Color: Yellow" | "Color: Gray"</span></div></code></div></pre>
<p>Good stuff!</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20How to iterate over TypeScript union types">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        How to apply a global Git commit template
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/global-git-commit-templates/"/>
      <published>2025-02-07T23:00:00+00:00</published>
      <updated>2025-02-07T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/global-git-commit-templates/
      </id>
      <category term="tilPost"></category>
        <category term="git"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>When you commit things in Git via the CLI, you'll be greeted with something that looks like this:</p>
<pre><code># Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# Changes to be committed:
#       new file:   ...
</code></pre>
<p>You'll write your commit message on the first line, hit save, close the file, and go on with your nerdy day.</p>
<p>The lines starting with a <code>#</code> will be ignored and not included in your commit data. Most lines in this boilerplate message start with a <code>#</code> and should enable you to make a good commit. It's good to know which files will be included and double-check that you're on the correct branch, right?</p>
<p>But what makes a good commit?</p>
<p>Creating good commits can be complicated because every project/team has its own rules. Some bet on <a href="https://gitmoji.dev/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Fgitmoji.dev%2F')">Gitmoji</a>. Some want you to include a ticket number. Others might prefer lengthy explanations of the changes to generate an automated changelog.</p>
<p>Today I learned that <strong>Git supports commit message templates to make your life easier if you're facing specific commit requirements</strong>.</p>
<p>Run this command...</p>
<pre class="language-sh"><code class="language-sh"><span class="token function">git</span> config <span class="token parameter variable">--global</span> commit.template ~/.gitmessage
</code></pre>
<p>... to add this line to your general <code>.gitconfig</code> file...</p>
<pre><code>[commit]
  template = ~/.gitmessage
</code></pre>
<p>... and create a <code>~/<wbr>.gitmessage</code> file...</p>
<pre><code># Yo, waz up! Dude, remember to follow these conventions (👇) , okay?
#
# 1. do this!
# 2. do that!
# ...
</code></pre>
<p>... that contains your custom commit template.</p>
<p>Now, you'll be greeted with your custom message when you make a commit.</p>
<p>
      <div class="sqip-container margin-top-l margin-bottom-l">
        <figure class="sqip-image" style="
          --color1: rgb(91,209,248); --color2: rgb(91,162,119); --color3: rgb(130,248,170); --color4: rgb(187,187,187); --color5: rgb(98,52,92);
          --sqip-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTUzIj48cGF0aCBmaWxsPSIjNjIzNDVjIiBkPSJNMCAwaDMwMHYxNTNIMHoiLz48ZyBmaWxsLW9wYWNpdHk9Ii41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSguNiAuNikgc2NhbGUoMS4xNzE4OCkiPjxlbGxpcHNlIGN4PSIxMjQiIGN5PSI2IiBmaWxsPSIjZmZmIiByeD0iMjU1IiByeT0iOCIvPjxjaXJjbGUgY3g9IjE1MCIgY3k9IjEwNSIgcj0iOTMiLz48ZWxsaXBzZSBjeD0iMyIgY3k9IjcyIiBmaWxsPSIjZmZmIiByeD0iMTAiIHJ5PSIxMzAiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMjQzIDBoMTN2MTMxaC0xM3oiLz48cGF0aCBmaWxsPSIjMDAxODE1IiBkPSJtMTUgMTUgMjMzLTFMMTMgMTQ2eiIvPjxlbGxpcHNlIGN4PSIxNzAiIGN5PSI1IiBmaWxsPSIjZmZmIiByeD0iMjUzIiByeT0iOSIvPjxlbGxpcHNlIGN4PSIyNTAiIGN5PSI2NiIgZmlsbD0iI2ZmZiIgcng9IjciIHJ5PSIxMzAiLz48ZWxsaXBzZSBjeD0iNCIgY3k9IjY0IiBmaWxsPSIjZmZmIiByeD0iOCIgcnk9IjEzMCIvPjwvZz48L3N2Zz4=');
        ">
          <a href="//images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png">
            <picture>
              <source type="image/avif"
                srcset="//images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=avif&fit=scale&q=75&w=300&h=153 300w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=avif&fit=scale&q=75&w=500&h=256 500w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=avif&fit=scale&q=75&w=700&h=359 700w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=avif&fit=scale&q=75&w=900&h=461 900w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=avif&fit=scale&q=75&w=1100&h=564 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <source type="image/webp"
                srcset="//images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=webp&fit=scale&q=75&w=300&h=153 300w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=webp&fit=scale&q=75&w=500&h=256 500w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=webp&fit=scale&q=75&w=700&h=359 700w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=webp&fit=scale&q=75&w=900&h=461 900w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=webp&fit=scale&q=75&w=1100&h=564 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <img width="1000" height="513"
                srcset="//images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=jpg&fit=scale&q=75&w=300&h=153 300w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=jpg&fit=scale&q=75&w=500&h=256 500w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=jpg&fit=scale&q=75&w=700&h=359 700w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=jpg&fit=scale&q=75&w=900&h=461 900w, //images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png?fm=jpg&fit=scale&q=75&w=1100&h=564 1100w"
                sizes="(max-width: 50em) 98vw, 700px"
                src="//images.ctfassets.net/f20lfrunubsq/6CyFMxiKzCwjfUtWMxdq4G/32b13edbeeddc5f8cfbf409de2eb680b/Frame_579.png"
                alt="Git commit in the CLI including a custom Git commit message"
                loading="lazy"
                onload="this.classList.add('kf-fade-in')">
            </picture>
          </a>
        </figure>
      </div>
    </p>
<p>Pretty sweet!</p>
<p>Setting a global commit template isn't very user friendly. It'd be great to use <a href="/today-i-learned/how-to-apply-directory-dependent-git-configuration-using-conditional-imports/">Git's <code>includeIf</code></a> to set up project-dependent commit templates somehow, but I'll leave this for another day.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20How to apply a global Git commit template">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        The difference between @ts-ignore and @ts-expect-error
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/the-difference-ts-ignore-and-ts-expect-error/"/>
      <published>2025-02-05T23:00:00+00:00</published>
      <updated>2025-02-05T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/the-difference-ts-ignore-and-ts-expect-error/
      </id>
      <category term="tilPost"></category>
        <category term="TypeScript"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>How do you deal with TypeScript when (for whatever reason) types are mixed up, everything's full of squigglies, but you're sure you want to ignore this one particular TypeScript error?</p>
<p>Let's look at a very simplified example; let's say you have a method taking in a <code>string</code> argument.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA"><data-lsp lsp='function logify(msg: string): void' >logify</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) msg: string' >msg</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0">string</span><span style="color: #D4D4D4">) {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE"><data-lsp lsp='var console: Console' >console</data-lsp></span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA"><data-lsp lsp='(method) Console.log(...data: any[]): void' >log</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">`Msg: </span><span style="color: #569CD6">${</span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) msg: string' >msg</data-lsp></span><span style="color: #569CD6">}</span><span style="color: #CE9178">!`</span><span style="color: #D4D4D4">);</span></div><div class='line'><span style="color: #D4D4D4">}</span></div></code></div></pre>
<p>But you want to use the <code>logify</code> function with a <code>number</code> type. Of course, TypeScript won't be happy about you using <code>logify</code> with the wrong argument types. Still, you're 100% certain you want to call the function with a <code>number</code> type.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #DCDCAA"><data-lsp lsp='function logify(msg: string): void' >logify</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8"><data-err>123</data-err></span><span style="color: #D4D4D4">);</span></div><span class="error"><span>Argument of type 'number' is not assignable to parameter of type 'string'.</span><span class="code">2345</span></span><span class="error-behind">Argument of type 'number' is not assignable to parameter of type 'string'.</span></code></div></pre>
<p>What can you do?</p>
<p>You can bring in your friends <code>@ts-ignore</code> and <code>@ts-expect-error</code> to silence TypeScript.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #6A9955">// @ts-ignore</span></div><div class='line'><span style="color: #DCDCAA"><data-lsp lsp='function logify(msg: string): void' >logify</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">);</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// @ts-expect-error</span></div><div class='line'><span style="color: #DCDCAA"><data-lsp lsp='function logify(msg: string): void' >logify</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">);</span></div></code></div></pre>
<p>Both, <code>@ts-ignore</code> and <code>@ts-expect-error</code>, will make TypeScript ignore the type error happening on the following line. But which one should you use?</p>
<p>Generally, <strong>it's recommended to go for <code>@ts-expect-error</code></strong> because it will lead to a TypeScript error when the following line isn't erroring anymore.</p>
<p>For example, it could be that the <code>logify</code> types will be changed to also accept numbers. Then, <code>@ts-expect-error</code> will let you know that you can remove it.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #6A9955">// Let&apos;s allow number types... 👇</span></div><div class='line'><span style="color: #569CD6">function</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA"><data-lsp lsp='function logify(msg: string | number): void' >logify</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) msg: string | number' >msg</data-lsp></span><span style="color: #D4D4D4">: </span><span style="color: #4EC9B0">string</span><span style="color: #D4D4D4"> | </span><span style="color: #4EC9B0">number</span><span style="color: #D4D4D4">) {</span></div><div class='line'><span style="color: #D4D4D4">  </span><span style="color: #9CDCFE"><data-lsp lsp='var console: Console' >console</data-lsp></span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA"><data-lsp lsp='(method) Console.log(...data: any[]): void' >log</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">`Msg: </span><span style="color: #569CD6">${</span><span style="color: #9CDCFE"><data-lsp lsp='(parameter) msg: string | number' >msg</data-lsp></span><span style="color: #569CD6">}</span><span style="color: #CE9178">!`</span><span style="color: #D4D4D4">);</span></div><div class='line'><span style="color: #D4D4D4">}</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// @ts-ignore</span></div><div class='line'><span style="color: #DCDCAA"><data-lsp lsp='function logify(msg: string | number): void' >logify</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">);</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955"><data-err>// @ts-expect-error</data-err></span></div><span class="error"><span>Unused '@ts-expect-error' directive.</span><span class="code">2578</span></span><span class="error-behind">Unused '@ts-expect-error' directive.</span><div class='line'><span style="color: #DCDCAA"><data-lsp lsp='function logify(msg: string | number): void' >logify</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">);</span></div></code></div></pre>
<p>The difference between the two comment directives is that <code>@ts-expect-error</code> forces you to clean up later. <code>@ts-expect-error</code> will let you know when there's no TypeScript error to silence and when it has become useless. <code>@ts-ignore</code> will stay put and sit silently wherever you placed it.</p>
<p>But whatever you choose, remember that you can extend <code>@ts</code>-comments as well. So, when you place them in your codebase, do yourself a favor and document why they're there.</p>
<pre class="shiki dark-plus twoslash lsp" style="background-color: #1E1E1E; color: #D4D4D4"><div class="language-id">ts</div><div class='code-container'><code><div class='line'><span style="color: #6A9955">// @ts-ignore: `logify` should also allow numbers.</span></div><div class='line'><span style="color: #DCDCAA"><data-lsp lsp='function logify(msg: string): void' >logify</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">);</span></div><div class='line'>&nbsp;</div><div class='line'><span style="color: #6A9955">// @ts-expect-error: `logify` should also allow numbers.</span></div><div class='line'><span style="color: #DCDCAA"><data-lsp lsp='function logify(msg: string): void' >logify</data-lsp></span><span style="color: #D4D4D4">(</span><span style="color: #B5CEA8">123</span><span style="color: #D4D4D4">);</span></div></code></div></pre>
<p>That's it for today. ✌️</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20The difference between @ts-ignore and @ts-expect-error">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        How to load images in a spreadsheet
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/how-to-load-images-in-a-spreadsheet/"/>
      <published>2025-02-01T23:00:00+00:00</published>
      <updated>2025-02-01T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/how-to-load-images-in-a-spreadsheet/
      </id>
      <category term="tilPost"></category>
        <category term="Productivity"></category>
      
        <category term="Tools"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>Back in the days, I used to joke about people using spreadsheets for everything. Especially in larger companies; whatever can be placed in a spreadsheet will eventually land in a spreadsheet.</p>
<p>Thanks to Alex, today I learned, that <a href="https://alexwlchan.net/2025/images-and-spreadsheets/" style="--image-url: url('https://avatar.stefanjudis.com/https%3A%2F%2Falexwlchan.net%2F2025%2Fimages-and-spreadsheets%2F')">some spreadsheet tools (Google Sheets and Excel) let us load images</a>, too.</p>
<pre><code>=IMAGE(&quot;https://www.google.com/images/srpr/logo3w.png&quot;)
</code></pre>
<p>I think this is pretty wild.</p>
<p>
      <div class="sqip-container margin-top-l margin-bottom-l">
        <figure class="sqip-image" style="
          --color1: rgb(36,84,204); --color2: rgb(99,135,167); --color3: rgb(156,195,224); --color4: rgb(199,192,172); --color5: rgb(132,97,74);
          --sqip-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTYwIj48cGF0aCBmaWxsPSIjODQ2MTRhIiBkPSJNMCAwaDMwMHYxNjBIMHoiLz48ZyBmaWxsLW9wYWNpdHk9Ii41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSguNiAuNikgc2NhbGUoMS4xNzE4OCkiPjxjaXJjbGUgY3g9IjE2NyIgY3k9Ijg2IiByPSIyMDAiIGZpbGw9IiNmZmYiLz48Y2lyY2xlIHI9IjEiIGZpbGw9IiNmZmYiIHRyYW5zZm9ybT0ibWF0cml4KC00LjYzMTMyIDExNy4xODk2MiAtMTcxLjc3NDc3IC02Ljc4ODUyIDEzNS42IDY5KSIvPjxjaXJjbGUgcj0iMSIgZmlsbD0iI2ZjZmZmZiIgdHJhbnNmb3JtPSJtYXRyaXgoMy4zMjIgLTEwMC4xNDA0MyAyMTcuNjExMiA3LjIxODkgMTc1LjMgODYuNikiLz48Y2lyY2xlIHI9IjEiIGZpbGw9IiM4NWJiZTIiIHRyYW5zZm9ybT0ibWF0cml4KDgyLjA4MzM5IDMuMTEyMzMgLS40NzQzNCAxMi41MTAwMiA0NC43IDkxKSIvPjxjaXJjbGUgcj0iMSIgZmlsbD0iI2ZmZiIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MS40IDExMC4yIDI0LjcpIHNjYWxlKDY0LjkzNjI0IDE5My41MDExMykiLz48ZWxsaXBzZSBjeD0iNTEiIGN5PSIxMjgiIGZpbGw9IiNmZmYiIHJ4PSIyNTUiIHJ5PSIyNiIvPjxwYXRoIGZpbGw9IiM3YjY0NGYiIGQ9Im0xMTEuMSA4OS4yLTctMi4yLTQtMTguNUwxMTQgNzB6Ii8+PHBhdGggZmlsbD0iIzExYzQ2MSIgZD0iTTcuOSAyMS4xIDcuMSAyLjVsMTIuOCA3LjEtLjQtLjN6Ii8+PC9nPjwvc3ZnPg==');
        ">
          <a href="//images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png">
            <picture>
              <source type="image/avif"
                srcset="//images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=avif&fit=scale&q=75&w=300&h=160 300w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=avif&fit=scale&q=75&w=500&h=268 500w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=avif&fit=scale&q=75&w=700&h=375 700w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=avif&fit=scale&q=75&w=900&h=482 900w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=avif&fit=scale&q=75&w=1100&h=590 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <source type="image/webp"
                srcset="//images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=webp&fit=scale&q=75&w=300&h=160 300w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=webp&fit=scale&q=75&w=500&h=268 500w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=webp&fit=scale&q=75&w=700&h=375 700w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=webp&fit=scale&q=75&w=900&h=482 900w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=webp&fit=scale&q=75&w=1100&h=590 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <img width="1000" height="536"
                srcset="//images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=jpg&fit=scale&q=75&w=300&h=160 300w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=jpg&fit=scale&q=75&w=500&h=268 500w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=jpg&fit=scale&q=75&w=700&h=375 700w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=jpg&fit=scale&q=75&w=900&h=482 900w, //images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png?fm=jpg&fit=scale&q=75&w=1100&h=590 1100w"
                sizes="(max-width: 50em) 98vw, 700px"
                src="//images.ctfassets.net/f20lfrunubsq/VMCPaNYAQJQ27KTYOHCfj/4e95bac796b47f4fb7b4fa6b95948474/Screenshot_2025-02-02_at_14.02.13.png"
                alt="Google Sheet rendering image with the &quot;IMAGE()&quot; function."
                loading="lazy"
                onload="this.classList.add('kf-fade-in')">
            </picture>
          </a>
        </figure>
      </div>
    </p>
<p>But it might make sense to enrich sheets with images in product listings and similar things. 🤷</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20How to load images in a spreadsheet">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
    
    <entry>
      <title>
        Playwright has an experimental CLI watch mode
      </title>
      <link href="https://www.stefanjudis.com/today-i-learned/playwrights-cli-watch-mode/"/>
      <published>2025-01-26T23:00:00+00:00</published>
      <updated>2025-01-26T23:00:00+00:00</updated>
      <id>
        https://www.stefanjudis.com/today-i-learned/playwrights-cli-watch-mode/
      </id>
      <category term="tilPost"></category>
        <category term="Playwright"></category>
      
      <content type="html">
        <![CDATA[
          <div class="markdown"><p>You probably know that you can spin up Playwright's UI mode with <code>npx playwright test --ui</code> and rerun your tests with the watch mode.</p>
<p>
      <div class="sqip-container margin-top-l margin-bottom-l">
        <figure class="sqip-image" style="
          --color1: rgb(12,140,236); --color2: rgb(91,134,164); --color3: rgb(76,101,242); --color4: rgb(204,140,140); --color5: rgb(41,55,71);
          --sqip-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMDAgMTc2Ij48cGF0aCBmaWxsPSIjMjkzNzQ3IiBkPSJNMCAwaDMwMHYxNzZIMHoiLz48ZyBmaWxsLW9wYWNpdHk9Ii41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSguNiAuNikgc2NhbGUoMS4xNzE4OCkiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Im0yMzIuOCAxMS42LS42IDE4LTMzLTEuMi42LTE4eiIvPjxjaXJjbGUgcj0iMSIgZmlsbD0iIzI1MWQxMiIgdHJhbnNmb3JtPSJtYXRyaXgoLTE5NC42NTkwOSAtNjAuMjU3MDUgMjUuOTcxMDQgLTgzLjg5ODg5IDEwNi4xIDgzLjcpIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTIwMiAxM2gzMHYxN2gtMzB6Ii8+PGVsbGlwc2UgY3g9IjE5MSIgY3k9IjExOSIgZmlsbD0iIzQ1OTNjNCIgcng9Ijg1IiByeT0iNCIvPjxwYXRoIGZpbGw9IiMzODQ4NTYiIGQ9Ik0wIDQ5aDE2MnYxOEgweiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0yMDEgMTJoMjl2MTZoLTI5eiIvPjxjaXJjbGUgY3g9IjE1MCIgY3k9IjEiIHI9IjUxIiBmaWxsPSIjMjAxYTE0Ii8+PGVsbGlwc2UgY3g9IjI1NSIgY3k9IjMzIiBmaWxsPSIjMTkxNjE4IiByeD0iMjIiIHJ5PSI4OSIvPjwvZz48L3N2Zz4=');
        ">
          <a href="//images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png">
            <picture>
              <source type="image/avif"
                srcset="//images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=avif&fit=scale&q=75&w=300&h=177 300w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=avif&fit=scale&q=75&w=500&h=295 500w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=avif&fit=scale&q=75&w=700&h=413 700w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=avif&fit=scale&q=75&w=900&h=531 900w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=avif&fit=scale&q=75&w=1100&h=649 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <source type="image/webp"
                srcset="//images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=webp&fit=scale&q=75&w=300&h=177 300w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=webp&fit=scale&q=75&w=500&h=295 500w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=webp&fit=scale&q=75&w=700&h=413 700w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=webp&fit=scale&q=75&w=900&h=531 900w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=webp&fit=scale&q=75&w=1100&h=649 1100w"
                sizes="(max-width: 50em) 98vw, 700px">
              <img width="1000" height="590"
                srcset="//images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=jpg&fit=scale&q=75&w=300&h=177 300w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=jpg&fit=scale&q=75&w=500&h=295 500w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=jpg&fit=scale&q=75&w=700&h=413 700w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=jpg&fit=scale&q=75&w=900&h=531 900w, //images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png?fm=jpg&fit=scale&q=75&w=1100&h=649 1100w"
                sizes="(max-width: 50em) 98vw, 700px"
                src="//images.ctfassets.net/f20lfrunubsq/5dRHXtveQ2iYYcI96MuvmB/baf0bd79772559b49fe6153c10d09cb3/Screenshot_2025-01-27_at_16.09.59.png"
                alt="Playwright UI mode with an arrow highlighting the button to turn on watch mode."
                loading="lazy"
                onload="this.classList.add('kf-fade-in')">
            </picture>
          </a>
        </figure>
      </div>
    </p>
<p>It's a handy feature when you're developing your tests while still being in the try-and-error phase. Especially when I'm working on quick tests, I miss the quick feedback loop of CLI watch mode, which doesn't seem to be implemented. Or is it?</p>
<p>I just discovered a CLI watch mode topic GitHub issue.</p>
<p><div class="highlightBox github margin-top-l margin-bottom-l">
<div class="cornerBubble">
<svg aria-hidden="true">
<use xlink:href="/sprite.svg#icon-github"/>
</svg>
</div>
<div class="highlightBox__header">
<div class="d-flex-start-center">
<img src="https://avatars.githubusercontent.com/u/6154722?v=4" width="50" height="50" class="b-r-50 h-2em w-auto">
<a href="https://github.com/microsoft/playwright/issues/21960"><span class="margin-left-s">microsoft/playwright</span> <strong>#21960</strong></a> <span class="highlightBox__pill margin-left-auto margin-right-m">OPEN</span>
</div>
</div></p>
<p><div class="o-headline-3 margin-top-m">
[Feature] &quot;Headless&quot; CLI Watch Mode
</div>
<div class="d-flex-start-center"><img src="https://avatars.githubusercontent.com/u/2391878?u=d1884de65cdcb6ecc1122899bfefd15e8600c28d&v=4" width="50" height="50" class="b-r-50 h-2em w-auto"><span class="margin-left-s"> RichiCoder1 opened it Mar 24 2023</span></div>
</div></p>
<p>Turns out, people are asking for a CLI watch mode, and a first version is implemented, and you can use it by setting an environment variable.</p>
<pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">PWTEST_WATCH</span><span class="token operator">=</span><span class="token number">1</span> npx playwright <span class="token builtin class-name">test</span>
</code></pre>
<p>I played around with it for a minute and will definitely use the hack out of it.</p>
<p>
      <figure class="margin-top-l margin-bottom-l text-align-c text-style-italic">
        <video controls preload="metadata">
          <source src="//videos.ctfassets.net/f20lfrunubsq/5z3MmBblBMVq3zO3ii2ati/aa4d6c30f663b9935df250ce1f188aa4/watch_1.mp4" type="video/mp4">
        </video>
        <figcaption>Demo session showing Playwright&apos;s experimental CLI watch mode.</figcaption>
      </figure>
    </p>
<p>But be aware: <strong>the current implementation is experimental, and it's there to gather feedback from the community.</strong> The feature might break or even disappear, but if you have feedback or want to give a thumbs-up, head over to the GitHub issue.</p>
<p>I'd love to see this becoming a real <code>--watch</code> mode, though.</p>
</div>

          <a href="mailto:stefanjudis@gmail.com?subject=Re%3A%20Playwright has an experimental CLI watch mode">
            Reply to Stefan
          </a>
        ]]>
      </content>
    </entry>
</feed>
