<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/css" href="/c/feed-styles-CzFUArQc.css"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://jakearchibald.com/</id>
    <title>Jake Archibald's blog</title>
    <updated>2026-09-15T22:18:48.489Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://jakearchibald.com/"/>
    <logo>https://jakearchibald.com/c/icon-CJ5sT8Gh.png</logo>
    <icon>https://jakearchibald.com/favicon.ico</icon>
    <rights>https://creativecommons.org/licenses/by/4.0/</rights>
    <entry>
        <title type="html"><![CDATA[Controlling when CSS custom property values are computed]]></title>
        <id>https://jakearchibald.com/2026/css-custom-property-compute-time/</id>
        <link href="https://jakearchibald.com/2026/css-custom-property-compute-time/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-7bxD1M_W.png" type="image/png"/>
        <updated>2026-08-26T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[The default might not be what you expect, and it has a big impact on your styles.]]></summary>
        <content type="html"><![CDATA[<p>When and how CSS custom property values are calculated might not be what you expect, and it has a big impact on your styles.</p>
<h2 id="the-default"><a href="#the-default">The default</a></h2>
<p>Take this HTML:</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#808080">>&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> class</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"second-item"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">p</span><span style="color:#808080">></span><span style="color:#D4D4D4">…</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">p</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div><p>And this CSS:</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.second-item</span><span style="color:#D4D4D4"> {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: sibling-index();</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">&#x26; > :first-child {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">scale: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><p>If you&#39;re unfamiliar with <code>sibling-index()</code>, here&#39;s a quick video intro I posted recently:</p>
<style>
  .social-links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.7em;
    margin: 1.5em 0;
    padding: 0;
    list-style: none;
  }
  .social-links li {
    margin: 0;
  }
  .social-links a {
    display: flex;
    align-items: center;
    gap: 0.5em;
    padding: 0.35em 0.9em 0.35em 0.35em;
    line-height: 1;
  }
  .social-links img {
    display: block;
    width: 28px;
    height: 28px;
    border-radius: 7px;
  }
  .social-label {
    text-box: cap alphabetic;
  }
</style><ul class="social-links">
  <li>
    <a target="_blank" href="https://www.instagram.com/reel/Dcds4AjD4qo/">
      <img src="/c/instagram-DvVIhRIn.svg" alt="" width="28" height="28" /><span class="social-label">Instagram</span>
    </a>
  </li>
  <li>
    <a target="_blank" href="https://www.tiktok.com/@firefoxwebdevs/video/7677956182056733984">
      <img src="/c/tiktok-g3Cv2CY2.svg" alt="" width="28" height="28" /><span class="social-label">TikTok</span>
    </a>
  </li>
  <li>
    <a target="_blank" href="https://youtube.com/shorts/SSD_rbPhn_s?feature=share">
      <img src="/c/youtube-Cy40t_XB.svg" alt="" width="28" height="28" /><span class="social-label">YouTube</span>
    </a>
  </li>
</ul><p>The question is, how much is the paragraph going to scale?</p>
<p><code>sibling-index()</code> is 1-indexed, but is it going to pick up the index of the second div - where the custom property is defined, or its first child - where the custom property is used? The answer is: it depends.</p>
<p>If the code is taken in isolation, the value of the custom property will be stored as a token stream, so it&#39;s computed a little like this:</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.second-item</span><span style="color:#D4D4D4"> {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: sibling-index();</span></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* …Which computes to: */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: sibling-index();</span></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* (as in, no change) */</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">&#x26; > :first-child {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">scale: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#6A9955">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#6A9955">/* …Which is substituted for: */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">scale</span><span style="color:#D4D4D4">: sibling-index();</span></span></span><span class="line"><span class="indent"><span style="color:#6A9955">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#6A9955">/* …Which computes to: */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">scale</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><p>So the <code>var()</code> is substituted for <code>sibling-index()</code>, which is evaluated in that context. Since the paragraph is the first child of its parent, it will scale by 1.</p>
<p>This is reflected in <code>getComputedStyle()</code>:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#569CD6">const</span><span style="color:#4FC1FF"> div</span><span style="color:#D4D4D4"> = </span><span style="color:#9CDCFE">document</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">querySelector</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#CE9178">'.second-item'</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#9CDCFE">console</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">log</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#DCDCAA">getComputedStyle</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">div</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">).</span><span style="color:#DCDCAA">getPropertyValue</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'--index'</span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#6A9955">// Logs: "sibling-index()"</span></span></span></code></pre></div><div class="code-variant code-variant--48"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#569CD6">const</span><span style="color:#4FC1FF"> div</span><span style="color:#D4D4D4"> = </span><span style="color:#9CDCFE">document</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">querySelector</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#CE9178">'.second-item'</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#9CDCFE">console</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">log</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#DCDCAA">getComputedStyle</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">div</span><span style="color:#D4D4D4">).</span><span style="color:#DCDCAA">getPropertyValue</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">'--index'</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#6A9955">// Logs: "sibling-index()"</span></span></span></code></pre></div><div class="code-variant code-variant--56"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#569CD6">const</span><span style="color:#4FC1FF"> div</span><span style="color:#D4D4D4"> = </span><span style="color:#9CDCFE">document</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">querySelector</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'.second-item'</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#9CDCFE">console</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">log</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#DCDCAA">getComputedStyle</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">div</span><span style="color:#D4D4D4">).</span><span style="color:#DCDCAA">getPropertyValue</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'--index'</span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#6A9955">// Logs: "sibling-index()"</span></span></span></code></pre></div><div class="code-variant code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#569CD6">const</span><span style="color:#4FC1FF"> div</span><span style="color:#D4D4D4"> = </span><span style="color:#9CDCFE">document</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">querySelector</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'.second-item'</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#9CDCFE">console</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">log</span><span style="color:#D4D4D4">(</span><span style="color:#DCDCAA">getComputedStyle</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">div</span><span style="color:#D4D4D4">).</span><span style="color:#DCDCAA">getPropertyValue</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'--index'</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#6A9955">// Logs: "sibling-index()"</span></span></span></code></pre></div></div><p>But that&#39;s just the default…</p>
<h2 id="changing-the-default"><a href="#changing-the-default">Changing the default</a></h2>
<p>Let&#39;s register that custom property:</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#C586C0">@property</span><span style="color:#D4D4D4"> --index {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">syntax: '&#x3C;number>';</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">inherits: true;</span></span></span><span class="line"><span class="indent"><span style="color:#569CD6">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#569CD6">initial-value</span><span style="color:#D4D4D4">: 0;</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><p>Custom properties you haven&#39;t registered behave as if they have <code>syntax: &#39;*&#39;</code>, meaning &#39;anything&#39;, which computes to a token stream as before. But here we&#39;re using <code>&#39;&lt;number&gt;&#39;</code>, meaning it computes the value as a number:</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.second-item</span><span style="color:#D4D4D4"> {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: sibling-index();</span></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* …Which computes to: */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">&#x26; > :first-child {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">scale: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#6A9955">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#6A9955">/* …Which is substituted for: */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">scale</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#6A9955">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#6A9955">/* …Which computes to: */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">scale</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><p>Registering the custom property &amp; giving it a syntax meant the full value was computed earlier, in a different context, so the result was different.</p>
<p>Again, this is reflected in <code>getComputedStyle()</code>:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#569CD6">const</span><span style="color:#4FC1FF"> div</span><span style="color:#D4D4D4"> = </span><span style="color:#9CDCFE">document</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">querySelector</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#CE9178">'.second-item'</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#9CDCFE">console</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">log</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#DCDCAA">getComputedStyle</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">div</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">).</span><span style="color:#DCDCAA">getPropertyValue</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'--index'</span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#6A9955">// Logs: "2"</span></span></span></code></pre></div><div class="code-variant code-variant--48"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#569CD6">const</span><span style="color:#4FC1FF"> div</span><span style="color:#D4D4D4"> = </span><span style="color:#9CDCFE">document</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">querySelector</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#CE9178">'.second-item'</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#9CDCFE">console</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">log</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#DCDCAA">getComputedStyle</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">div</span><span style="color:#D4D4D4">).</span><span style="color:#DCDCAA">getPropertyValue</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">'--index'</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#6A9955">// Logs: "2"</span></span></span></code></pre></div><div class="code-variant code-variant--56"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#569CD6">const</span><span style="color:#4FC1FF"> div</span><span style="color:#D4D4D4"> = </span><span style="color:#9CDCFE">document</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">querySelector</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'.second-item'</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#9CDCFE">console</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">log</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#DCDCAA">getComputedStyle</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">div</span><span style="color:#D4D4D4">).</span><span style="color:#DCDCAA">getPropertyValue</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'--index'</span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#6A9955">// Logs: "2"</span></span></span></code></pre></div><div class="code-variant code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#569CD6">const</span><span style="color:#4FC1FF"> div</span><span style="color:#D4D4D4"> = </span><span style="color:#9CDCFE">document</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">querySelector</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'.second-item'</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#9CDCFE">console</span><span style="color:#D4D4D4">.</span><span style="color:#DCDCAA">log</span><span style="color:#D4D4D4">(</span><span style="color:#DCDCAA">getComputedStyle</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">div</span><span style="color:#D4D4D4">).</span><span style="color:#DCDCAA">getPropertyValue</span><span style="color:#D4D4D4">(</span><span style="color:#CE9178">'--index'</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#6A9955">// Logs: "2"</span></span></span></code></pre></div></div><p>And it&#39;s not just <code>sibling-index()</code> that behaves this way. This also impacts relative font units (<code>em</code>, <code>ex</code>, <code>ch</code>, <code>cap</code>, <code>lh</code> etc.), container query units (<code>cqw</code>, <code>cqi</code>, <code>cqb</code> etc.), and more. In all these cases, <code>@property</code> controls which element the values are computed against.</p>
<p>Another thing that&#39;s impacted, although slightly differently, is URLs. In CSS, URLs are generally resolved against the stylesheet&#39;s base URL at computed-value time - so if a token-stream custom property is defined in one stylesheet and <code>var()</code>&#39;d from another, the URL resolves against the consuming stylesheet. Registering the custom property with <code>syntax: &#39;&lt;url&gt;&#39;</code> pins it to the stylesheet that declared it.</p>
<h2 id="but-what-about-var"><a href="#but-what-about-var">But what about var()?</a></h2>
<p>Here&#39;s a trickier example:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40 code-variant--48"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.second-item</span><span style="color:#D4D4D4"> {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--multiplier</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">sibling-index() * </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--multiplier</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">&#x26; > :first-child {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">--multiplier: </span><span style="color:#B5CEA8">3</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">scale</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--56 code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.second-item</span><span style="color:#D4D4D4"> {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--multiplier</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(sibling-index() * </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--multiplier</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">&#x26; > :first-child {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">--multiplier: </span><span style="color:#B5CEA8">3</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">scale</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div></div><p>What happens here? Well, <code>var()</code> behaves differently to <code>sibling-index()</code>. <code>var()</code> is an <a href="https://drafts.csswg.org/css-values-5/#arbitrary-substitution-function">&quot;arbitrary substitution function&quot;</a>, along with a bunch of other functions like <code>if()</code>, <code>attr()</code>, and <code>ident()</code>. These are substituted regardless of the syntax of the custom property. So:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40 code-variant--48"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.second-item</span><span style="color:#D4D4D4"> {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--multiplier</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">sibling-index() * </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--multiplier</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* …Which is substituted for: */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(sibling-index() * </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--56 code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.second-item</span><span style="color:#D4D4D4"> {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--multiplier</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(sibling-index() * </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--multiplier</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* …Which is substituted for: */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--index</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(sibling-index() * </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div></div><p>What happens next depends on the defined syntax of <code>--index</code>. It&#39;ll either stay as the above token stream, or it&#39;ll compute down to <code>2</code> - the same rules as before. In both cases, <code>--multiplier: 3</code> on the child is ignored.</p>
<p>This was a detail of CSS that I sort-of half-knew, as in, I knew <code>@property</code> changed computation timing somehow, but I hadn&#39;t fully understood when or how, until I looked it up today. I hope you understand it now too!</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Fixing my tooltip accessibility mistake]]></title>
        <id>https://jakearchibald.com/2026/my-tooltip-a11y-mistake/</id>
        <link href="https://jakearchibald.com/2026/my-tooltip-a11y-mistake/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-DSh_Zna4.png" type="image/png"/>
        <updated>2026-07-28T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[aria-describedby isn&#39;t always enough.]]></summary>
        <content type="html"><![CDATA[<p>I made an accessibility error, and I want you to learn from my mistakes.</p>
<p>I&#39;ve been making short videos recently about web platform features as they land in Firefox, and also about other web standards &amp; development stuff. If you&#39;d prefer to watch a 3-minute video version of this article, pick your platform, and give the account a follow if this kind of thing interests you:</p>
<style>
  .social-links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.7em;
    margin: 1.5em 0;
    padding: 0;
    list-style: none;
  }
  .social-links li {
    margin: 0;
  }
  .social-links a {
    display: flex;
    align-items: center;
    gap: 0.5em;
    padding: 0.35em 0.9em 0.35em 0.35em;
    line-height: 1;
  }
  .social-links img {
    display: block;
    width: 28px;
    height: 28px;
    border-radius: 7px;
  }
</style><ul class="social-links">
  <li>
    <a href="https://www.instagram.com/reel/DbTA45PjDWT/">
      <img src="/c/instagram-DvVIhRIn.svg" alt="" width="28" height="28" />Instagram
    </a>
  </li>
  <li>
    <a href="https://www.tiktok.com/@firefoxwebdevs/video/7667190734281248032">
      <img src="/c/tiktok-g3Cv2CY2.svg" alt="" width="28" height="28" />TikTok
    </a>
  </li>
  <li>
    <a href="https://www.youtube.com/shorts/hPrZiRSRCE4">
      <img src="/c/youtube-Cy40t_XB.svg" alt="" width="28" height="28" />YouTube
    </a>
  </li>
  <li>
    <a href="https://mastodon.social/@firefoxwebdevs/116992048337520434">
      <img src="/c/mastodon-B2ifWMXN.svg" alt="" width="28" height="28" />Mastodon
    </a>
  </li>
  <li>
    <a href="https://www.linkedin.com/posts/firefox-web-developers_i-made-an-accessibility-error-in-one-of-my-activity-7487491350968434690-fc6k">
      <img src="/c/linkedin-A5bi48Qo.svg" alt="" width="28" height="28" />LinkedIn
    </a>
  </li>
</ul><p>The account is <a href="https://bsky.app/profile/webdevs.firefox.com">also on Bluesky</a>, but video uploading has been broken there for a while.</p>
<p>Otherwise, here&#39;s the written version…</p>
<h2 id="what-i-did"><a href="#what-i-did">What I did</a></h2>
<p>I made the accessibility error when I created a tooltip like this:</p>
<figure class="full-figure">
  <img style="height:auto" src="/c/toolbar-C5-zEKJP.avif" width="1303" height="808" alt="A text formatting toolbar, featuring buttons like bold, italic, underline etc. A mouse pointer is over the bold button, and a tooltip is visible, saying 'Bold (⌘B)'." />
</figure><p>That was part of a video on <code>popover=&quot;hint&quot;</code>, which also covers how to show the tooltip for visual users. Oh ok, here are links to that video too:</p>
<ul class="social-links">
  <li>
    <a href="https://www.instagram.com/reel/DbGK4PtCqbj/">
      <img src="/c/instagram-DvVIhRIn.svg" alt="" width="28" height="28" />Instagram
    </a>
  </li>
  <li>
    <a href="https://www.tiktok.com/@firefoxwebdevs/video/7665339722767633697">
      <img src="/c/tiktok-g3Cv2CY2.svg" alt="" width="28" height="28" />TikTok
    </a>
  </li>
  <li>
    <a href="https://www.youtube.com/shorts/HWetxbInaVc">
      <img src="/c/youtube-Cy40t_XB.svg" alt="" width="28" height="28" />YouTube
    </a>
  </li>
  <li>
    <a href="https://mastodon.social/@firefoxwebdevs/116963801309656205">
      <img src="/c/mastodon-B2ifWMXN.svg" alt="" width="28" height="28" />Mastodon
    </a>
  </li>
  <li>
    <a href="https://www.linkedin.com/feed/update/urn:li:activity:7485683643156406272">
      <img src="/c/linkedin-A5bi48Qo.svg" alt="" width="28" height="28" />LinkedIn
    </a>
  </li>
</ul><p>I did try to get it right! I spoke to friends who know more about accessibility than me, and they pointed me towards <a href="https://scottaohara.github.io/a11y_tooltips/">these demos by Scott O&#39;Hara</a>.</p>
<p>Now, Scott knows what he&#39;s talking about, so I figured I could just copy the patterns in his demo. Scott&#39;s demos predate hint popovers, so here&#39;s a modernised version of one of his examples:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40 code-variant--48"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span><span style="color:#9CDCFE"> aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"edit-tooltip"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">Edit</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"edit-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">Modify account settings.</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div><div class="code-variant code-variant--56 code-variant--64"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span><span style="color:#9CDCFE"> aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"edit-tooltip"</span><span style="color:#808080">></span><span style="color:#D4D4D4">Edit</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"edit-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">Modify account settings.</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div><div class="code-variant code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span><span style="color:#9CDCFE"> aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"edit-tooltip"</span><span style="color:#808080">></span><span style="color:#D4D4D4">Edit</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"edit-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span><span style="color:#D4D4D4">Modify account settings.</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div></div><p>The button is connected to the tooltip with <code>aria-describedby</code>, so screen readers will usually read the tooltip when the button is focused.</p>
<p>One of the nice things about <code>aria-describedby</code> is that it works even if the element it&#39;s pointing at is hidden, which is the case here.</p>
<p>So, I adapted Scott&#39;s code for my toolbar demo:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40 code-variant--48"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">aria-label</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"Bold"</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span><span style="color:#D4D4D4">(bold icon)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">Bold (⌘B)</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div><div class="code-variant code-variant--56"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">aria-label</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"Bold"</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span><span style="color:#D4D4D4">(bold icon)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span><span style="color:#D4D4D4">Bold (⌘B)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div><div class="code-variant code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span><span style="color:#9CDCFE"> aria-label</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"Bold"</span><span style="color:#9CDCFE"> aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span><span style="color:#D4D4D4">(bold icon)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span><span style="color:#D4D4D4">Bold (⌘B)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div></div><p>It&#39;s a very similar pattern, although I have <code>aria-label</code> on the button, because it contains an SVG icon rather than text.</p>
<p>Like Scott, I added <code>aria-describedby</code> to the button, to connect the button to the tooltip.</p>
<p>I tested it in VoiceOver, and it said &quot;Bold. Bold. Command B. Button&quot;. And I thought, that&#39;s not quite right - it doesn&#39;t need to say &quot;bold&quot; twice.</p>
<p>That was happening because, unlike Scott&#39;s example, where the tooltip contained just additional content, my tooltip also contains the label. Bold is said twice, because it&#39;s there, twice.</p>
<p>So I dropped the <code>aria-label</code>…</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus has-diff" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line diff remove"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span><span style="color:#9CDCFE"> aria-label</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"Bold"</span><span style="color:#9CDCFE"> aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#808080">></span></span></span><span class="line diff add"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span><span style="color:#9CDCFE"> aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span><span style="color:#D4D4D4">(bold icon)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span><span style="color:#D4D4D4">Bold (⌘B)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div><p>…and VoiceOver now said &quot;Clickable image. Bold. Command B. Button&quot;. I thought, that&#39;ll do! I hit publish on my video, and I went to the pub.</p>
<h2 id="where-i-went-wrong"><a href="#where-i-went-wrong">Where I went wrong</a></h2>
<p>I got a message from <a href="https://front-end.social/@tink">Léonie Watson</a> and <a href="https://www.linkedin.com/in/gez-lemon-0240692/">Gez Lemon</a> from the accessibility agency <a href="https://tetralogical.com/">TetraLogical</a>, telling me, very politely, that I&#39;d done a silly.</p>
<p>Scott&#39;s pattern was correct, but I&#39;d gone off the rails when I removed the <code>aria-label</code>, because now, the button has no accessible name. <code>aria-describedby</code> provides additional information - it isn&#39;t a replacement for the accessible name.</p>
<p>In fact, JAWS on Windows tries to gather accessible text from nearby elements, and ends up announcing the bold button as both bold and italic, since the italic tooltip element was a sibling in the DOM.</p>
<p>There was a bit of a clue when VoiceOver said &quot;clickable image&quot; - this was VoiceOver indicating it didn&#39;t have an accessible name to announce. I just didn&#39;t realise it at the time.</p>
<h2 id="the-fix"><a href="#the-fix">The fix</a></h2>
<p>Léonie and Gez told me how to fix it: I switched from <code>aria-describedby</code> to <code>aria-labelledby</code>.</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus has-diff" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line diff remove"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span><span style="color:#9CDCFE"> aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#808080">></span></span></span><span class="line diff add"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span><span style="color:#9CDCFE"> aria-labelledby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span><span style="color:#D4D4D4">(bold icon)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span><span style="color:#D4D4D4">Bold (⌘B)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div><p><code>aria-labelledby</code> works like <code>aria-describedby</code>, but it provides the accessible name, rather than additional description.</p>
<p>Now the button gets its accessible name from the tooltip, and VoiceOver says &quot;Bold. Command B. Button.&quot; - perfect!</p>
<p>If I wanted, I could split this up, so the accessible name is just &quot;Bold&quot;, and the additional description is &quot;Command B&quot;:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">aria-labelledby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-label"</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-description"</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span><span style="color:#D4D4D4">(bold icon)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">span</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-label"</span><span style="color:#808080">></span><span style="color:#D4D4D4">Bold</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">span</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">span</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-description"</span></span></span><span class="line"><span class="indent"><span style="color:#808080">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#808080">></span><span style="color:#D4D4D4">(⌘B)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">span</span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div><div class="code-variant code-variant--48 code-variant--56 code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">button</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">aria-labelledby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-label"</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">aria-describedby</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-description"</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span><span style="color:#D4D4D4">(bold icon)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">svg</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">button</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">div</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-tooltip"</span><span style="color:#9CDCFE"> popover</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"hint"</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">span</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-label"</span><span style="color:#808080">></span><span style="color:#D4D4D4">Bold</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">span</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"><span style="color:#808080">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#808080">&#x3C;</span><span style="color:#569CD6">span</span><span style="color:#9CDCFE"> id</span><span style="color:#D4D4D4">=</span><span style="color:#CE9178">"bold-description"</span><span style="color:#808080">></span><span style="color:#D4D4D4">(⌘B)</span><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">span</span><span style="color:#808080">></span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#808080">&#x3C;/</span><span style="color:#569CD6">div</span><span style="color:#808080">></span></span></span></code></pre></div></div><p>I think that&#39;s overkill in this case, but it might make sense if you have a tooltip with a longer description.</p>
<p>So there you go! Always ensure interactive elements have an accessible name. And test with multiple screen readers if you can. Testing in one screen reader is like testing in one browser - it doesn&#39;t always give you the full picture.</p>
<p><a href="https://random-stuff.jakearchibald.com/popover-hint-tooltip/">Here&#39;s the fixed demo</a>, although it doesn&#39;t work quite right in Safari, as it doesn&#39;t yet support <code>popover=&quot;hint&quot;</code>.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Goldilocks customizable select height]]></title>
        <id>https://jakearchibald.com/2026/goldilocks-select-height/</id>
        <link href="https://jakearchibald.com/2026/goldilocks-select-height/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-CxMtxBGV.png" type="image/png"/>
        <updated>2026-06-29T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[The &#39;ideal&#39; sizing is more complicated than you think…]]></summary>
        <content type="html"><![CDATA[<p>I recently gave a talk on customizable (as in fully-stylable) <code>&lt;select&gt;</code>, and as I was building demos I realised there&#39;s a sizing &#39;pattern&#39; that&#39;s almost always the-one-you-want, but it took me a long time to figure out how to do it in CSS.</p>
<p>Well, I say I figured it out. I actually failed, and asked a bunch of people for help, who (thankfully, for my ego) also struggled. Eventually, <a href="https://bsky.app/profile/bfgeek.bsky.social">Ian Kilpatrick</a> pointed me at the feature I was missing…</p>
<p>TL;DR: If you just want the solution, <a href="#putting-it-all-together">skip to the end</a>.</p>
<p>Also, if you want a general introduction to customizable <code>&lt;select&gt;</code>, <a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select">MDN has you covered</a>.</p>
<h2 id="default-sizing"><a href="#default-sizing">Default sizing</a></h2>
<p>Here&#39;s a mock-up of a custom select:</p>
<style>
  @font-face {
    font-family: seti;
    src: url('/c/seti-BYvugthT.woff') format('woff');
  }
  .ua-picker-styles {
    box-sizing: border-box;
    border: 1px solid;
    padding: 0;
    margin: 0;
    inset: auto;
    min-inline-size: anchor-size(self-inline);
    max-block-size: 100%;
    max-block-size: stretch;
    position-area: self-block-end span-self-inline-end;
    position-try-order: most-block-size;
    position-try-fallbacks: self-block-start span-self-inline-end, self-block-end span-self-inline-start, self-block-start span-self-inline-start;
    min-block-size: 1lh;
  }
  .select-picker {
    line-height: normal;
    font-family: system-ui;
    border: none;
    background: none;
    overflow: clip;
    container-type: anchored;
    filter: drop-shadow(0 0.3em 0.275em rgba(0, 0, 0, 0.25));
    grid-template-rows: 1fr;

    opacity: 0;
    transition:
      opacity 250ms ease,
      height 250ms,
      display 250ms step-end allow-discrete,
      overlay 250ms allow-discrete;

    &:popover-open {
      display: grid;
      opacity: 1;
      @starting-style {
        opacity: 0;
      }

      .select-picker-box {
        translate: 0 0;
        transition-timing-function: ease;

        @starting-style {
          translate: 0 -6.25em;
          /* flip-block, but explicit so all above-anchor fallbacks match */
          @container anchored(fallback: self-block-start span-self-inline-end) or anchored(fallback: self-block-start span-self-inline-start) or anchored(fallback: flip-block) or anchored(fallback: flip-block flip-inline) {
            translate: 0 6.25em;
          }
        }
      }
    }
  }
  .select-picker-box {
    background: #f8f8f8;
    border: 0.0625em solid #d4d4d4;
    border-radius: 0.5em;
    overflow: auto;
    overscroll-behavior: none;
    scroll-padding-top: 3.125em;

    transition: translate 250ms ease-in;
    translate: 0 -6.25em;

    /* flip-block, but explicit so all above-anchor fallbacks match */
    @container anchored(fallback: self-block-start span-self-inline-end) or anchored(fallback: self-block-start span-self-inline-start) or anchored(fallback: flip-block) or anchored(fallback: flip-block flip-inline) {
      translate: 0 6.25em;
    }
  }
  .select-picker-lang-icon {
    font-family: seti;
    font-size: 1.5em;
    display: grid;
    justify-content: center;

    &::before {
      content: attr(data-char);
    }

    &.lang-blue {
      color: #498ba7;
    }
    &.lang-orange {
      color: #cc6d2e;
    }
    &.lang-purple {
      color: #9068b0;
    }
    &.lang-yellow {
      color: #b7b73b;
    }
    &.lang-red {
      color: #b8383d;
    }
    &.lang-green {
      color: #7fae42;
    }
    &.lang-grey {
      color: #627379;
    }
    &.lang-pink {
      color: #dd4b78;
    }
    &.lang-white {
      color: #bfc2c1;
    }
    &.lang-dark {
      color: #3b4b52;
    }
  }
  .select-picker-legend {
    position: sticky;
    top: 0;
    container-type: scroll-state;
    z-index: 1;
    background: #ebebeb;
    color: #6d8086;
    padding: 0.5em 0.75em;
    font-size: 0.85em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    border-bottom: 0.0625em solid #d4d4d4;
    transition: all 500ms ease;
    transition-property: background-color, color;

    @container scroll-state(stuck: top) {
      background-color: #e0e0e0;
      color: #41535b;
    }
  }
  .select-picker-group {
    --legend-text: attr(label);
    position: relative;

    @supports (container-type: scroll-state) {
      &:first-of-type > .select-picker-legend::before {
        background-color: #e0e0e0;
        color: #41535b;
      }
    }

    & > .select-picker-legend::before {
      content: var(--legend-text);
    }
  }

  .select-picker-option {
    padding: 0.4em 0.5em;
    color: #41535b;
    display: grid;
    grid-template-columns: 2.2ch 10.6ch;
    gap: 0.5em;
    align-items: center;
    position: relative;

    &:hover {
      background: #e0e7ea;
    }
  }

  .margin-block-end {
    margin-block-end: 1em;
  }
  .margin-firefox-fix {
    --viewport-margin: 1em;
    max-block-size: calc(100% - var(--viewport-margin));

    @supports (max-block-size: stretch) {
      max-block-size: stretch;
      margin-block-end: var(--viewport-margin);
    }
  }
  .margin-full-fix {
    --viewport-margin: 1em;
    max-block-size: calc(100% - var(--viewport-margin));
    position-try-fallbacks:
      flip-block,
      flip-inline,
      flip-block flip-inline;

    @supports (max-block-size: stretch) {
      max-block-size: stretch;
      margin-block-end: var(--viewport-margin);
    }
  }
  .min-size {
    min-block-size: 12em;
  }
  .max-size-fix {
    --max-size: 30em;
    --viewport-margin: 1em;
    max-block-size: min(calc(100% - var(--viewport-margin)), var(--max-size));
    position-try-fallbacks:
      flip-block,
      flip-inline,
      flip-block flip-inline;

    @supports (max-block-size: calc-size(stretch, min(size, 1px))) {
      max-block-size: calc-size(stretch, min(size, var(--max-size)));
      margin-block-end: var(--viewport-margin);
    }
  }
  .min-size-fix {
    --min-size: 12em;
    min-block-size: var(--min-size);

    @supports (min-block-size: calc-size(fit-content, min(size, 1px))) {
      min-block-size: calc-size(fit-content, min(size, var(--min-size)));
    }

    @supports not (min-block-size: calc-size(fit-content, min(size, 1px))) {
      &:not(:has(:where(.select-picker-option:nth-child(5)))) {
        min-block-size: 0;
        max-block-size: fit-content;
      }
    }
  }

  .full-solution {
    --viewport-margin: 1em;
    --min-size: 12em;
    --max-size: 30em;

    min-block-size: var(--min-size);
    max-block-size: min(calc(100% - var(--viewport-margin)), var(--max-size));
    position-try-fallbacks:
      flip-block,
      flip-inline,
      flip-block flip-inline;

    @supports (min-block-size: calc-size(fit-content, min(size, 1px))) {
      min-block-size: calc-size(fit-content, min(size, var(--min-size)));
      max-block-size: calc-size(stretch, min(size, var(--max-size)));
      margin-block-end: var(--viewport-margin);
    }

    @supports not (min-block-size: calc-size(fit-content, min(size, 1px))) {
      &:not(:has(:where(.select-picker-option:nth-child(5)))) {
        min-block-size: 0;
        max-block-size: fit-content;
      }
    }
  }

  /* body > * {
    visibility: hidden;
  }
  toggle-picker, [popover] {
    visibility: visible;
  } */
</style><div id="select-picker" popover>
  <div class="select-picker-box">
    <div class="select-picker-group">
      <div class="select-picker-legend">Markup Languages</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        HTML
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        CSS
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        XML
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-purple" data-char=""></span>
        SVG
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Markdown
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">Scripting Languages</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-yellow" data-char=""></span>
        JavaScript
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Python
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Ruby
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-purple" data-char=""></span>
        PHP
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Perl
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Lua
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Shell
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        PowerShell
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">Transpiled Languages</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        TypeScript
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-yellow" data-char=""></span>
        CoffeeScript
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-yellow" data-char=""></span>
        Babel
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Elm
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        ReasonML
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        ReScript
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        Haxe
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Dart
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">Compiled Systems Languages</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        C
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        C++
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        C#
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-grey" data-char=""></span>
        Rust
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Go
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        Zig
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        D
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-grey" data-char=""></span>
        Vala
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">JVM Languages</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Java
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Scala
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        Kotlin
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Clojure
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Groovy
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">Functional Languages</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-purple" data-char=""></span>
        Haskell
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        OCaml
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        F#
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-purple" data-char=""></span>
        Elixir
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-white" data-char=""></span>
        PureScript
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        Prolog
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">Web Frameworks &amp; Libraries</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        React
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Vue
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Svelte
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">Template Languages</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-yellow" data-char=""></span>
        EJS
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Jade/Pug
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Haml
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Liquid
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        Mustache
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Nunjucks
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Twig
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        Slim
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-yellow" data-char=""></span>
        Smarty
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Jinja
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">CSS Preprocessors</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-pink" data-char=""></span>
        Sass/SCSS
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Less
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Stylus
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">Config Files</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-yellow" data-char=""></span>
        JSON
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-purple" data-char=""></span>
        YAML
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-grey" data-char=""></span>
        TOML
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        CSV
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        TSConfig
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-yellow" data-char=""></span>
        Vite
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Webpack
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Rollup
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        Grunt
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Gulp
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Gradle
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-red" data-char=""></span>
        Maven
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        SBT
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Bazel
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-purple" data-char=""></span>
        ESLint
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-white" data-char=""></span>
        Stylelint
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        Bower
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-dark" data-char=""></span>
        NPM
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Yarn
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Docker
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-green" data-char=""></span>
        Karma
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">Database &amp; Query</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-pink" data-char=""></span>
        SQL
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-pink" data-char=""></span>
        GraphQL
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        Prisma
      </div>
    </div>
    <div class="select-picker-group">
      <div class="select-picker-legend">Other Languages</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-purple" data-char=""></span>
        Julia
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        R
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-white" data-char=""></span>
        Crystal
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-yellow" data-char=""></span>
        Nim
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-orange" data-char=""></span>
        Swift
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-purple" data-char=""></span>
        WebAssembly
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        LaTeX
      </div>
    </div>
  </div>
</div><div id="select-picker-small" popover>
  <div class="select-picker-box">
    <div class="select-picker-group">
      <div class="select-picker-legend">The best Languages</div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        HTML
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-blue" data-char=""></span>
        CSS
      </div>
      <div class="select-picker-option">
        <span class="select-picker-lang-icon lang-purple" data-char=""></span>
        SVG
      </div>
    </div>
  </div>
</div><p><toggle-picker data-picker-classes="ua-picker-styles select-picker"></toggle-picker></p>
<script>
  for (const picker of document.querySelectorAll('[id^="select-picker"]')) {
    picker.addEventListener('click', (event) => {
      event.currentTarget.hidePopover();
    });
  }

  const togglePickerStyles = new CSSStyleSheet();
  togglePickerStyles.replaceSync(`
    :host {
      display: block;
    }
    .toggle-button-panner {
      position: relative;
      touch-action: none;
      width: fit-content;
    }

    /* Shared button look */
    button {
      appearance: none;
      margin: 0;
      display: flex;
      align-items: stretch;
      border-radius: 0.375em;
      overflow: clip;
      background: #e9e9ed;
      border: 0.0625em solid #b0b0b8;
      box-shadow: 0 0.0625em 0.0625em rgba(0, 0, 0, 0.08);
      color: #1c1c1e;
      font: inherit;
      padding: 0;
      cursor: pointer;
      white-space: nowrap;

      &:hover {
        background: #dededf;
      }
      &:active {
        background: #d4d4d6;
      }

      & > .label {
        padding: 0.6em 0.7em;
        line-height: 1;
      }
    }

    .panner {
      display: grid;
      place-items: center;
      padding: 0 0.3em;
      cursor: grab;
      touch-action: none;
      /* Visually separate from, but joined to, the button */
      border-right: 0.0625em solid #b0b0b8;

      &:active {
        cursor: grabbing;
      }

      & > svg {
        display: block;
        pointer-events: none;
        fill: #5c5c61;
        width: 20px;
        aspect-ratio: 1;
      }
    }

    .toggle-button-panner.dragging .panner {
      cursor: grabbing;
    }

    /* Reset button only appears once the toggle button has moved away */
    .reset {
      display: none;
    }

    .toggle-button-panner.moved {
      & > .toggle {
        /* Lift out of flow only once moved, positioned at the drop point */
        position: absolute;
        top: var(--panner-y, 0px);
        left: var(--panner-x, 0px);
        margin: 0;
        z-index: 1;
      }

      & > .reset {
        display: flex;
      }
    }
  `);

  customElements.define(
    'toggle-picker',
    class extends HTMLElement {
      #group;
      #toggle;
      #reset;
      #panner;
      #picker;

      connectedCallback() {
        if (this.shadowRoot) return;

        this.#picker = document.getElementById(
          this.dataset.picker || 'select-picker',
        );

        const root = this.attachShadow({ mode: 'open' });
        root.adoptedStyleSheets = [togglePickerStyles];
        root.innerHTML = `
          <div class="toggle-button-panner">
            <button class="toggle" type="button" command="toggle-popover">
              <span class="panner"><svg height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-80 310-250l57-57 73 73v-206H235l73 72-58 58L80-480l169-169 57 57-72 72h206v-206l-73 73-57-57 170-170 170 170-57 57-73-73v206h205l-73-72 58-58 170 170-170 170-57-57 73-73H520v205l72-73 58 58L480-80Z"/></svg></span>
              <span class="label"><slot>Toggle picker</slot></span>
            </button>
            <button class="reset" type="button">
              <span class="label">Reset</span>
            </button>
          </div>
        `;

        this.#group = root.querySelector('.toggle-button-panner');
        this.#toggle = root.querySelector('.toggle');
        this.#reset = root.querySelector('.reset');
        this.#panner = root.querySelector('.panner');

        this.#toggle.addEventListener('click', (event) => {
          event.preventDefault();
          this.#picker.className = this.dataset.pickerClasses || '';
          this.#picker.togglePopover({ source: this.#toggle });
        });

        this.#toggle.commandForElement = this.#picker;

        this.#wireDrag();
        this.#reset.addEventListener('click', () => this.#resetPosition());
      }

      #resetPosition() {
        this.#group.classList.remove('moved');
        this.#group.style.removeProperty('--panner-x');
        this.#group.style.removeProperty('--panner-y');
      }

      #wireDrag() {
        const group = this.#group;
        const toggle = this.#toggle;
        const panner = this.#panner;

        let pointerId = null;
        // Position of the toggle's top-left at grab time, and where the
        // pointer started, both relative to the group (the offset parent).
        let baseX = 0;
        let baseY = 0;
        let startX = 0;
        let startY = 0;

        panner.addEventListener('pointerdown', (event) => {
          if (pointerId !== null) return;
          pointerId = event.pointerId;
          panner.setPointerCapture(pointerId);
          group.classList.add('dragging');

          // Capture the current position relative to the group before going
          // absolute, so the button doesn't jump on the first move.
          const groupRect = group.getBoundingClientRect();
          const toggleRect = toggle.getBoundingClientRect();
          baseX = toggleRect.left - groupRect.left;
          baseY = toggleRect.top - groupRect.top;
          startX = event.clientX;
          startY = event.clientY;
        });

        panner.addEventListener('pointermove', (event) => {
          if (event.pointerId !== pointerId) return;
          // First movement lifts the button out of flow and reveals Reset.
          group.classList.add('moved');
          group.style.setProperty('--panner-x', `${baseX + event.clientX - startX}px`);
          group.style.setProperty('--panner-y', `${baseY + event.clientY - startY}px`);
        });

        const end = (event) => {
          if (event.pointerId !== pointerId) return;
          panner.releasePointerCapture(pointerId);
          pointerId = null;
          group.classList.remove('dragging');
        };

        panner.addEventListener('pointerup', end);
        panner.addEventListener('pointercancel', end);

        // The panner lives inside the button, so swallow its activation: a tap
        // or drag on the panner should never fire the button's toggle.
        panner.addEventListener('click', (event) => {
          event.preventDefault();
          event.stopPropagation();
        });
      }
    },

);
</script><p class="old-safari-warning" hidden><span style="font-weight: bold; color: red;">Unfortunately you're using Safari 26.5 or lower</span>, which has a buggy implementation of anchor positioning, so <span style="font-weight: bold; color: red;">these demos won't work for you</span>. They work in Safari Technology Preview, so the techniques in this article should work by the time Safari ships custom select.</p><script>
  {
    const ua = navigator.userAgent;
    const isSafari =
      /Safari/.test(ua) && !/Chrome|Chromium|CriOS|Android/.test(ua);
    const versionMatch = ua.match(/Version\/(\d+)(?:\.(\d+))?/);

    // Technology Preview has the same UA string, but it supports base-select, so don't show the warning for it.
    const supportsBaseSelect = CSS.supports('appearance', 'base-select');

    if (isSafari && versionMatch && !supportsBaseSelect) {
      const major = Number(versionMatch[1]);
      const minor = Number(versionMatch[2] || 0);
      // 26.5 or lower
      if (major < 26 || (major === 26 && minor <= 5)) {
        document.querySelector('.old-safari-warning').hidden = false;
      }
    }
  }
</script><p>It isn&#39;t actually a custom select. Firefox and Safari are actively working on custom select, but haven&#39;t released it yet, so to make the demos work in more browsers, and to make it easier for you to inspect with DevTools, I&#39;ve built the demos from <a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API">popovers</a>, and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Anchor_positioning">CSS anchor positioning</a>, which are the same primitives custom select uses under the hood.</p>
<p>You can drag it around and see how it reacts to being in other parts of the viewport, and how it reacts to scrolling. If you can&#39;t be bothered with all that, here&#39;s a video:</p>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/874">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/1-av1-CXNyB3ut.mp4" />
    <source type="video/mp4" src="/c/1-avc-CHjZ2tdg.mp4" />
  </video>
</figure><p>Here are the default UA styles that impact the picker&#39;s position and height:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">inset</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">auto</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-inline-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">anchor-size</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">self-inline</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-area</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">self-block-end</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">span-self-inline-end</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-order</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">most-block-size</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">:</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">self-block-start</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">span-self-inline-end</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">self-block-end</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">span-self-inline-start</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">self-block-start</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">span-self-inline-start</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* Not part of the spec, but it's something Chrome does, so I've included it */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">lh;</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--48"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">inset</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">auto</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-inline-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">anchor-size</span><span style="color:#D4D4D4">(self-inline);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-area</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">self-block-end</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">span-self-inline-end</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-order</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">most-block-size</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">:</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">self-block-start</span><span style="color:#CE9178"> span-self-inline-end</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">self-block-end</span><span style="color:#CE9178"> span-self-inline-start</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">self-block-start</span><span style="color:#CE9178"> span-self-inline-start</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* Not part of the spec, but it's something Chrome does, so I've included it */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">lh;</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--56 code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">inset</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">auto</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-inline-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">anchor-size</span><span style="color:#D4D4D4">(self-inline);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-area</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">self-block-end</span><span style="color:#CE9178"> span-self-inline-end</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-order</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">most-block-size</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">:</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">self-block-start</span><span style="color:#CE9178"> span-self-inline-end</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">self-block-end</span><span style="color:#CE9178"> span-self-inline-start</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">self-block-start</span><span style="color:#CE9178"> span-self-inline-start</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* Not part of the spec, but it's something Chrome does, so I've included it */</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1</span><span style="color:#D4D4D4">lh;</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div></div><p>As a result:</p>
<ul>
<li><code>min-inline-size</code> means the picker will always be at least as wide as the <code>&lt;select&gt;</code> button (or toggle button in this case).</li>
<li><code>max-block-size</code> means the picker will not overflow the viewport. Its <code>stretch</code> size is the full anchor positioning cell (the area from the edge of the <code>&lt;select&gt;</code> button to the edge of the viewport).</li>
<li><code>position-area</code> defines the default anchor positioning cell to use, which is below the <code>&lt;select&gt;</code> button, and from its left edge to the right edge of the viewport.</li>
<li><code>position-try-fallbacks</code> defines fallbacks for the anchor positioning cell, so it can appear above the <code>&lt;select&gt;</code> button, and/or clamp to the button&#39;s right edge.</li>
<li><code>position-try-order</code> means the picker will initially appear in the anchor positioning cell that offers it the <code>most-block-size</code>, which means vertical space in this writing-mode. This doesn&#39;t currently work in Firefox (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2050547">ticket</a>) or Safari (<a href="https://bugs.webkit.org/show_bug.cgi?id=317916">ticket</a>), as it <a href="https://github.com/w3c/csswg-drafts/issues/13268#issuecomment-4801719311">wasn&#39;t clear in the spec</a>.</li>
</ul>
<p>This is a reasonable set of defaults, but I think there are a number of things we can do to improve the UX.</p>
<h2 id="prevent-the-picker-from-hitting-the-viewport-edge"><a href="#prevent-the-picker-from-hitting-the-viewport-edge">Prevent the picker from hitting the viewport edge</a></h2>
<p><toggle-picker data-picker-classes="ua-picker-styles select-picker"></toggle-picker></p>
<p>Right now the picker extends to the edge of the viewport, making it hard to tell if it&#39;s actually stopping there, or if it&#39;s overflowing the viewport. The only visual clue is the small border &amp; rounded corners.</p>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/874">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/1-av1-CXNyB3ut.mp4" />
    <source type="video/mp4" src="/c/1-avc-CHjZ2tdg.mp4" />
  </video>
</figure><p>Instead, I&#39;ll add a small margin:</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><p>Try it out:</p>
<p><toggle-picker data-picker-classes="ua-picker-styles select-picker margin-block-end"></toggle-picker></p>
<p>This isn&#39;t quite right, because:</p>
<ul>
<li>In Firefox, it simply isn&#39;t working.</li>
<li>In Chrome &amp; Safari, the margin is on the bottom, which looks bad when the picker flips above the button.</li>
</ul>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/872">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/2-av1-uXqGQ1Bg.mp4" />
    <source type="video/mp4" src="/c/2-avc-BTWGe3l0.mp4" />
  </video>
</figure><h3 id="fixing-firefox"><a href="#fixing-firefox">Fixing Firefox</a></h3>
<p>Remember when I said pickers have <code>max-block-size: stretch</code>? Well, Firefox doesn&#39;t support that, so I threw in <code>max-block-size: 100%</code> as a fallback. However, with percent heights, margins don&#39;t take away from the height, so the picker still hits the viewport edge, and the margin is outside it.</p>
<p>We can work around it:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#B5CEA8">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#B5CEA8">100%</span><span style="color:#D4D4D4"> - </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">--viewport-margin</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--48"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#B5CEA8">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#B5CEA8">100%</span><span style="color:#D4D4D4"> - </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--56 code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">100%</span><span style="color:#D4D4D4"> - </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div></div><p>Now, for Firefox, we&#39;re deducting the margin from the 100% <code>max-block-size</code>. For browsers that support <code>stretch</code>, we stick with the previous solution.</p>
<p>And here&#39;s the result:</p>
<p><toggle-picker data-picker-classes="ua-picker-styles select-picker margin-firefox-fix"></toggle-picker></p>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/874">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/3-av1-Ddhc9sZL.mp4" />
    <source type="video/mp4" src="/c/3-avc-Bttyx-6o.mp4" />
  </video>
</figure><p>It even does the right thing when the picker flips above the button! So… why am I not using this solution for the other browsers? Well, there&#39;s a slight imperfection with how the percent-based solution behaves. See if you can spot it - I&#39;ll come back to it later.</p>
<h3 id="fixing-chrome--safari"><a href="#fixing-chrome--safari">Fixing Chrome &amp; Safari</a></h3>
<p>We need to fix the margin when the picker flips above the button. Now, there&#39;s a feature called <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Anchor_positioning/Anchored_container_queries">anchored container queries</a> which lets us apply different styles when the anchored item flips position, but it isn&#39;t supported in Safari. Thankfully, there&#39;s an even better solution that Safari <em>does</em> support. Watch this…</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40"><pre class="shiki dark-plus has-diff" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#B5CEA8">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#B5CEA8">100%</span><span style="color:#D4D4D4"> - </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line diff add"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">: </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block, </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-inline, </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block flip-inline; </span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">--viewport-margin</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--48"><pre class="shiki dark-plus has-diff" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#B5CEA8">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#B5CEA8">100%</span><span style="color:#D4D4D4"> - </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line diff add"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">: </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block, </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-inline, </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block flip-inline; </span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--56 code-variant--64 code-variant--72"><pre class="shiki dark-plus has-diff" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">100%</span><span style="color:#D4D4D4"> - </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">));</span></span></span><span class="line diff add"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">: </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block, </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-inline, </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block flip-inline; </span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div></div><p>This replaces the UA default <code>position-try-fallbacks</code>, which were specific about the positioning, with these <code>flip-*</code> values that achieve the same thing. However, the <code>flip-*</code> values come with <em>dark magic</em>.</p>
<p>When the flips take effect, it tries to flip other styles too. This works with some properties, but not others. <a href="https://drafts.csswg.org/css-anchor-position-1/#execute-a-try-tactic">Here&#39;s the spec</a>, good luck!</p>
<p>Margins are among the things it does work for, so when the picker flips above the button, our <code>margin-block-end</code> is treated as a <code>margin-block-start</code>. Spooky, yet convenient!</p>
<p>Here&#39;s the result:</p>
<p><toggle-picker data-picker-classes="ua-picker-styles select-picker margin-full-fix"></toggle-picker></p>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/872">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/4-av1-Cy3gA8Gl.mp4" />
    <source type="video/mp4" src="/c/4-avc-tG07Ckdu.mp4" />
  </video>
</figure><p><a href="https://github.com/w3c/csswg-drafts/issues/13541#issuecomment-4732677022">The CSS Working Group has resolved to change the <code>position-try-fallbacks</code> defaults</a> for select pickers to something similar to the above, so the above override won&#39;t be needed in future.</p>
<p>Anyway, that&#39;s that problem sorted, but we still have work to do.</p>
<h2 id="prevent-the-picker-from-getting-too-small"><a href="#prevent-the-picker-from-getting-too-small">Prevent the picker from getting too small</a></h2>
<p>If you open the picker and drag it to the viewport edge, it gets really really small - unusably small, before it flips position. Chrome sets a default <code>min-block-size</code> of <code>1lh</code>, so let&#39;s just make that bigger!</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><p><toggle-picker data-picker-classes="ua-picker-styles select-picker margin-full-fix min-size"></toggle-picker></p>
<p>But no, that creates another issue:</p>
<p><toggle-picker data-picker="select-picker-small" data-picker-classes="ua-picker-styles select-picker margin-full-fix min-size">Toggle small picker</toggle-picker></p>
<p>When the picker has only a few options, its full size is smaller than our minimum, so it looks kinda bad.</p>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/874">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/5-av1-Bjo6vmXk.mp4" />
    <source type="video/mp4" src="/c/5-avc-CaUkQEvj.mp4" />
  </video>
</figure><p>What we want is for our minimum size to be like <code>min(fit-content, 12em)</code>, but <code>min()</code> doesn&#39;t allow intrinsic sizes like <code>fit-content</code>. Enter <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/calc-size"><code>calc-size()</code></a> - this was the bit <a href="https://github.com/w3c/csswg-drafts/issues/13617#issuecomment-4025033386">Ian Kilpatrick unlocked for me</a>:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40 code-variant--48 code-variant--56"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div></div><p><code>calc-size()</code> lets us state an intrinsic size in the first argument, then perform a calculation with it in the second argument, where the <code>size</code> keyword represents the intrinsic size. Yeah, it&#39;s a little weird, but it works! Well, it works in Chrome. It isn&#39;t yet supported in Firefox (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=calc-size">ticket</a>) or Safari (<a href="https://bugs.webkit.org/show_bug.cgi?id=274177">ticket</a>), so we can use a bit of a hack in the meantime:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* The calc-size way… */</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* The hacky fallback… */</span></span></span><span class="line"><span class="indent"><span style="color:#C586C0">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#C586C0">@supports</span><span style="color:#D4D4D4"> not (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">&#x26;</span><span style="color:#D7BA7D">:not</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">option:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">4</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">optgroup:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--48 code-variant--56"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* The calc-size way… */</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* The hacky fallback… */</span></span></span><span class="line"><span class="indent"><span style="color:#C586C0">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#C586C0">@supports</span><span style="color:#D4D4D4"> not (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">&#x26;</span><span style="color:#D7BA7D">:not</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">option:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">4</span><span style="color:#D4D4D4">))),</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">optgroup:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">)))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--64"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* The calc-size way… */</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* The hacky fallback… */</span></span></span><span class="line"><span class="indent"><span style="color:#C586C0">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#C586C0">@supports</span><span style="color:#D4D4D4"> not (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">&#x26;</span><span style="color:#D7BA7D">:not</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">option:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">4</span><span style="color:#D4D4D4">))),</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">optgroup:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">)))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* The calc-size way… */</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">))) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">)));</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#6A9955">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#6A9955">/* The hacky fallback… */</span></span></span><span class="line"><span class="indent"><span style="color:#C586C0">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#C586C0">@supports</span><span style="color:#D4D4D4"> not (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">&#x26;</span><span style="color:#D7BA7D">:not</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">option:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">4</span><span style="color:#D4D4D4">))),</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">optgroup:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">)))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div></div><p>Ok, that&#39;s a lot. Here&#39;s what it&#39;s doing:</p>
<ol>
<li>Set a minimum block size of <code>12em</code> from <code>--min-size</code>.</li>
<li>If <code>calc-size()</code> is supported, use it as before.</li>
<li>Otherwise, if the picker has fewer than 4 options and fewer than 2 optgroups, remove the minimum block size, and prevent it from shrinking when it hits the edge of the viewport.</li>
</ol>
<p>And here&#39;s the result:</p>
<p><toggle-picker data-picker-classes="ua-picker-styles select-picker margin-full-fix min-size-fix"></toggle-picker></p>
<p><toggle-picker data-picker="select-picker-small" data-picker-classes="ua-picker-styles select-picker margin-full-fix min-size-fix">Toggle small picker</toggle-picker></p>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/874">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/6-av1-BpG94YdS.mp4" />
    <source type="video/mp4" src="/c/6-avc-BNjWLe8K.mp4" />
  </video>
</figure><h2 id="prevent-the-picker-from-getting-too-big"><a href="#prevent-the-picker-from-getting-too-big">Prevent the picker from getting too big</a></h2>
<p>The last issue to tackle is preventing the picker from getting too <em>big</em>. Right now, it will always grow to fill the anchor positioning cell, which can end up feeling too tall. To solve this, we set a maximum.</p>
<p>However, we already used <code>max-block-size</code> to stop the picker hitting the edge of the viewport, so we need to use <code>min()</code> to allow for two max-sizes. One of the max sizes is <code>max-block-size: stretch</code>, so we need to use <code>calc-size()</code> again, which allows the intrinsic <code>stretch</code> size to be used in the <code>min()</code> calculation.</p>
<div class="code-example code-example--shiki"><pre class="shiki dark-plus has-diff" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line diff add"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">30em</span><span style="color:#D4D4D4">; </span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line diff remove"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">calc</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">100%</span><span style="color:#D4D4D4"> - </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">)); </span></span></span><span class="line diff add"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">100%</span><span style="color:#9CDCFE"> -</span><span style="color:#DCDCAA"> var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">), </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">)); </span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">:</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-inline,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block flip-inline;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line diff remove"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">) { </span></span></span><span class="line diff add"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">))) { </span></span></span><span class="line diff remove"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">; </span></span></span><span class="line diff add"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">))); </span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><p>And here&#39;s the final result:</p>
<p><toggle-picker data-picker-classes="ua-picker-styles select-picker full-solution"></toggle-picker></p>
<p><toggle-picker data-picker="select-picker-small" data-picker-classes="ua-picker-styles select-picker full-solution">Toggle small picker</toggle-picker></p>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/940">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/7-av1-BkpJ_nbL.mp4" />
    <source type="video/mp4" src="/c/7-avc-BsKt9xTR.mp4" />
  </video>
</figure><p>Because we&#39;re using <code>calc-size()</code> for the fix, which isn&#39;t supported in Safari, Safari is now using the <code>100%</code> fallback as well as Firefox, which is <em>almost perfect</em>, but not quite. Have you spotted the imperfection? Here&#39;s the issue:</p>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/874">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/8-av1-DrCf40eg.mp4" />
    <source type="video/mp4" src="/c/8-avc-ZaH3wfNk.mp4" />
  </video>
</figure><p>Once we get to the minimum height, the picker will move towards the edge of the viewport before flipping, whereas in Chrome which uses <code>calc-size()</code> + <code>stretch</code>, it flips as soon as it hits the minimum height. It&#39;s a minor thing, but it&#39;ll be nicer when all browsers support <code>calc-size()</code>.</p>
<h2 id="putting-it-all-together"><a href="#putting-it-all-together">Putting it all together</a></h2>
<p>Here&#39;s the full CSS for the picker, which adds the margin to the viewport, applies a minimum size, and a maximum size, all in one place to copy-paste and for LLMs to steal:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">30em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#B5CEA8">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#B5CEA8">100%</span><span style="color:#9CDCFE"> -</span><span style="color:#DCDCAA"> var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">:</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-inline,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block flip-inline;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">--viewport-margin</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#C586C0">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#C586C0">@supports</span><span style="color:#D4D4D4"> not (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">&#x26;</span><span style="color:#D7BA7D">:not</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">option:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">4</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">optgroup:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--48 code-variant--56"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">30em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#B5CEA8">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#B5CEA8">100%</span><span style="color:#9CDCFE"> -</span><span style="color:#DCDCAA"> var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">:</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-inline,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block flip-inline;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#C586C0">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#C586C0">@supports</span><span style="color:#D4D4D4"> not (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">        </span></span><span class="content"><span class="copy-indent">        </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">&#x26;</span><span style="color:#D7BA7D">:not</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">option:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">4</span><span style="color:#D4D4D4">))),</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">optgroup:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">)))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--64"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">30em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#B5CEA8">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#B5CEA8">100%</span><span style="color:#9CDCFE"> -</span><span style="color:#DCDCAA"> var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">),</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">:</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-inline,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block flip-inline;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#C586C0">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#C586C0">@supports</span><span style="color:#D4D4D4"> not (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">&#x26;</span><span style="color:#D7BA7D">:not</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">option:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">4</span><span style="color:#D4D4D4">))),</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">optgroup:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">)))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">30em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">100%</span><span style="color:#9CDCFE"> -</span><span style="color:#DCDCAA"> var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">), </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">position-try-fallbacks</span><span style="color:#D4D4D4">:</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-inline,</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">flip-block flip-inline;</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">@supports (</span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">))) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--min-size</span><span style="color:#D4D4D4">)));</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--max-size</span><span style="color:#D4D4D4">)));</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#DCDCAA">var</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">--viewport-margin</span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"></span></span><span class="line"><span class="indent"><span style="color:#C586C0">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#C586C0">@supports</span><span style="color:#D4D4D4"> not (</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">1px</span><span style="color:#D4D4D4">))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">&#x26;</span><span style="color:#D7BA7D">:not</span><span style="color:#D4D4D4">(</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">option:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">4</span><span style="color:#D4D4D4">))),</span></span></span><span class="line"><span class="indent"><span style="color:#D7BA7D">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#D7BA7D">:has</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">:where</span><span style="color:#D4D4D4">(</span><span style="color:#D7BA7D">optgroup:nth-of-type</span><span style="color:#D4D4D4">(</span><span style="color:#B5CEA8">2</span><span style="color:#D4D4D4">)))</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">0</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">      </span></span><span class="content"><span class="copy-indent">      </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">}</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div></div><p>And one last time:</p>
<p><toggle-picker data-picker-classes="ua-picker-styles select-picker full-solution"></toggle-picker></p>
<p><toggle-picker data-picker="select-picker-small" data-picker-classes="ua-picker-styles select-picker full-solution">Toggle small picker</toggle-picker></p>
<figure>
  <video controls muted style="width: 100%; display: block; aspect-ratio: 1358/940">
    <source type="video/webm; codecs=av01.0.08M.08.0.110.01.01.01.1" src="/c/7-av1-BkpJ_nbL.mp4" />
    <source type="video/mp4" src="/c/7-avc-BsKt9xTR.mp4" />
  </video>
</figure><p>This doesn&#39;t work in the current version of Safari (26.5), but it does work in Safari Technology Preview, so it should be supported by the time Safari ships custom select.</p>
<p>If the CSS above looks scary, it&#39;s mostly due to fallbacks for things browsers don&#39;t support yet, and a workaround for something the CSS Working Group will change. Eventually we&#39;ll be able to do the same thing with:</p>
<div class="code-example code-example--shiki"><div class="code-variant code-variant--base code-variant--40 code-variant--48"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">30em</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--56"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span></span></span><span class="line"><span class="indent"><span style="color:#CE9178">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">,</span></span></span><span class="line"><span class="indent"><span style="color:#DCDCAA">    </span></span><span class="content"><span class="copy-indent">    </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">)</span></span></span><span class="line"><span class="indent"><span style="color:#D4D4D4">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#D4D4D4">);</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">30em</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div><div class="code-variant code-variant--64 code-variant--72"><pre class="shiki dark-plus" style="background-color:#1E1E1E;color:#D4D4D4" tabindex="0"><code><span class="line"><span class="indent"></span><span class="content"><span style="color:#D7BA7D">.custom-select</span><span style="color:#D4D4D4">::picker(</span><span style="color:#D7BA7D">select</span><span style="color:#D4D4D4">) {</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">min-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">fit-content</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">12em</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">max-block-size</span><span style="color:#D4D4D4">: calc-size(</span><span style="color:#CE9178">stretch</span><span style="color:#D4D4D4">, </span><span style="color:#DCDCAA">min</span><span style="color:#D4D4D4">(</span><span style="color:#9CDCFE">size</span><span style="color:#D4D4D4">, </span><span style="color:#B5CEA8">30em</span><span style="color:#D4D4D4">));</span></span></span><span class="line"><span class="indent"><span style="color:#9CDCFE">  </span></span><span class="content"><span class="copy-indent">  </span><span style="color:#9CDCFE">margin-block-end</span><span style="color:#D4D4D4">: </span><span style="color:#B5CEA8">1em</span><span style="color:#D4D4D4">;</span></span></span><span class="line"><span class="indent"></span><span class="content"><span style="color:#D4D4D4">}</span></span></span></code></pre></div></div><h2 id="so-everythings-great"><a href="#so-everythings-great">So, everything&#39;s great?</a></h2>
<p>Well, not quite. The way the picker reacts to scrolling seems weird to me, and it&#39;s inconsistent across browsers and devices. <a href="https://github.com/w3c/csswg-drafts/issues/14112">I&#39;ve filed a CSSWG issue to discuss it</a>, because none of the behaviours seem right to me.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Importing vs fetching JSON]]></title>
        <id>https://jakearchibald.com/2025/importing-vs-fetching-json/</id>
        <link href="https://jakearchibald.com/2025/importing-vs-fetching-json/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-WUSWKqXB.png" type="image/png"/>
        <updated>2025-10-22T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[They behave differently, so make sure you pick the right one.]]></summary>
        <content type="html"><![CDATA[<p>This year, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with">JSON module imports became baseline &#39;newly available&#39;</a>, meaning they&#39;re implemented across browser engines.</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">import</span> data <span class="token keyword">from</span> <span class="token string">'./data.json'</span> <span class="token keyword">with</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>

<span class="token comment">// And…</span>

<span class="token keyword">const</span> <span class="token punctuation">{</span> <span class="token keyword">default</span><span class="token operator">:</span> data <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./data.json'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token keyword">with</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <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><p>I&#39;m glad JavaScript has this feature, but I can&#39;t see myself using it in a browser environment, other than small demos. I might still use it in frontend source code, but generally only in cases where it&#39;d be bundled away before reaching the browser.</p>
<p>It comes down to the behaviour differences compared to this:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'./data.json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Here&#39;s why…</p>
<h2 id="error-handling"><a href="#error-handling">Error handling</a></h2>
<p>With a static import:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">import</span> data <span class="token keyword">from</span> <span class="token string">'./data.json'</span> <span class="token keyword">with</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre></div><p>If the above fails, it takes the whole module graph down with it. Because of this, I&#39;d never use this pattern with some third-party JSON, as I&#39;d want to be able to provide a fallback if the third-party service fails.</p>
<p>But <code>import()</code> allows exactly that:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">try</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> <span class="token punctuation">{</span> <span class="token keyword">default</span><span class="token operator">:</span> data <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token punctuation">{</span>
    <span class="token keyword">with</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <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>
<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">// Fallback logic</span>
<span class="token punctuation">}</span></code></pre></div><p>This is pretty good. Although the <code>fetch()</code> alternative:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">try</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'./data.json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</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">// Fallback logic</span>
<span class="token punctuation">}</span></code></pre></div><p>…allows much more introspection in event of a failure. There&#39;s <code>response.status</code>, or you can use <code>response.text()</code>, meaning you still have the source if JSON parsing fails. Maybe that doesn&#39;t matter in all cases.</p>
<p>I think the bigger issue is…</p>
<h2 id="caching-and-garbage-collection"><a href="#caching-and-garbage-collection">Caching and garbage collection</a></h2>
<p>When you import a module (be it JS, WASM, CSS, or JSON), it&#39;s cached for the lifetime of the environment (e.g. a page or worker), even if the result is a network or parsing failure. All imports for a given specifier &amp; type return the same module.</p>
<p>This is generally a good thing, as it means your JS module returns the same objects every time, and state can be shared across all importers. But if you&#39;re doing something like:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> <span class="token punctuation">{</span> <span class="token keyword">default</span><span class="token operator">:</span> results <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'/api/search?q=whatever'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token keyword">with</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <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><p>…then you have a memory leak, because each set of search results will live in the module graph for the life of the page. That isn&#39;t the case with <code>fetch()</code>, where returned objects can be garbage collected once they&#39;re out of reference.</p>
<p>The same applies to cases like:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">let</span> <span class="token punctuation">{</span> <span class="token keyword">default</span><span class="token operator">:</span> largeData <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'/large-data.json'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token keyword">with</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span> <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>

<span class="token keyword">const</span> someSmallPart <span class="token operator">=</span> largeData<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
largeData <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span></code></pre></div><p>If the above used <code>fetch()</code>, then the data other than <code>someSmallPart</code> could be garbage collected. But with <code>import()</code>, the whole <code>largeData</code> object remains in memory for the life of the page.</p>
<h2 id="when-should-json-modules-be-used"><a href="#when-should-json-modules-be-used">When should JSON modules be used?</a></h2>
<p>It makes sense to use JSON module imports for local static JSON resources where you need all/most of the data within. Particularly since bundlers can understand JSON imports, and bundle the object with other modules. That isn&#39;t possible with <code>fetch()</code>, unless you use some pretty hacky plugins.</p>
<p>In server code, I might import <code>package.json</code> to get the version number. However, I wouldn&#39;t do this with frontend code, as it&#39;s wasteful to bundle all of <code>package.json</code> just to get a single value – bundlers don&#39;t perform tree-shaking of individual object keys.</p>
<p><strong>Update:</strong> <a href="https://mastodon.social/@jed/115418637695312552">Jed</a> and <a href="https://bsky.app/profile/uncenter.dev/post/3m3sb5ihaks2g">leah</a> point out, many bundlers (esbuild, Vite, and Rollup at least) will expose top level keys as individual exports if you use their non-standard JSON import syntax.</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// No treeshaking</span>
<span class="token keyword">import</span> data <span class="token keyword">from</span> <span class="token string">'./package.json'</span> <span class="token keyword">assert</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</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>data<span class="token punctuation">.</span>version<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// No treeshaking</span>
<span class="token keyword">import</span> data <span class="token keyword">from</span> <span class="token string">'./package.json'</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>data<span class="token punctuation">.</span>version<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Treeshaking works!</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> version <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./package.json'</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>version<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>This only tree-shakes the top-level keys, so if your data is deeply nested, you&#39;re still bundling more than you need. Generally, I try to turn any &quot;fetch-and-process&quot; logic into a Vite/Rollup plugin, so it happens at build time rather than runtime.</p>
<p>I&#39;m glad native JSON importing feature exists, but it should be used with care, and not as a blanket replacement for <code>fetch()</code>ing JSON.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[The present and potential future of progressive image rendering]]></title>
        <id>https://jakearchibald.com/2025/present-and-future-of-progressive-image-rendering/</id>
        <link href="https://jakearchibald.com/2025/present-and-future-of-progressive-image-rendering/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-ClfgDFJb.jpg" type="image/jpg"/>
        <updated>2025-10-15T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[Exploring progressive image rendering across JPEG, PNG, WebP, AVIF, and JPEG XL.]]></summary>
        <content type="html"><![CDATA[<p>Progressive image formats allow the decoder to create a partial rendering when only part of the image resource is available. Sometimes it&#39;s part of the image, and sometimes it&#39;s a low quality/resolution version of the image. I&#39;ve been digging into it recently, and I think there are some common misconceptions, and I&#39;d like to see a more pragmatic solution from AVIF.</p>
<p>Let&#39;s dive straight into the kinds of progressive rendering that are currently available natively in browsers.</p>
<h2 id="the-current-state-of-progressive-image-rendering-in-browsers"><a href="#the-current-state-of-progressive-image-rendering-in-browsers">The current state of progressive image rendering in browsers</a></h2>
<p>Here&#39;s the image I&#39;m going to use for comparison, and you&#39;ll be sick of seeing it by the end of the article:</p>
<figure class="full-figure max-figure">
  <img src="/c/fox-DSbxk4jA.avif" loading="lazy" width="1598" height="1753" alt="A winter photo of a fox, standing on grass, with foliage in the background" style="height:auto" />
</figure><p>Due to its high resolution (1598x1753), and sharp detail, it&#39;s 155 kB, even using AVIF.</p>
<p>I created similarly file-sized versions of the image in other formats to compare when they start to render, and when they show enough detail to identify it as a fox in woodland</p>
<p>Let&#39;s go!</p>
<h3 id="jpeg"><a href="#jpeg">JPEG</a></h3>
<p>Trusty old JPEG can be encoded in a progressive way, which is the default in <a href="https://squoosh.app/">Squoosh</a>.</p>
<p><a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/">Here&#39;s a little web app</a> that lets you test progressive rendering of any image, and here&#39;s the <a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox.jpg&density=2">app ready-loaded with the JPEG version of the fox image</a>.</p>
<p>You get a top-to-bottom low resolution render that starts around 1.2 kB in, and it finishes its first pass at around 33 kB (21% of the file size). Interestingly, that looks different in Firefox and Chromium-based browsers compared to Safari:</p>
<figure class="full-figure max-figure">
  <img src="/c/jpeg-safari-vs-Br8Dbgrp.avif" loading="lazy" width="1598" height="1721" alt="The previous fox image, partially loaded. In Safari it appears blocky, in Firefox and Chromium it appears smooth" style="height:auto" />
</figure><p>In both cases you can tell what the image is, but the Safari rendering is blocky, whereas Firefox and Chromium are smoothed. I much prefer the Firefox/Chromium rendering.</p>
<p>After that, the resolution is improved over multiple passes.</p>
<p>With JPEG, progressive rendering seems to be free in terms of quality/size, but it isn&#39;t free in terms of decoding time. <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/">Here&#39;s another little web app</a>, this one benchmarks image decoding in the browser. Comparing <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox.jpg">progressive JPEG</a> with <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox-no-progressive.jpg">non-progressive JPEG</a>, the progressive JPEG takes around 40% longer to decode, but on my M4 pro that&#39;s only about 1.3ms, which seems like a reasonable trade-off.</p>
<h3 id="png"><a href="#png">PNG</a></h3>
<p>PNG generally renders from top to bottom. There&#39;s an interlaced mode, but it significantly increases the file size, so I don&#39;t recommend it.</p>
<p>For completion, <a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox-interlaced.png&density=2">here&#39;s a demo</a>. I had to… umm… modify the image somewhat, in order to bring the file size down to some acceptable level.</p>
<h3 id="webp"><a href="#webp">WebP</a></h3>
<p>In Firefox and Chrome, WebP renders from top to bottom. It also takes around 43 kB (28%) before it renders anything. I guess the start of the file contains the colour data, so it can only render progressively as it starts receiving the final luma channel.</p>
<p>Unfortunately, Safari doesn&#39;t perform any progressive rendering of WebP. It doesn&#39;t render anything until it has the whole file.</p>
<p><a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox.webp&density=2">Here&#39;s a demo</a>.</p>
<h3 id="avif"><a href="#avif">AVIF</a></h3>
<p><strong>Update:</strong> Since I posted this article, progressive support in AVIF has been massively improved. You can now provide progressive up to three progressive frames, processed in whatever way you want, and fractionally scaled. I intend to post more on this in future.</p>
<p>Regular AVIF doesn&#39;t support any kind of progressive rendering. You get nothing, then you get the whole image (<a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox.avif&density=2">demo</a>).</p>
<p>But, AVIF does have a little-known progressive rendering feature! I couldn&#39;t get good results with the <code>--progressive</code> flag it offers, but I got something interesting with the lower level <code>--layered</code> flag. It&#39;s experimental, and I had to compile libavif myself to get it working. Here&#39;s the command I used:</p>
<div class="code-example"><pre>avifenc -s 0 -y 420 --layered \
  --scaling-mode:u 1/8 -q:u 0 fox.png \
  --scaling-mode:u 1 -q:u 16 fox.png \
  fox-progressive.avif</pre></div><p>This creates two layers, one at 1/8th resolution and &#39;minimum&#39; quality, and one at full resolution and good quality. These layers work similarly to frames of a video, with the second layer using data from the first to save space (a P-frame in video terms).</p>
<p><a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox-progressive.avif&density=2">Here&#39;s a demo</a>. It only progressively renders in Chromium browsers. You get two renders: the full image when all the data has been received, and an earlier render at ~5.8 kB (4% of the data):</p>
<style>
.async-content {
  position: relative;
}
@keyframes async-content-fade-in {
  from { opacity: 0; animation-timing-function: ease-in-out; }
}
.async-content-loading {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, .62);
  display: flex;
  align-items: center;
  justify-content: center;
  animation: 300ms async-content-fade-in;
}
.async-content-loading loading-spinner {
  --color: #fff;
}
</style><style>
.tabs-component {
  display: flex;
  flex-flow: row wrap;
}
.tabs-component-tab {
  flex: 1;
  min-width: 70px;
  display: grid;
}
.tabs-component input[type=radio] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.tabs-component-label {
  border-top: 7px solid #ffe454;
  padding: 0.7em 0.7em;
  text-align: center;
  cursor: pointer;
  line-height: 1.3;
  font-size: 0.9rem;
}
input[type=radio]:checked + .tabs-component-label {
  background: #ffe454;
}
input[type=radio]:focus-visible + .tabs-component-label {
  background: #b9b9b9;
}
input[type=radio]:focus-visible:checked + .tabs-component-label {
  background: #ffc254;
}
</style><style>
.image-tabs {
  position: relative;
}
.image-tabs-pop-out-icon {
  position: absolute;
  top: 10px;
  right: 10px;
  fill: #fff;
  width: 32px;
  filter: drop-shadow(0px 2px 3px rgba(0, 0, 0, 0.7));
}
.image-tabs-preview {
  position: relative;
  overflow: hidden;
  background: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path d="M1 2V0h1v1H0v1z" fill-opacity=".025"/></svg>');
  background-size: 20px 20px;
}
.image-tabs-background {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0 auto;
}
.image-tabs-transformer {
  position: relative;
}
.image-tabs-transformer img,
.image-tabs-transformer canvas {
  position: absolute;
  top: 0;
  left: 50%;
  width: 100%;
  height: 100%;
  margin: 0 auto;
  transform: translateX(-50%);
}
</style><figure class="full-figure max-figure">
<div class="post-component-2"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="0"/><span class="tabs-component-label">5.8 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">Full image (151 kB)</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-ERumC6T-.js';import Component from '/c/ImageTabs-niMRUh-e.js';render(h(Component, {"ratio":0.91158015,"initial":0,"images":[["5.8 kB","/c/initial-pass-Dx0jAW22.avif"],["Full image (151 kB)","/c/fox-progressive-CU6pVNDH.avif"]]}), document.querySelector('.post-component-2'));</script>
</figure><p>It looks… bad, but you can tell what it is, and it&#39;s pretty good for ~5.8 kB.</p>
<p>The downside is, something odd happens with the quality option when you use <code>--layered</code> mode. It doesn&#39;t equate to the same numbers as normal AVIF encoding.</p>
<p>Also, there seems to be some overhead in terms of file size, but it&#39;s hard to measure. From playing around with MS-SSIM, it seems to be in the region of 5-6 kB, so… the size of the initial pass.</p>
<p>The configuration of layers I used is just one example, but it seems pretty limited. Only particular scaling values are allowed, and 1/8 is the smallest. Supposedly, additional layers are possible, allowing for extra steps in the progressive render, but whenever I tried this, the encoder would error out, or explode the file size to ~400 kB, even at lowest quality. I guess that&#39;s why it&#39;s marked &#39;experimental&#39;.</p>
<p>In terms of decoding time, here&#39;s <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox-progressive.avif">progressive AVIF</a> vs <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox.avif">regular AVIF</a>. In Firefox and Safari, the decoding time for progressive is about 1-2% longer. In Chrome it takes ~40% longer, which is 3.5ms on my M4 Pro. My guess is that Chrome is missing a fast-path for cases where it doesn&#39;t need to render the first layer.</p>
<p>Here&#39;s <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1712813">the issue for tracking progressive rendering of AVIF in Firefox</a>.</p>
<h3 id="jpeg-xl"><a href="#jpeg-xl">JPEG XL</a></h3>
<p>If you ask web developers why they want JPEG XL (as I did on <a href="https://bsky.app/profile/jakearchibald.com/post/3m2mcuqjekc2a">bluesky</a> and <a href="https://mastodon.social/@jaffathecake/115333326583200558">mastodon</a>), almost everyone mentions progressive rendering. Given that, I was surprised that Safari (the only browser to support JPEG XL) doesn&#39;t perform any kind of progressive rendering. <a href="https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox-progressive.jxl&density=2">Here&#39;s a demo</a>, and <a href="https://bugs.webkit.org/show_bug.cgi?id=272350">a bug report</a>.</p>
<p>I was also surprised to see that, in Safari, <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox.jxl">JPEG XL takes 150% longer</a> (as in 2.5x) to decode vs <a href="https://random-stuff.jakearchibald.com/apps/img-decode-bench/?demo=fox.avif">an equivalent AVIF</a>. That&#39;s 17ms longer on my M4 Pro. Apple hardware tends to be high-end, but this could still be significant. This isn&#39;t related to progressive rendering; the decoder is just slow. <a href="https://www.reddit.com/r/jpegxl/comments/1djcccj/comment/l9at8p3/">There&#39;s some suggestion</a> that the Apple implementation is running on a single core, so maybe there&#39;s room for improvement.</p>
<p>JPEG XL support in Safari actually comes from the underlying OS rather than the browser. My guess is that Apple is considering using JPEG XL for iPhone photo storage rather than HEIC, and JPEG XL&#39;s inclusion in the browser is a bit of an afterthought. I&#39;m just guessing though.</p>
<p>The implementation that was in Chromium behind a flag <em>did</em> support progressive rendering to some degree, but it didn&#39;t render anything until ~60 kB (39% of the file). The rendering is similar to the initial JPEG rendering above, but takes much more image data to get there. This is a weakness in the decoder rather than the format itself. I&#39;ll dive into what JPEG XL is capable of shortly.</p>
<p>I also tested the performance of the old behind-a-flag Chromium JPEG XL decoder, and it&#39;s over 500% slower (6x) to decode than AVIF. The old behind-a-flag Firefox JPEG XL decoder is about as slow as the Safari decoder. It&#39;s not fair to judge the performance of experimental unreleased things, but I was kinda hoping one of these would suggest that the Safari implementation was an outlier.</p>
<p>I thought that &quot;fast decoding&quot; was one of the selling points of JPEG XL over AVIF, but now I&#39;m not so sure.</p>
<p><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1986393">We have a Rust implementation of JPEG XL underway in Firefox</a>, but performance needs to get a lot better before we can land it.</p>
<h2 id="but-do-we-actually-benefit-from-progressive-rendering"><a href="#but-do-we-actually-benefit-from-progressive-rendering">But do we actually benefit from progressive rendering?</a></h2>
<figure class="full-figure max-figure">
  <img src="/c/f1-DK8ANwoR.avif" loading="lazy" width="1598" height="1478" alt="A old Williams F1 car speeding up a hill" style="height:auto" />
</figure><p>Thanks to modern image formats (AVIF in this case), the above image is 56.4 kB. That&#39;s pretty incredible for a more-than-HD image. The fox image is much larger because of its sharp details, but that isn&#39;t true for most images. This means that, for many images, there&#39;s little benefit to progressive rendering.</p>
<p>But even with the 155 kB fox image, <a href="https://mastodon.social/@kornel/115339016909614657">Kornel makes a good point</a>:</p>
<blockquote class="quote"><p>Congestion and bufferbloat make data arrive in laggy bursts rather than slowly. Very bad signal strength also tends to be on/off.</p></blockquote><p>This means that even if it takes 5 seconds to receive the image, it&#39;s unlikely that 155 kB will be received gradually throughout that period. I&#39;m currently on aeroplane WiFi, and yeah… most of the slowness is while <em>nothing</em> is being received.</p>
<p>That means seeing a progressive render requires a particular mix of good and bad luck:</p>
<ul>
<li>Bad luck: your connection isn&#39;t great, so it&#39;s taking a while to fetch the whole image.</li>
<li>Good luck: a portion of the image has been received.</li>
</ul>
<p>Progressive rendering is a good experience when you hit this (not-)sweet spot, but you might not get to see it even on slow connections. It certainly isn&#39;t an alternative to taking care over your image sizes.</p>
<p>Because of this, it feels like encoding an image in a way that enables a progressive render should be minimal cost in terms of file size overhead, and minimal/zero cost in decoding overhead in cases where progressive isn&#39;t needed.</p>
<p>I know that&#39;s a weak &#39;story&#39;. When I set about writing this article, I intended it to be a strong argument for progressive rendering. But after digging into it, my feelings are less certain.</p>
<h2 id="what-about-progressive-rendering-instead-of-responsive-images"><a href="#what-about-progressive-rendering-instead-of-responsive-images">What about progressive rendering instead of responsive images?</a></h2>
<p>When I asked folks why they&#39;re interested in JPEG XL, some folks suggested it would enable them to replace responsive images with a single maximum-resolution JPEG XL file. The idea is:</p>
<ol>
<li>The browser receives some image data.</li>
<li>The browser performs a partial decode of the image data.</li>
<li>The browser realises it doesn&#39;t need any more pixel density.</li>
<li>The browser tells the server &quot;stop sending me this resource&quot;.</li>
<li>The server dutifully stops sending the resource.</li>
</ol>
<p>I&#39;m sorry to be the bad news guy, but I&#39;m pretty certain <em>this won&#39;t work</em>.</p>
<p>The problem is there&#39;s significant lag between each of these steps, to the point where it&#39;s very likely that you&#39;ll receive megabytes of data you don&#39;t need, per image, before the response is successfully cancelled.</p>
<p>It might be workable along with a new browser feature, where the ranges are known up-front, so the browser can make the required partial request:</p>
<div class="code-example"><pre class="language-html"><code><span class="token comment">&lt;!-- Made-up example --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span>
  <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>
    img.jxl#range=0-10000   200w,
    img.jxl#range=0-30000   400w,
    img.jxl#range=0-70000   800w,
    img.jxl#range=0-150000 1600w
  <span class="token punctuation">"</span></span>
  <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>…as usual…<span class="token punctuation">"</span></span>
<span class="token punctuation">/></span></span></code></pre></div><p>But even if we get this <code>#range=</code> feature (and there are some big questions around CORS that could make it a no-go), the complexity is about the same as responsive images today, and with responsive images you get more control over the available resolutions.</p>
<p>I guess an advantage of the &#39;range&#39; approach is the browser could &#39;resume&#39; the image download if it later finds it needs a higher resolution, such as going from thumbnail to full-screen. But even given that, current responsive images seem more versatile.</p>
<h2 id="potential-progressive-rendering-with-jpeg-xl"><a href="#potential-progressive-rendering-with-jpeg-xl">Potential progressive rendering with JPEG XL</a></h2>
<p>Aside from browser decoders, there&#39;s some promising work in progressive rendering of JPEG XL. However, it&#39;s unclear how much of it could make it into browsers, and the encoder settings are a little confusing.</p>
<p>The current version of cjxl (0.11.1) has a <code>--progressive</code> flag, but this creates a file that can&#39;t be rendered until a significant amount of the file has been received (39% of the file with the fox image).</p>
<p>To get an earlier render, you need to use <code>--progressive_dc 1</code>, which <code>cjxl --help</code> doesn&#39;t mention. You only get to find out about it if you use the verbose flag, twice: <code>cjxl --help -v -v</code>. It seems like this has been changed in the codebase, where <a href="https://github.com/libjxl/libjxl/commit/62712477eec764c4e730f94476113f6e4756dfb2"><code>--progressive</code> now implies <code>progressive_dc 1</code></a>, but that hasn&#39;t made it into a release yet.</p>
<p>It doesn&#39;t seem like <code>progressive_dc</code> has an impact on file size. If it does, it&#39;s small. I haven&#39;t seen a difference in decode time either (from testing in Safari), but JPEG XL decoding is slow in general, so it isn&#39;t a huge win.</p>
<p>djxl doesn&#39;t support the earlier rendering that <code>--progressive_dc</code> offers, but there&#39;s also a Rust-based decoder, <a href="https://github.com/tirr-c/jxl-oxide">jxl-oxide</a>, which can handle earlier rendering.</p>
<p>jxl-oxide isn&#39;t fast enough for browsers, but it&#39;s an interesting preview of what&#39;s possible. There&#39;s also a <a href="https://jxl-oxide.tirr.dev/demo/index.html">neat wasm version, that lets you see partial renders</a>. Here&#39;s a <a href="/c/fox-progressive-wBqOQbxQ.jxl">progressive version of the fox image</a>, if you want to try it yourself. Otherwise, here&#39;s a rough guide:</p>
<figure class="full-figure max-figure">
<div class="post-component-3"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="0"/><span class="tabs-component-label">2 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">3 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="2"/><span class="tabs-component-label">4.25 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="3"/><span class="tabs-component-label">6 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="4"/><span class="tabs-component-label">10 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="5"/><span class="tabs-component-label">17 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="6"/><span class="tabs-component-label">27 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="7"/><span class="tabs-component-label">46 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="8"/><span class="tabs-component-label">60 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="9"/><span class="tabs-component-label">68 kB</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-ERumC6T-.js';import Component from '/c/ImageTabs-niMRUh-e.js';render(h(Component, {"ratio":0.91158015,"initial":0,"images":[["2 kB","/c/2000-D6ESPIzI.avif"],["3 kB","/c/3000-B1joeRWz.avif"],["4.25 kB","/c/4250-YeB75_4X.avif"],["6 kB","/c/6000-CL0crpuE.avif"],["10 kB","/c/10000-032SbDmD.avif"],["17 kB","/c/17000-B4rt11Xh.avif"],["27 kB","/c/27000-Cl4PZSI1.avif"],["46 kB","/c/46000-Da3ZvuEo.avif"],["60 kB","/c/60000-Db0PUBzp.avif"],["68 kB","/c/68000-J7o32Hc6.avif"]]}), document.querySelector('.post-component-3'));</script>
</figure><p>I&#39;d say that the 6 kB mark gives you a decent impression of the image, and things get really clear around 46 kB. It could use some smoothing, like we saw with the JPEG progressive render in Firefox/Chrome.</p>
<p>After the 60 kB mark, the full detail appears in square blocks. A nice feature of JPEG XL is these can appear in any order, so you can see in the 68 kB render that sharpness appears around the fox&#39;s face before anywhere else. cjxl lets you specify a point to use as the &quot;center&quot;, but a smarter encoder could detect things like faces and prioritise them.</p>
<p>It&#39;s subjective, but I&#39;d say the 5.8 kB AVIF progressive render from earlier is, detail-wise, somewhere between the 17-27 kB JPEG XL renders, but make up your own mind:</p>
<figure class="full-figure max-figure">
<div class="post-component-4"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" value="0"/><span class="tabs-component-label">JXL 6 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">JXL 10 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="2"/><span class="tabs-component-label">JXL 17 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="3"/><span class="tabs-component-label">AVIF 5.8 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="4"/><span class="tabs-component-label">JXL 27 kB</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="5"/><span class="tabs-component-label">JXL 46 kB</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-ERumC6T-.js';import Component from '/c/ImageTabs-niMRUh-e.js';render(h(Component, {"ratio":0.91158015,"initial":3,"images":[["JXL 6 kB","/c/6000-CL0crpuE.avif"],["JXL 10 kB","/c/10000-032SbDmD.avif"],["JXL 17 kB","/c/17000-B4rt11Xh.avif"],["AVIF 5.8 kB","/c/initial-pass-Dx0jAW22.avif"],["JXL 27 kB","/c/27000-Cl4PZSI1.avif"],["JXL 46 kB","/c/46000-Da3ZvuEo.avif"]]}), document.querySelector('.post-component-4'));</script>
</figure><p>So if the browser manages to download 6 kB then stalls, then AVIF can produce a better result. But since the AVIF rendering is only two-pass, if the browser has say 50 kB, then the JPEG XL rendering is much better.</p>
<p>This is all just &#39;in theory&#39; until it becomes fast enough to land in a browser.</p>
<p>There&#39;s another JPEG XL rust decoder jxl-rs, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1986393">being developed for Firefox</a>. That team are also exploring how progressive rendering could work.</p>
<h2 id="a-better-progressive-feature-for-avif"><a href="#a-better-progressive-feature-for-avif">A better &#39;progressive&#39; feature for AVIF?</a></h2>
<p>This article got longer than I expected, but one last thing…</p>
<p><strong>Update:</strong> Since I posted this article, progressive support in AVIF has been massively improved. You can now provide progressive up to three progressive frames, processed in whatever way you want, and fractionally scaled. I intend to post more on this in future.</p>
<p>In my experience AVIF results in smaller files at what I&#39;d consider &#39;web quality&#39; compared to JPEG XL (particularly when <a href="/2021/serving-sharp-images-to-high-density-screens/">optimising for high-density</a>). And based on current implementations, AVIF decodes significantly quicker than JPEG XL. Smaller files and faster decode is a huge advantage, but that said, I&#39;d like AVIF to have a better &#39;progressive&#39; feature. It&#39;d be great if it supported an early render that allows more control over the quality and scale of the initial pass, and ideally a configurable post-process blur effect to hide compression artefacts.</p>
<p>Maybe the &#39;layers&#39; approach can be improved. Or maybe it&#39;s an over-complication. For example:</p>
<figure class="full-figure max-figure">
<div class="post-component-5"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="0"/><span class="tabs-component-label">With blur</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">Without blur</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="2"/><span class="tabs-component-label">Final image</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-ERumC6T-.js';import Component from '/c/ImageTabs-niMRUh-e.js';render(h(Component, {"ratio":0.91158015,"initial":0,"images":[["With blur","/c/blur-H6B46gPz.avif"],["Without blur","/c/fox-low-ZQiIxumG.avif"],["Final image","/c/fox-DSbxk4jA.avif"]]}), document.querySelector('.post-component-5'));</script>
</figure><p>The above image is 1.97 kB. It&#39;s scaled down, compressed with AVIF at minimum quality, and has a post-process blur applied.</p>
<p>With the blur applied, I think this works great as a preview. So could this… just go at the start of the full image file?</p>
<p>This means the &#39;preview&#39; is 100% overhead in terms of file size, but it also means as web developers, we can freely configure the quality of this &#39;preview&#39; by deciding how much overhead we find acceptable. Maybe I&#39;m being too conservative by only spending 1.97 kB, rather than, say, <span style="white-space: nowrap">10 kB</span>:</p>
<figure class="full-figure max-figure">
<div class="post-component-6"><div class="image-tabs"><div class="image-tabs-preview"><div class="async-content"><div class="image-tabs-transformer"><div class="image-tabs-sizer"><div style="padding-top:109.6996243281515%;"></div></div></div></div></div><form class="tabs-component"><label class="tabs-component-tab"><input name="tabs" type="radio" checked value="0"/><span class="tabs-component-label">With blur</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="1"/><span class="tabs-component-label">Without blur</span></label><label class="tabs-component-tab"><input name="tabs" type="radio" value="2"/><span class="tabs-component-label">Final image</span></label></form></div></div><script type="module">import { h, render } from '/c/preact-ERumC6T-.js';import Component from '/c/ImageTabs-niMRUh-e.js';render(h(Component, {"ratio":0.91158015,"initial":0,"images":[["With blur","/c/less-blur-Cpj0-TVK.avif"],["Without blur","/c/fox-lowish-BiwxtDNq.avif"],["Final image","/c/fox-DSbxk4jA.avif"]]}), document.querySelector('.post-component-6'));</script>
</figure><p>And because it&#39;s an entirely separate image, the decoding overhead is zero if the browser doesn&#39;t see a benefit to displaying the preview. As in, in cases where the browser has the whole file, it can just skip the preview.</p>
<p>The post-process blur would need to be part of the format, rather than left to something like CSS. This would allow the format to use a &#39;cheap&#39; blur filter, avoiding expensive rendering costs.</p>
<p><a href="https://github.com/AOMediaCodec/av1-avif/issues/102">I originally pitched this idea back in 2020</a> before the &#39;layers&#39; idea was explored. But maybe it&#39;s time to consider it again. Here&#39;s hoping!</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Fetch streams are great, but not for measuring upload/download progress]]></title>
        <id>https://jakearchibald.com/2025/fetch-streams-not-for-progress/</id>
        <link href="https://jakearchibald.com/2025/fetch-streams-not-for-progress/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-BGVsUjKW.png" type="image/png"/>
        <updated>2025-09-15T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[They&#39;re inaccurate, and there are better ways.]]></summary>
        <content type="html"><![CDATA[<p>Part of my role at Mozilla is making sure we&#39;re focusing on the right features, and we got onto the topic of fetch upload streams. It&#39;s something Chrome has supported for a while, but it isn&#39;t yet supported in either Firefox or Safari.</p>
<p>I asked folks on <a href="https://bsky.app/profile/jakearchibald.com/post/3lxws4wvgns27">various</a> <a href="https://mastodon.social/@jaffathecake/115140724791632506">social</a> <a href="https://x.com/jaffathecake/status/1963240536891895973">platforms</a> what they thought of the feature, and what they&#39;d use it for. The most common answer by far was &quot;to measure upload progress&quot;, but… using it for that will give inaccurate results, and may even lead to bad implementations in browsers.</p>
<p>Let&#39;s dig in…</p>
<h2 id="response-streams"><a href="#response-streams">Response streams</a></h2>
<p>Streaming responses have been available in all browsers for years now:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> reader <span class="token operator">=</span> response<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">getReader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> <span class="token punctuation">{</span> done<span class="token punctuation">,</span> value <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> reader<span class="token punctuation">.</span><span class="token function">read</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>done<span class="token punctuation">)</span> <span class="token keyword">break</span><span class="token punctuation">;</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>value<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><span class="token string">'Done!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>This lets you get the response chunk-by-chunk, so you can start processing it as you receive it.</p>
<p>This is even easier with async iterators:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">for</span> <span class="token keyword">await</span> <span class="token punctuation">(</span><span class="token keyword">const</span> chunk <span class="token keyword">of</span> response<span class="token punctuation">.</span>body<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>chunk<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><span class="token string">'Done!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>…but that isn&#39;t supported in Safari. I&#39;ve <a href="https://github.com/web-platform-tests/interop/issues/1068">proposed it for interop 2026</a>.</p>
<p>The chunks are <code>Uint8Array</code>s, but you can <a href="https://developer.mozilla.org/docs/Web/API/TextDecoderStream/TextDecoderStream">use <code>TextDecoderStream</code></a> to get the chunks as text.</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> textStream <span class="token operator">=</span> response<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">pipeThrough</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">TextDecoderStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><h3 id="but-theyre-not-ideal-for-measuring-download-progress"><a href="#but-theyre-not-ideal-for-measuring-download-progress">But they&#39;re not ideal for measuring download progress</a></h3>
<p>You could try to measure download progress like this:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// THIS DOESN'T ALWAYS WORK!</span>
<span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> contentLength <span class="token operator">=</span> <span class="token function">Number</span><span class="token punctuation">(</span>response<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'Content-Length'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> downloaded <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>

<span class="token keyword">for</span> <span class="token keyword">await</span> <span class="token punctuation">(</span><span class="token keyword">const</span> chunk <span class="token keyword">of</span> response<span class="token punctuation">.</span>body<span class="token punctuation">)</span> <span class="token punctuation">{</span>
  downloaded <span class="token operator">+=</span> chunk<span class="token punctuation">.</span>length<span class="token punctuation">;</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>contentLength<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>
      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Downloaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>downloaded <span class="token operator">/</span> contentLength<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><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><p>The good part is that you&#39;re measuring the point at which you have the data, which is what matters when it comes to download. Let&#39;s say you were receiving three packages via mail – you&#39;re measuring the point each package arrives in your possession, which is fine.</p>
<p>However, this all falls down if the response has a <code>Content-Encoding</code> (such as brotli or gzip), because in that case the <code>Content-Length</code> represents the encoded size, but the chunks are <em>decoded</em> chunks. This means your <code>downloaded</code> value is likely to exceed the <code>contentLength</code> value.</p>
<p>Although, if you made that mistake, you wouldn&#39;t be alone. A few years ago <a href="https://x.com/jaffathecake/status/996720153575546880">I investigated how browsers handled download progress of compressed resources</a>, and the results were… messy.</p>
<p>There&#39;s <a href="https://github.com/whatwg/fetch/issues/1524">a feature request for a way to get the raw body</a>, without decompressing.</p>
<h2 id="request-streams"><a href="#request-streams">Request streams</a></h2>
<p>Request streams are basically the same as response streams, but for uploads rather than downloads. Imagine a video uploading app that did some transcoding/processing of the video before uploading:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// Get a video from disk, or from the camera</span>
<span class="token keyword">const</span> videoStream <span class="token operator">=</span> <span class="token function">getVideoStreamSomehow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Process it in some way, e.g. editing or transcoding</span>
<span class="token keyword">const</span> processedVideo <span class="token operator">=</span> videoStream<span class="token punctuation">.</span><span class="token function">pipeThrough</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">SomeVideoProcessor</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">// Upload the stream</span>
<span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'POST'</span><span class="token punctuation">,</span>
  <span class="token literal-property property">body</span><span class="token operator">:</span> processedVideo<span class="token punctuation">,</span>
  <span class="token literal-property property">duplex</span><span class="token operator">:</span> <span class="token string">'half'</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-Type'</span><span class="token operator">:</span> <span class="token string">'video/mp4'</span><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><span class="token punctuation">;</span></code></pre></div><p>In this model, the video processing can happen in parallel with the uploading. More traditional methods would require processing the whole video before the upload begins, which is potentially much slower.</p>
<h3 id="restrictions"><a href="#restrictions">Restrictions</a></h3>
<p><code>duplex: &#39;half&#39;</code> means that the request must complete before the response becomes available. This &#39;reserves&#39; a future feature where browsers can support handling the request and response in parallel.</p>
<p>If you want to have a request and response in parallel, you can perform two fetches – one that streams request data, and another that streams response data.</p>
<p>Also, since the <code>Content-Length</code> of the request isn&#39;t known, the feature is restricted to transports that are already used to handling request data in chunks, which is HTTP/2 onwards.</p>
<h3 id="but-theyre-bad-for-measuring-upload-progress"><a href="#but-theyre-bad-for-measuring-upload-progress">But they&#39;re bad for measuring upload progress</a></h3>
<p>I&#39;m not even going to show the code for this because it&#39;s too unreliable.</p>
<p>With <em>request</em> streams, you end up measuring the point at which the data is taken from your stream. In terms of the postal metaphor, you&#39;re measuring the point each package is collected by the courier, which does not represent the delivery of those packages.</p>
<p>Fetch is the courier. It will take some chunks off your hands and start handling the network parts. Successfully sending those chunks will happen later, or maybe it will fail.</p>
<p>This is normal behaviour with streams, and it helps keep everything running as smoothly as possible in cases where parts of the pipe flow quicker than others. Each stream has a &quot;high water mark&quot;, which is essentially an ideal amount to buffer.</p>
<h3 id="using-streams-to-measure-upload-progress-could-cause-problems-in-future"><a href="#using-streams-to-measure-upload-progress-could-cause-problems-in-future">Using streams to measure upload progress could cause problems in future</a></h3>
<p>If you try to use streams to measure upload progress, the results will be inaccurate, but it may also limit the quality of this feature in future.</p>
<p>Let&#39;s say browsers ship this, and do a fairly small amount of buffering. If folks use this for upload progress, it won&#39;t be right, but it might be close enough.</p>
<p>Then let&#39;s say a browser discovers that they can get better performance by buffering more within fetch. If large parts of the web are using this feature for upload progress, that browser faces two choices: Ship the change, which means these websites&#39; upload progress measurements become even less accurate, or avoid shipping the general improvement. I don&#39;t want browsers to end up having to make a compromise here.</p>
<h2 id="how-to-measure-upload-progress"><a href="#how-to-measure-upload-progress">How to measure upload progress</a></h2>
<p>Upload and download progress is unfortunately a missing feature in fetch. If you want progress events <em>today</em>, the best way is to use XHR:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> xhr <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">XMLHttpRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Upload progress</span>
xhr<span class="token punctuation">.</span>upload<span class="token punctuation">.</span><span class="token function-variable function">onprogress</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>lengthComputable<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>
      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Uploaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>loaded <span class="token operator">/</span> event<span class="token punctuation">.</span>total<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><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><span class="token punctuation">;</span>

<span class="token comment">// Download progress</span>
xhr<span class="token punctuation">.</span><span class="token function-variable function">onprogress</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>lengthComputable<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>
      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Downloaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>loaded <span class="token operator">/</span> event<span class="token punctuation">.</span>total<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><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><span class="token punctuation">;</span>

xhr<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span><span class="token string">'POST'</span><span class="token punctuation">,</span> url<span class="token punctuation">)</span><span class="token punctuation">;</span>
xhr<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span>blobOrWhatever<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><h2 id="but-in-future"><a href="#but-in-future">But in future…</a></h2>
<p><a href="https://bsky.app/profile/lukewarlow.dev">Luke Warlow from Igalia</a> is currently working on <a href="https://github.com/whatwg/fetch/pull/1843">an API for fetch</a> to add progress events for both uploads and downloads.</p>
<p>The API might change before it lands, but right now it looks like this:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token punctuation">{</span>
  <span class="token function">observer</span><span class="token punctuation">(</span><span class="token parameter">requestObserver<span class="token punctuation">,</span> responseObserver</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    requestObserver<span class="token punctuation">.</span><span class="token function-variable function">onprogress</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
      <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>lengthComputable<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>
          <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Uploaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>loaded <span class="token operator">/</span> event<span class="token punctuation">.</span>total<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><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><span class="token punctuation">;</span>

    responseObserver<span class="token punctuation">.</span><span class="token function-variable function">onprogress</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
      <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>lengthComputable<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>
          <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Downloaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">(</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>loaded <span class="token operator">/</span> event<span class="token punctuation">.</span>total<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFixed</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><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><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><span class="token punctuation">;</span></code></pre></div><p>This should give us all the modern benefits of <code>fetch</code>, while plugging the progress feature gap!</p>
<p>If you&#39;re interested in using request streams for things other than progress events, there&#39;s <a href="https://github.com/web-platform-tests/interop/issues/1072">an Interop 2026 proposal for them</a>. We also want to gauge developer interest <a href="https://survey.alchemer.com/s3/8460326/Fetch-Request-Streaming">via this survey</a>, or via the interop GitHub thread.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Making XML human-readable without XSLT]]></title>
        <id>https://jakearchibald.com/2025/making-xml-human-readable-without-xslt/</id>
        <link href="https://jakearchibald.com/2025/making-xml-human-readable-without-xslt/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/icon-CJ5sT8Gh.png" type="image/png"/>
        <updated>2025-09-02T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[JavaScript is right there.]]></summary>
        <content type="html"><![CDATA[<p>Ok. This one is niche. But heh, you might enjoy a dive into this esoteric corner of the web – I certainly did.</p>
<h2 id="xslt-and-browser-support"><a href="#xslt-and-browser-support">XSLT and browser support</a></h2>
<p>I want to get to the technical-fun bits quickly, so here&#39;s a quick rundown:</p>
<ul>
<li>XSLT is an XML language for transforming XML, including transforming it into non-XML formats, such as HTML.</li>
<li>Browsers currently support a ~25 year old version of XSLT natively (examples coming up later).</li>
<li>This feature is barely used. Usage is lower than features that were subsequently removed from browsers, such as mutation events and Flash.</li>
<li>The feature is often the source of <a href="https://www.neowin.net/news/google-project-zero-exposes-security-flaw-in-libxslt-library-used-in-gnome-applications/">significant security issues</a>.</li>
</ul>
<p>This leaves browsers with a choice: Take development time away from features that have significant usage and developer demand, and instead use those resources to improve (probably rewrite) XSLT processing in browsers. Or, remove XSLT from browser engines.</p>
<p>Currently, <a href="https://github.com/whatwg/html/issues/11523#issue-3285251497">Chrome</a>, <a href="https://github.com/whatwg/html/issues/11523#issuecomment-3149280766">Safari</a>, and <a href="https://github.com/mozilla/standards-positions/issues/1287#issuecomment-3227145793">Firefox</a> support removing XSLT.</p>
<p>If you want to know more of the process &amp; history here, I recommend <a href="https://meyerweb.com/eric/thoughts/2025/08/22/no-google-did-not-unilaterally-decide-to-kill-xslt/">Eric Meyer&#39;s blog post</a>.</p>
<p>Right, let&#39;s take a look at XSLT and the alternatives.</p>
<h2 id="how-xslt-works"><a href="#how-xslt-works">How XSLT works</a></h2>
<p>Take some XML like this:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token prolog">&lt;?xml version="1.0" encoding="UTF-8"?></span>
<span class="token prolog">&lt;?xml-stylesheet type="text/xsl" href="books.xsl"?></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>authors</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>author</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>name</span><span class="token punctuation">></span></span>Douglas Adams<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>name</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>book</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>The Hitchhiker's Guide to the Galaxy<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>genre</span><span class="token punctuation">></span></span>Science Fiction Comedy<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>genre</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>cover</span><span class="token punctuation">></span></span>https://covers.openlibrary.org/b/isbn/0330258648-L.jpg<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>cover</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>book</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>book</span><span class="token punctuation">></span></span>
      …
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>book</span><span class="token punctuation">></span></span>
    …
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>author</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>author</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>name</span><span class="token punctuation">></span></span>George Orwell<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>name</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>book</span><span class="token punctuation">></span></span>
      …
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>book</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>author</span><span class="token punctuation">></span></span>
  …
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>authors</span><span class="token punctuation">></span></span></code></pre></div><p>If you <a href="/demos/xml-transformation/via-xslt/index.xml">visit that document</a>, you&#39;ll (for now, at least), see a grid of books, with formatting and images, and that&#39;s all down to:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token prolog">&lt;?xml-stylesheet type="text/xsl" href="books.xsl"?></span></code></pre></div><p>…which tells the browser to transform the XML using XSLT.</p>
<p><a href="https://github.com/jakearchibald/jakearchibald.com/blob/main/root-static/demos/xml-transformation/via-xslt/books.xsl">Here&#39;s the full source</a> if you&#39;re interested, but here&#39;s the snippet that renders each book:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token namespace">xsl:</span>template</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>book<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>div</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>book<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>img</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>cover<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{cover}<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><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>div</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>book-info<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>div</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</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>title<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><span class="token namespace">xsl:</span>value-of</span> <span class="token attr-name">select</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>title<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>span</span><span class="token punctuation">></span></span>
        -
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</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>author<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><span class="token namespace">xsl:</span>value-of</span> <span class="token attr-name">select</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>../name<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>span</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</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>genre<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><span class="token namespace">xsl:</span>value-of</span> <span class="token attr-name">select</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>genre<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>span</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token namespace">xsl:</span>template</span><span class="token punctuation">></span></span></code></pre></div><p>This template maps a <code>&lt;book&gt;</code> into a <code>&lt;div class=&quot;book&quot;&gt;</code>, although it reaches up into the parent <code>&lt;author&gt;</code> to get the author name.</p>
<p>I used to write XSLT as part of my first job ~20 years ago. Making this demo felt <em>deeply weird</em>.</p>
<p>Anyway, assuming the above will stop working someday, what are the alternatives?</p>
<h2 id="styling-xml"><a href="#styling-xml">Styling XML</a></h2>
<p>In some cases, you might get away with just styling XML, which you can do using:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token prolog">&lt;?xml-stylesheet type="text/css" href="styles.css"?></span></code></pre></div><p>Now you can style your XML document using regular CSS. Just make sure your document is served with <code>Content-Type: text/xml</code> (rather than, e.g. a more specific RSS content type), otherwise the browser won&#39;t use the styles.</p>
<p>What you can do here is pretty limited. In the XSLT example, I take the text content of the <code>&lt;cover&gt;</code> element, and make it the <code>src</code> of an image. I also repeat the author&#39;s <code>&lt;name&gt;</code> for each book. Neither of these is possible with CSS alone.</p>
<p>Also, the document will have the semantics of the XML document, which likely means &quot;no semantics&quot;, so the result will have poor accessibility. For example, even if your XML contains <code>&lt;h1&gt;</code>, unless you&#39;ve told it to be an HTML element, the browser won&#39;t recognise it as a heading.</p>
<p>That said, if you just want to catch users that mistakenly land on some XML, you can get away with something like this:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">:root</span> <span class="token punctuation">{</span>
  <span class="token selector">&amp; > *</span> <span class="token punctuation">{</span>
    <span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token selector">&amp;::before</span> <span class="token punctuation">{</span>
    <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">'Hi there! This is my RSS feed. '</span>
      <span class="token string">"It isn't meant to be human-readable…"</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>Which is what I&#39;ve done with <a href="/posts.rss">my RSS feed</a>.</p>
<p>For anything more complex, you need to do some kind of transformation of the source data.</p>
<h2 id="wait-do-you-really-want-to-do-this-client-side"><a href="#wait-do-you-really-want-to-do-this-client-side">Wait, do you really want to do this client-side?</a></h2>
<p>In almost all cases, the best thing to do is convert your data to HTML on the server, or as part of a build process. When you do that:</p>
<ul>
<li>The HTML form of your data is more likely to be seen by crawlers and scrapers.</li>
<li>The browser can render the page in a streaming way, meaning a faster experience for users.</li>
<li>You can use whatever tools you like to do the transformation – you aren&#39;t limited to things the browser supports natively.</li>
</ul>
<p>This might mean you end up with two published files, one for humans (as HTML), and one for machines (as XML or whatever). This is fine. My blog posts and my RSS feed are different resources, even though they have data in common. All my posts have this in the head:</p>
<div class="code-example"><pre class="language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span>
  <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>alternate<span class="token punctuation">"</span></span>
  <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>application/rss+xml<span class="token punctuation">"</span></span>
  <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Jake Archibald's Blog<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://jakearchibald.com/posts.rss<span class="token punctuation">"</span></span>
<span class="token punctuation">/></span></span></code></pre></div><p>…meaning feed readers can find the RSS feed from any post.</p>
<p>But if you really need to, here&#39;s how to do the transformation on the client, without XSLT:</p>
<h2 id="transforming-xml-client-side-without-xslt"><a href="#transforming-xml-client-side-without-xslt">Transforming XML client-side without XSLT</a></h2>
<p>If you just want to make some existing XSLT work in browsers that don&#39;t support it, check out <a href="https://github.com/mfreed7/xslt_polyfill">Mason&#39;s XSLT polyfill</a>. But if you don&#39;t need to use XSLT, JavaScript is, in my opinion, a better option.</p>
<p><a href="/demos/xml-transformation/via-js/index.xml">Here&#39;s a demo</a> that produces the same result as the XSLT demo, but this time using JS. Here&#39;s how it works…</p>
<p>In the XML, instead of including XSLT, I include a script as the first child of the root:</p>
<div class="code-example"><pre class="language-xml"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>authors</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span>
    <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/1999/xhtml<span class="token punctuation">"</span></span>
    <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>script.js<span class="token punctuation">"</span></span>
    <span class="token attr-name">defer</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token punctuation">"</span></span>
  <span class="token punctuation">/></span></span>
  …</code></pre></div><p>The <code>xmlns</code> attribute is important, as it tells the browser this is an HTML script element, rather than some other kind of XML element.</p>
<p>Technically, this is changing the data structure. Validating parsers would see a <code>&lt;script&gt;</code> they weren&#39;t expecting, and throw an error. However, most parsers are not validating, as they want to allow for extensions to whatever format they&#39;re reading.</p>
<p>Then, in that script:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// Get the XML data</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> document<span class="token punctuation">.</span>documentElement<span class="token punctuation">;</span>
<span class="token comment">// Create an HTML root element</span>
<span class="token keyword">const</span> htmlEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElementNS</span><span class="token punctuation">(</span>
  <span class="token string">'http://www.w3.org/1999/xhtml'</span><span class="token punctuation">,</span>
  <span class="token string">'html'</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Swap the two over</span>
data<span class="token punctuation">.</span><span class="token function">replaceWith</span><span class="token punctuation">(</span>htmlEl<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Now all I had to do was populate that <code>html</code> element. I created <a href="https://github.com/jakearchibald/jakearchibald.com/blob/main/root-static/demos/xml-transformation/via-js/html.js">a simple templating function</a> to handle escaping. For example, here&#39;s the snippet that renders each book:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> <span class="token function-variable function">bookHTML</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">bookEl</span><span class="token punctuation">)</span> <span class="token operator">=></span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
  &lt;div class="book">
    &lt;img
      class="cover"
      src="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bookEl<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">':scope > cover'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"
      alt=""
    />
    &lt;div class="book-info">
      &lt;div>
        &lt;span class="title">
          </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bookEl<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">':scope > title'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
        &lt;/span>
        -
        &lt;span class="author">
          </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bookEl<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">':scope > name'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
        &lt;/span>
      &lt;/div>
      &lt;div>
        &lt;span class="genre">
          </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bookEl<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">':scope > genre'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
        &lt;/span>
      &lt;/div>
    &lt;/div>
  &lt;/div>
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code></pre></div><p>It&#39;s very similar to the XSLT equivalent, <em>and</em> it can be debugged using regular JavaScript tooling.</p>
<h3 id="ensure-youre-creating-_html_-elements"><a href="#ensure-youre-creating-_html_-elements">Ensure you&#39;re creating <em>HTML</em> elements</a></h3>
<p>One gotcha when creating HTML in an XML document is it&#39;s easy to accidentally create XML elements rather than HTML elements. If you want proper semantics, accessibility, and behaviour, you want real HTML elements.</p>
<p>For example:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// In an XML document this will not create an HTML element:</span>
<span class="token keyword">const</span> xmlElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'h1'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Instead, you need to use:</span>
<span class="token keyword">const</span> htmlElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElementNS</span><span class="token punctuation">(</span>
  <span class="token string">'http://www.w3.org/1999/xhtml'</span><span class="token punctuation">,</span>
  <span class="token string">'h1'</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>If you&#39;re using a framework to create the elements, check that it&#39;s creating real HTML elements – <code>el.namespaceURI</code> should be <code>&quot;http://www.w3.org/1999/xhtml&quot;</code>.</p>
<p>The <code>xmlns</code> attribute we used earlier only works via the parser, so this <em>doesn&#39;t</em> create an HTML element in an XML document:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> h1 <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'h1'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
h1<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'xmlns'</span><span class="token punctuation">,</span> <span class="token string">'http://www.w3.org/1999/xhtml'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>However, <code>innerHTML</code> and <code>setHTMLUnsafe</code> will assume the content is HTML, which is what I used:</p>
<div class="code-example"><pre class="language-js"><code>htmlEl<span class="token punctuation">.</span><span class="token function">setHTMLUnsafe</span><span class="token punctuation">(</span>html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
  &lt;head>
    &lt;title>Some books&lt;/title>
    &lt;meta name="viewport" content="width=device-width, initial-scale=1" />
  &lt;/head>
  &lt;body>
    &lt;h1>Some books&lt;/h1>
    &lt;ul class="book-list">
      </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token punctuation">[</span><span class="token operator">...</span>data<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'book'</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span>
        <span class="token punctuation">(</span><span class="token parameter">book</span><span class="token punctuation">)</span> <span class="token operator">=></span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;li></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">bookHTML</span><span class="token punctuation">(</span>book<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/li></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
      <span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
    &lt;/ul>
  &lt;/body>
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Here&#39;s <a href="/demos/xml-transformation/via-js/index.xml">the full working demo</a>, and <a href="https://github.com/jakearchibald/jakearchibald.com/tree/main/root-static/demos/xml-transformation/via-js">the source</a>. And that&#39;s it! Wow you made it to the end. Huh. I didn&#39;t think anyone would.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Give footnotes the boot]]></title>
        <id>https://jakearchibald.com/2025/give-footnotes-the-boot/</id>
        <link href="https://jakearchibald.com/2025/give-footnotes-the-boot/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/icon-CJ5sT8Gh.png" type="image/png"/>
        <updated>2025-07-01T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[I hate footnotes, and hopefully by the end of this, you will too.]]></summary>
        <content type="html"><![CDATA[<style>
  html {
    scroll-padding-top: 1.3rem;
  }
  @keyframes ref-alert {
    0% {
      transform: scale(17);
      opacity: 0;
    }
    20% {
      opacity: 1;
    }
    80% {
      opacity: 1;
    }
    100% {
      transform: scale(1);
      opacity: 0;
    }
  }
  sup.ref {
    margin-left: 0.09em;
    display: inline-block;
    position: relative;
  }
  .target-effect:target::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 1.2em;
    aspect-ratio: 1 / 1;
    border-radius: 1000px;
    border: 3px solid red;
    translate: -50% -50%;
    opacity: 0;
    animation: ref-alert 0.8s ease;
  }
  .footnotes {
    font-size: 0.85em;
    > ol {
      padding-inline-start: 0;
      list-style-position: inside;
      > li {
        margin-bottom: 1em;
      }
    }
  }
  .footnote {
    background-image: linear-gradient(to right, transparent 0%, yellow 0%, yellow 100%, transparent 100%);
    background-position: left top;
    background-repeat: no-repeat;
    transition: background-size 1.7s ease;
    background-size: 0% 100%;
    &:target {
      @starting-style {
        background-size: 0% 100%;
      }
      background-size: 100% 100%;
    }
  }
  a:active {
    outline: 2px solid red;
  }
  .footnote-marker-button {
    all: unset;
    position: inline-block;
    cursor: pointer;
    color: #cc2100;
    &:hover, &:focus {
      color: #ff8f00;
    }
  }
  @keyframes backdrop-in {
    from {
      opacity: 0;
    }
  }
  @keyframes popover-in {
    from {
      opacity: 0;
      translate: 0 20px;
    }
  }
  .footnote-popover {
    &:not(:popover-open) {
      all: unset;
    }

    --side-margin: 20px;
    --border-radius: 7px;

    @media (width >= 530px) {
      --side-margin: 30px;
    }

    &:popover-open {
      all: unset;
      font-size: 1rem;

      .footnote-popover-content {
        position: fixed;
        background: #fff;
        padding: 0.9em 1.1em;
        max-width: min(476px, calc(100vw - 60px));
        border-radius: var(--border-radius);
        animation: popover-in 0.2s ease;
      }

      &::backdrop {
        background: rgba(0, 0, 0, 0.3);
        animation: backdrop-in 0.2s ease;
      }
    }

    @supports not (bottom: anchor(top)) {
      .footnote-popover-content {
        margin: auto;
        inset: 0;
        height: fit-content;
      }
    }

    @supports (bottom: anchor(top)) {
      &:popover-open {
        --arrow-size: 10px;

        .footnote-popover-content {
          position-anchor: var(--position-anchor);
          bottom: anchor(top);
          justify-self: anchor-center;
          margin: 0 var(--side-margin) var(--arrow-size);
          max-width: 476px;
        }

        &::after {
          position-anchor: var(--position-anchor);
          content: '';
          position: fixed;
          bottom: anchor(top);
          justify-self: anchor-center;
          border: calc(var(--arrow-size) + 1px) solid transparent;
          border-bottom-width: 0;
          border-top-color: #fff;
          width: 0;
          height: 0;
          margin: 0 calc(var(--side-margin) + var(--border-radius));
          animation: popover-in 0.2s ease;
        }
      }
    }
  }
  [role=note] {
    margin: 0 -20px;
    padding: 0 20px;
    background: #dbf3f7;
    display: flow-root;

    @media (width >= 530px) {
      padding: 0 1.6em;
      margin: 0;
    }

    > h4:first-child {
      margin-top: 1.3em;
    }
  }
  :root {
    interpolate-size: allow-keywords;
  }
  .details-demo {
    margin: 0 -20px;
    background: #dbf3f7;
    display: flow-root;
    overflow: clip;

    @media (width >= 530px) {
      margin: 0;
    }

    summary {
      margin: 1.2em 0;
      padding: 0 20px;

      @media (width >= 530px) {
        padding: 0 30px;
      }
    }

    .details-content {
      padding: 0 20px;

      @media (width >= 530px) {
        padding: 0 30px;
      }

      > p:first-child {
        margin-top: 0;
      }
    }

    .code-example {
      margin-bottom: 0;
      --unmargin: 20px;
      margin-left: calc(var(--unmargin) * -1);
      margin-right: calc(var(--unmargin) * -1);
      padding-left: var(--unmargin);
      padding-right: var(--unmargin);

      @media screen and (min-width: 530px) {
        --unmargin: 32px;
      }
    }

    &::details-content {
      height: 0;
      overflow: clip;
      display: flow-root;
      transition: height 0.5s ease, content-visibility 0.5s ease allow-discrete;
    }
    &[open]::details-content {
      height: auto;
    }
  }
</style><p>I hate footnotes<sup class="ref">1</sup>, and hopefully by the end of this, you will too. Let&#39;s get down to it…</p>
<h2 id="the-ux-of-footnotes-in-printed-media"><a href="#the-ux-of-footnotes-in-printed-media">The UX of footnotes in printed media</a></h2>
<p>You, the reader, encounter a tiny number<sup class="ref">2</sup> within some prose. This indicates to you that I, the writer, have something more to say on this topic. And, for your inconvenience, I&#39;ve put it way down at the bottom of the page.</p>
<p>The choice is yours: do you skip over it, and stay in the flow of the article, or do you set off on a side-quest to discover the extra wisdom I have to offer?</p>
<p>It&#39;s like one of those &quot;choose your own adventure&quot; books, except you know the destination is a dead-end, meaning you&#39;ll have to re-trace your steps so you can continue the main thread of the article.</p>
<p>Maybe if you know me well enough, you can be in tune with the kinds of things that I&#39;d choose to leave in a footnote, and you can make some kind of judgement on whether it&#39;s likely to be worth your time. But since this is the first time I&#39;ve used footnotes in an article<sup class="ref">3</sup>, you don&#39;t really have anything to go on. All that little 3 tells you is that there&#39;s some additional content that may or may not be contextually useful to you, and that it&#39;s the third time I&#39;ve done this to you.</p>
<p>Whether you go out of your way to read the footnote content is really just a test of your curiosity.</p>
<p>If you did make your way down to the footnotes, I hope you appreciated that I set the text size to be a little smaller than the &#39;ideal&#39; size that I chose for the main body content, making them harder to read, such is the tradition within the footnote community.</p>
<p>And if you enjoyed all of that, I have <em>great news for you</em>: many people bring this experience to the world wide web.</p>
<h2 id="footnotes-on-the-web"><a href="#footnotes-on-the-web">Footnotes on the web</a></h2>
<p>If reproduced as-is, footnotes on the web are even worse than their printed counterparts. In print, the footnotes are usually at the bottom of the current page, so they&#39;re a quick glance away, and you can use a finger to mark your place in the main text while you&#39;re off on your side-quest. Whereas on this beautiful pageless web of ours, you have to scroll all the way down to the end of the article. You can try flinging right to the bottom of the document, but if there&#39;s a sizeable footer or comments section, you&#39;ll overshoot the footnotes. And of course, after all of this, you have to scroll back to where you were, which is easier said than done.</p>
<p>However, most webby footnoters have realised that in the printed world, the numbering of footnotes is used to manually <em>link</em> the primary content to the supplemental content, and the web has a dedicated feature to enhance connections like this: hyperlinks<a href="#footnote-4"><sup class="ref">4</sup></a>.</p>
<p>Of course, with footnotes being at the bottom of the article, sometimes these links will simply take you to the end of the document, so you&#39;ll <em>still</em> have to visually scan for the pertinent footnote<a href="#footnote-5"><sup class="ref">5</sup></a>.</p>
<p>If the footnote markers are links, then the user can use the back button/gesture to return to the main content. But, even though this restores the previous scroll position, the user is still left with the challenge of finding their previous place in a wall of text<a href="#footnote-6"><sup class="ref target-effect" id="footnote-marker-6">6</sup></a>.</p>
<p>We could try to solve that problem by dynamically pulling the content from the footnotes and displaying it in a popover. In some browsers <span class="popover-support-commentary"></span> that will display like a tooltip, pointing directly back to the footnote marker. Thanks to modern web features, this can be done entirely without JavaScript<button class="footnote-marker-button" popovertarget="footnote-7" style="anchor-name: --footnote-7"><sup class="ref">7</sup></button>.</p>
<script>
  {
    const el = document.querySelector('.popover-support-commentary');
    if (CSS.supports('bottom: anchor(top)')) {
      el.textContent = '(including yours)';
    } else {
      el.textContent = '(not yours, sorry)';
    }
  }
</script><p>But this is still shit! I see good, smart people, who&#39;d always <a href="https://www.w3.org/QA/Tips/noClickHere">avoid using &quot;click here&quot; as link text</a>, littering their articles with link texts such as <sup>1</sup>, <sup>7</sup>, and <em>sometimes even <sup>12</sup></em>. Not only is this as contextless as &quot;click here&quot;, it also provides the extra frustration of a tiny-weeny hit target.</p>
<p><em>Update:</em> Adrian Roselli pointed out that there are <a href="https://adrianroselli.com/2022/09/brief-note-on-super-and-subscript-text.html">numerous bugs with accessibility tooling and superscript</a>.</p>
<p>And all this for what? To cargo-cult academia? Stop it! Stop it now! Footnotes are a shitty hack built on the limitations of printed media. It&#39;s dumb to build on top of those limitations when they don&#39;t exist on the web platform. So I ask you to break free of footnotes and do something better.</p>
<h2 id="alternatives-to-footnotes-on-the-web"><a href="#alternatives-to-footnotes-on-the-web">Alternatives to footnotes on the web</a></h2>
<p>Here are the goals:</p>
<ul>
<li>Provide easily-read supplementary content.</li>
<li>Let the user easily skip over it if they&#39;re not interested.</li>
<li>Make it easy for the user to decide if they&#39;re interested or not.</li>
</ul>
<p>Given that:</p>
<h3 id="just-like-parentheses"><a href="#just-like-parentheses">Just, like… parentheses?</a></h3>
<p>Honestly, if you&#39;ve got a quick little aside, just pop it in parentheses. If the reader really wants to skip it, scanning for the next closing parenthesis is pretty easy (unless you&#39;re nesting them – don&#39;t do that), and it keeps the reader in the flow of the article.</p>
<h3 id="a-note-section"><a href="#a-note-section">A &#39;note&#39; section</a></h3>
<p>If your content is a little longer, you can use <code>&lt;section role=&quot;note&quot;&gt;</code> and style it to look self-contained.</p>
<section role="note"><h4 id="why-not-aside"><a href="#why-not-aside">Why not <code>&lt;aside&gt;</code>?</a></h4>
<p>I thought <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/aside">the <code>&lt;aside&gt;</code> element</a> would be ideal for this, but that uses <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/complementary_role">the <code>complementary</code> role</a> which doesn&#39;t seem like a good fit for this kind of content. Whereas <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/note_role">the <code>note</code> role</a> is specifically for &quot;parenthetic or ancillary content&quot;, which sounds perfect.</p>
</section><p>The heading makes the topic of the note clear, and since the content is semantically and visually contained, it&#39;s easy to skip over.</p>
<h3 id="a-details-section"><a href="#a-details-section">A &#39;details&#39; section</a></h3>
<p>If your content is too long for a note, consider using <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details"><code>&lt;details&gt;</code> and <code>&lt;summary&gt;</code></a>.</p>
<details class="details-demo">
  <summary>Animating the height of the <code>&lt;details&gt;</code> element</summary>  <div class="details-content"><p>Thanks to a few newer CSS features (currently only available in Chromium), such as <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::details-content">the <code>::details-content</code> pseudo-element</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior#allow-discrete"><code>allow-discrete</code> transitions</a>, and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/interpolate-size">interpolate-size</a>, we can animate the opening and closing of <code>&lt;details&gt;</code> elements.</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">:root</span> <span class="token punctuation">{</span>
  <span class="token property">interpolate-size</span><span class="token punctuation">:</span> allow-keywords<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token selector">.details-demo</span> <span class="token punctuation">{</span>
  <span class="token selector">&amp;::details-content</span> <span class="token punctuation">{</span>
    <span class="token property">height</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
    <span class="token property">overflow</span><span class="token punctuation">:</span> clip<span class="token punctuation">;</span>
    <span class="token property">display</span><span class="token punctuation">:</span> flow-root<span class="token punctuation">;</span>
    <span class="token property">transition</span><span class="token punctuation">:</span>
      height 0.5s ease<span class="token punctuation">,</span>
      content-visibility 0.5s ease allow-discrete<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token selector">&amp;[open]::details-content</span> <span class="token punctuation">{</span>
    <span class="token property">height</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div>  </div>
</details><p>Like the note section, the summary makes the topic of the details clear, and since the content is semantically and visually contained, it&#39;s easy to skip over. The benefit with <code>&lt;details&gt;</code> is the user doesn&#39;t have to scroll past lots of content if they&#39;re not interested.</p>
<p>I&#39;m not a UX designer, so maybe you can think of better patterns. But if the alternative is footnotes, the bar is lowwwww.</p>
<h2 id="update-are-my-alternatives-too-disruptive"><a href="#update-are-my-alternatives-too-disruptive">Update: are my alternatives too disruptive?</a></h2>
<p>I&#39;ve really enjoyed the reasoned feedback to this post. Particularly those from <a href="https://front-end.social/@leaverou/114784950642671149">Lea Verou</a>, and <a href="https://www.kryogenix.org/days/2025/07/03/a-limited-defence-of-footnotes/">Stuart Langridge</a>. I recommend reading them in full (and also try a &#39;chello-bull for yourself), but I want to respond to the general sentiment that the alternatives are <em>too disruptive</em>.</p>
<p>Let&#39;s take my parenthetical example:</p>
<blockquote class="quote"><p>Honestly, if you&#39;ve got a quick little aside, just pop it in parentheses. If the reader really wants to skip it, scanning for the next closing parenthesis is pretty easy (unless you&#39;re nesting them – don&#39;t do that), and it keeps the reader in the flow of the article.</p>
</blockquote><p>Where the user experience is:</p>
<ol>
<li>Read &quot;scanning for the next closing parenthesis is pretty easy&quot;.</li>
<li>Read &quot;(unless you&#39;re nesting them&quot;.</li>
<li>Then one of the following:<ul>
<li>Read &quot; – don&#39;t do that)&quot;.</li>
<li>Or, if you&#39;ve become disinterested in this supplementary information, scan along to &quot;)&quot;.</li>
</ul>
</li>
<li>Continue reading.</li>
</ol>
<p>And compare it to the popover-footnote experience:</p>
<blockquote class="quote"><p>Honestly, if you&#39;ve got a quick little aside, just pop it in parentheses. If the reader really wants to skip it, scanning for the next closing parenthesis is pretty easy<button class="footnote-marker-button" popovertarget="footnote-8" style="anchor-name: --footnote-8"><sup class="ref">8</sup></button>, and it keeps the reader in the flow of the article.</p>
</blockquote><p>Where the user experience is:</p>
<ol>
<li>Read &quot;scanning for the next closing parenthesis is pretty easy<sup class="ref">8</sup>&quot;.</li>
<li>Decide, given no real clue as to the content of the footnote, to do one of the following:<ul>
<li>Explore, by clicking the <sup>8</sup>:<ol>
<li>Read &quot;Unless you&#39;re nesting them – don&#39;t do that.&quot;</li>
<li>Click outside the popover.</li>
<li>Continue reading.</li>
</ol>
</li>
<li>Or, skip over the <sup>8</sup>.</li>
</ul>
</li>
<li>Continue reading.</li>
</ol>
<p>Hopefully it&#39;s clear from the above that the simple parentheses solution is less disruptive, except maybe in the case where the reader is by-default disinterested in the extra content. And that&#39;s where the argument loses me.</p>
<p><strong>Why are you optimising the reading experience for people disinterested in your content?</strong> Like, if the content is really that pointless, don&#39;t put it in the article – that&#39;s even less disruptive! But, if you&#39;ve got something interesting to say, say it in context. If it&#39;s really optional, make it easy for readers to skip it once they realise it&#39;s not for them. Skipping over less-relevant content is a normal part of reading. We don&#39;t need to over-engineer it.</p>
<details class="details-demo">
  <summary>What about sidenotes?</summary>  <div class="details-content"><p>One of the footnote patterns I didn&#39;t explore were <a href="https://scottstuff.net/posts/2024/12/17/more-notes-on-notes/">sidenotes</a>, which only really work on desktop. But the disruption is similar. You see the superscript, and then you have to find the related sidenote (which is especially annoying in implementations that <code>Math.random()</code> the note to either the left or right), then visually find your way back to the main content.</p>
</details><p>All that said, I will admit I was really pleased with the no-JS popover footnotes when I got them working (although they&#39;re Chromium-only right now). But, I think that might be more down to the excitement of playing with a new CSS feature for the first time. Oh, on the topic of CSS anchors, my advice is to ignore <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/position-area"><code>position-area</code></a>, which included too much magic for me to figure out, and instead use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/anchor"><code>anchor()</code></a>. Although your mileage may vary.</p>
<p>You&#39;ll be glad to hear my new job starts in August, so I won&#39;t have as much time to waste on posts like this.</p>
<aside class="footnotes"><h2 id="footnotes-sigh"><a href="#footnotes-sigh">Footnotes (sigh)</a></h2>
<ol>
  <li>As always there are exceptions. Footnotes on Wikipedia avoid many of the pitfalls because they're only used for references, and they're a big enough site that visitors are frequent, and get used to that pattern. Although, I've only just noticed they use letter markers for 'notes' and numbers for 'references'. Also, I guess if your content is intended to be printed, then you are indeed tied to the limitations of printed media – although with media queries, you can tailor the experience specifically when printed. I don't have those requirements, so you won't catch me using footnotes again<sup class="ref">3</sup>.</li>
  <li>Not all footnotes are numbers, some are symbols, such as †.</li>
  <li style="list-style-type: '†. '">Oh, sorry, that wasn't an actual footnote marker.</li>
  <li value="3">Because, like I said, I hate them<sup class="ref">1</sup>.</li>
  <li><span id="footnote-4"></span>This is done using <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a">the anchor element</a>, which is "baseline widely available".</li>
  <li><span id="footnote-5" class="footnote"><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:target">CSS's <code>:target</code> pseudo-class</a> can help here by highlighting the linked footnote.</span></li>
  <li><span id="footnote-6" class="footnote">Some footnote authors resolve this by adding a link back to the footnote marker, some also employ <code>:target</code> styles to help the user find their previous place in the document. <a href="#footnote-marker-6">⇐</a></span></li>
  <li><span id="footnote-7" class="footnote-popover" popover style="--position-anchor: --footnote-7"><span class="footnote-popover-content">This is done using a combination of <a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API"><code>popover</code>, <code>popovertarget</code></a>, and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning">CSS anchor positioning</a>. Elements with the <code>popover</code> attribute don't usually display until activated, but I've forced it to display using CSS, which is how this content also appears in the footnotes.</span></span></li>
  <li><span id="footnote-8" class="footnote-popover" popover style="--position-anchor: --footnote-8"><span class="footnote-popover-content">Unless you're nesting them – don't do that.</span></span></li>
</ol></aside><p>There. I hope you&#39;re happy.</p>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Animating zooming using CSS: transform order is important… sometimes]]></title>
        <id>https://jakearchibald.com/2025/animating-zooming/</id>
        <link href="https://jakearchibald.com/2025/animating-zooming/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/img-BwRdLmfG.png" type="image/png"/>
        <updated>2025-06-17T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[How to get the right transform animation.]]></summary>
        <content type="html"><![CDATA[<p>I was using Discord the other day. I tapped to zoom into an image, and it animated in an odd way that I&#39;d seen before. Like this:</p>
<style>
  .full-figure img {
    height: auto;
  }
  .demo-buttons {
    display: flex;
    gap: 6px;
    flex-flow: row wrap;
  }
  .demo-container {
    overflow: clip;
    perspective: 1000px;
  }
  .scale-first-demo.zoom {
    transform: scale(3) translate(-33.1%, 20.2%);
  }
  .translate-first-calc-demo.zoom {
    --scale: 3;
    scale: var(--scale);
    translate: calc(-33.1% * var(--scale)) calc(20.2% * var(--scale));
  }
  .translate-z-demo.zoom {
    translate: -33.1% 20.2% 666.666px;
  }
  .zoomer {
    transition: transform 1s ease;
    transition-property: transform, translate, scale;
    position: relative;
    will-change: transform;
  }
  .zoomer-initial-rotate {
    transform: rotate(0);
  }
  .cat-sharp {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    transform: scale(0.3333333) translate(99.3%, -60.7%);
    will-change: transform;
  }
</style><figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer scale-first-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure><script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script><p>Notice how it kinda &#39;swoops&#39; into the wildcat&#39;s face, rather than zooming straight in? See how the right-hand side of the cat&#39;s head goes out-of-frame, and then back in again?</p>
<p>I recognised it immediately because I&#39;d made the same mistake myself on another project.</p>
<p>The CSS is pretty simple:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo</span> <span class="token punctuation">{</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> transform 1s ease<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>But watch this… If I change the starting transform from the default (<code>none</code>), to <code>rotate(0)</code>:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo</span> <span class="token punctuation">{</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> transform 1s ease<span class="token punctuation">;</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>The animation changes:</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer zoomer-initial-rotate scale-first-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure><script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script><p>Weird, huh? You wouldn&#39;t expect something like <code>rotate(0)</code>, which is equivalent to <code>none</code>, to completely change how the animation works, but this is happening entirely as designed in the CSS spec.</p>
<p>Let&#39;s dig into why.</p>
<h2 id="what-caused-the-quirky-animation"><a href="#what-caused-the-quirky-animation">What caused the quirky animation?</a></h2>
<p>Let&#39;s remove the <code>rotate(0)</code> for now and go back to the original code:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo</span> <span class="token punctuation">{</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> transform 1s ease<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>When zooming into part of an element, <code>scale(n) translate(x, y)</code> feels like the easiest way to do it. You use the <code>translate</code> to get the subject into the centre, then adjust the <code>scale</code> to zoom in. Tweaking the values in DevTools is easy, as is calculating the values in code.</p>
<p>However, while this order of values is easy to write, it doesn&#39;t produce the most natural animation.</p>
<h3 id="how-transforms-are-animated"><a href="#how-transforms-are-animated">How transforms are animated</a></h3>
<p>The CSS spec has a <a href="https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms">somewhat complex algorithm</a> to decide how to animate transforms. For our values, it takes the <code>from</code> and <code>to</code> values:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>…and begins by padding them out, so they have the same number of components:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> none none<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>Then, for each <code>from</code> and <code>to</code> pair of components, it converts them to use a common function that can express both types of value. In this case:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>1<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 0<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>Now that the transforms are in a similar format, it produces an animation that linearly interpolates each component separately. This means that the <code>scale</code> is animated linearly from <code>1</code> to <code>3</code>, and the <code>translate</code> is animated linearly from <code>0, 0</code> to <code>-33.1%, 20.2%</code>.</p>
<p>The animation itself isn&#39;t linear, as easing is applied, but linear interpolation is used as a starting point.</p>
<p>The problem is, with <code>scale</code> followed by <code>translate</code>, the <code>scale</code> acts as a multiplier for the <code>translate</code> values. Therefore, as the <code>scale</code> increases, even though the <code>translate</code> values are interpolated linearly, the effect is non-linear:</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer scale-first-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure><script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script><p>At the start of the animation, a 1px shift in the <code>translate</code> value results in a ~1px shift on screen, as the <code>scale</code> is ~1. But, towards the end of the animation, a 1px shift in the <code>translate</code> value results in a ~3px shift on screen, as the <code>scale</code> is ~3. The position appears to change faster towards the end of the animation, which creates the &#39;swooping&#39; effect.</p>
<h2 id="how-to-fix-it"><a href="#how-to-fix-it">How to fix it</a></h2>
<p>To fix this, we need to avoid the <code>scale</code> acting as a multiplier for the <code>translate</code>, and the way to do this is to put the <code>translate</code> first.</p>
<p>We can&#39;t just swap the order, since we&#39;re relying on the multiplying effect of the <code>scale</code> to make the <code>translate</code> do the right thing. To achieve the same effect, we need to manually multiply the <code>translate</code> values by the <code>scale</code> value:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translate</span><span class="token punctuation">(</span>-99.3%<span class="token punctuation">,</span> 60.6%<span class="token punctuation">)</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>And that&#39;s it!</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer translate-first-calc-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure><script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script><p>Although the <code>translate</code> values are still multiplied, they&#39;re multiplied by a constant 3, the end <code>scale</code>, rather than a changing <code>scale</code> value. The result is a steady move towards the target. Each 1px shift in the <code>translate</code> value results in a 1px shift on screen.</p>
<p>Unfortunately, this format is harder to tweak in DevTools, but you can fix that with a bit of <code>calc</code>!</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">--scale</span><span class="token punctuation">:</span> 3<span class="token punctuation">;</span>
  <span class="token property">--x</span><span class="token punctuation">:</span> -33.1%<span class="token punctuation">;</span>
  <span class="token property">--y</span><span class="token punctuation">:</span> 20.2%<span class="token punctuation">;</span>

  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translate</span><span class="token punctuation">(</span>
      <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--x<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
      <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--y<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span>
    <span class="token punctuation">)</span>
    <span class="token function">scale</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>Or even, split the <code>transform</code> into two separate properties:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">--scale</span><span class="token punctuation">:</span> 3<span class="token punctuation">;</span>
  <span class="token property">--x</span><span class="token punctuation">:</span> -33.1%<span class="token punctuation">;</span>
  <span class="token property">--y</span><span class="token punctuation">:</span> 20.2%<span class="token punctuation">;</span>

  <span class="token property">scale</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token property">translate</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--x<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--y<span class="token punctuation">)</span> * <span class="token function">var</span><span class="token punctuation">(</span>--scale<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>When you use the <code>scale</code> and <code>translate</code> properties separately, the <code>translate</code> is always applied first—which just happens to be the order we want.</p>
<p>Job done!</p>
<h2 id="but-wait-why-did-rotate0-fix-it"><a href="#but-wait-why-did-rotate0-fix-it">But wait, why did rotate(0) fix it?</a></h2>
<p>Back at the start of the article (remember that?), I mentioned that the animation could be &#39;fixed&#39; by making the starting transform <code>rotate(0)</code>:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.demo</span> <span class="token punctuation">{</span>
  <span class="token property">transition</span><span class="token punctuation">:</span> transform 1s ease<span class="token punctuation">;</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>Even though the <code>scale</code> and <code>translate</code> are in the wrong order, we get the animation we want. What gives? Well, I don&#39;t actually recommend using this &#39;fix&#39;, because it only &#39;works&#39; by hitting an edge case in the CSS spec.</p>
<p>Let&#39;s go through the algorithm again, but this time with the <code>rotate(0)</code> transform:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>As before, it pads out the values with <code>none</code> so they have the same number of components:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span> none<span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scale</span><span class="token punctuation">(</span>3<span class="token punctuation">)</span> <span class="token function">translate</span><span class="token punctuation">(</span>-33.1%<span class="token punctuation">,</span> 20.2%<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>Then, as before, it tries to convert each component pair to use a common function that can express both types of value. However, it can&#39;t. <code>rotate</code> and <code>scale</code> are considered too different to be converted into a common type.</p>
<p>When this happens, it &#39;recovers&#39; by converting those values, and all following values, to a single matrix:</p>
<div class="code-example"><pre class="language-css"><code><span class="token atrule"><span class="token rule">@keyframes</span> computed-keyframes</span> <span class="token punctuation">{</span>
  <span class="token selector">from</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">matrix</span><span class="token punctuation">(</span>1<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 1<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token selector">to</span> <span class="token punctuation">{</span>
    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">matrix</span><span class="token punctuation">(</span>3<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 3<span class="token punctuation">,</span> -673.75<span class="token punctuation">,</span> 231.904<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>And then it animates them as it would two matrices. The &#39;incorrect&#39; order of the <code>scale</code> and <code>translate</code> is lost, and the <code>translate</code> is already pre-multiplied by the <code>scale</code>. By coincidence, it animates exactly the way we want it to.</p>
<h2 id="bonus-round-scale-vs-3d-translate"><a href="#bonus-round-scale-vs-3d-translate">Bonus round: scale vs 3D translate</a></h2>
<p>In this article, we&#39;ve been animating <code>scale</code> to achieve the effect of &#39;zooming in&#39;. But, depending on the effect you want, you could use a 3D translate instead.</p>
<p>When you animate <code>scale</code>, the result is that the width and height of the target changes linearly throughout the animation (although, as before, easing can be applied). This feels similar to a camera zoom effect.</p>
<p>However, you may want an effect that&#39;s more like the object moving towards the camera, or the camera moving towards the object. To achieve this, you don&#39;t want the visual size of the object to change linearly.</p>
<p>This is because, when a far-away object moves by 1 metre, the amount of space it takes up in your field of view doesn&#39;t change much. But, when a close-up object moves by 1 metre, the amount of space it takes up in your field of view changes a lot. This is the effect of perspective.</p>
<p>So, instead of using <code>scale</code>, let&#39;s use the <code>perspective</code> property, and a 3D transform:</p>
<div class="code-example"><pre class="language-css"><code><span class="token selector">.container</span> <span class="token punctuation">{</span>
  <span class="token property">perspective</span><span class="token punctuation">:</span> 1000px<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token selector">.demo.zoom</span> <span class="token punctuation">{</span>
  <span class="token property">translate</span><span class="token punctuation">:</span> -33.1% 20.2% 666.666px<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>The rather demonic translate-z value was calculated by converting the <code>scale</code> to a translate-z value, using the following formula:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">const</span> <span class="token function-variable function">scaleToTranslateZ</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">scale<span class="token punctuation">,</span> perspective</span><span class="token punctuation">)</span> <span class="token operator">=></span>
  <span class="token punctuation">(</span>perspective <span class="token operator">*</span> <span class="token punctuation">(</span>scale <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">/</span> scale<span class="token punctuation">;</span></code></pre></div><p>And here&#39;s the result:</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer translate-z-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure><script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script><p>Ok, it&#39;s subtle. Here&#39;s the <code>scale</code> version again for comparison:</p>
<figure class="full-figure">
  <div class="demo-container">
    <div class="zoomer translate-first-calc-demo">
      <picture>
        <source srcset="/c/cat-bu4OyWPZ.avif" type="image/avif">
        <img width="1406" height="793" src="/c/cat-CxAfxtFW.webp" alt="A Scottish wildcat">
      </picture>
      <picture>
        <source srcset="/c/cat-zoomed-BJb1PzwU.avif" type="image/avif">
        <img class="cat-sharp" width="1406" height="793" src="/c/cat-zoomed-DQ6ps1TH.webp" alt="">
      </picture>
    </div>
  </div>
  <div class="figcaption demo-buttons"></div>
</figure><script>
  {
    const currentScript = document.currentScript;
    const demoContainer = currentScript.previousElementSibling;
    const buttonsContainer = demoContainer.querySelector('.demo-buttons');
    const zoomer = demoContainer.querySelector('.zoomer');

    const button = document.createElement('button');
    button.classList.add('btn');
    button.textContent = 'Toggle zoom';
    button.addEventListener('click', () => {
      zoomer.classList.toggle('zoom');
    });
    buttonsContainer.append(button);
  }
</script><p>It&#39;s mostly noticeable on the zoom-out part of the animation. The 3D version feels like it starts much faster than the <code>scale</code> version. Personally, I think the <code>scale</code> version feels better, since the intention is more of a &#39;zoom&#39; than a &#39;move&#39;. But, it&#39;s good to know the differences, so you can choose the right one for your desired effect.</p>
<small><p>Thanks to <a href="https://ohhelloana.blog/">Ana Rodrigues</a> for feedback that helped this make a lot more sense.</p>
</small>
]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Firefox + custom elements + iframes bug]]></title>
        <id>https://jakearchibald.com/2025/firefox-custom-elements-iframes-bug/</id>
        <link href="https://jakearchibald.com/2025/firefox-custom-elements-iframes-bug/"/>
        <link rel="enclosure" href="https://jakearchibald.com/c/icon-CJ5sT8Gh.png" type="image/png"/>
        <updated>2025-02-14T01:00:00.000Z</updated>
        <summary type="html"><![CDATA[A tricksy Firefox bug and how to work around it.]]></summary>
        <content type="html"><![CDATA[<p>Over at Shopify we&#39;ve been building a bunch of web components to use internally and in third party contexts. All of a sudden, we found some strange errors in our logs, all from Firefox. This is the post I wish existed when we discovered it.</p>
<p><strong>Update:</strong> This is <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1470017">now fixed</a>, and should land in Firefox 151.</p>
<h2 id="the-bug"><a href="#the-bug">The bug</a></h2>
<p>The bug happens when a custom element (or web component) is moved to a document from another JavaScript Realm <em>[spooky noises]</em>. A Realm is a separate JavaScript context with its own global, own implementation of <code>Array</code> etc etc. An iframe or popup window provides a document in a new JavaScript Realm.</p>
<p>The result of the bug is that the element&#39;s custom prototype is lost, and things like instance methods disappear.</p>
<p>Ok ok, sorry, I&#39;m trying to hit all the terms that people might search for to find a fix for this issue. Anyway, here&#39;s a simple custom element:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
  <span class="token function">say</span><span class="token punctuation">(</span><span class="token parameter">message</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>message<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">say</span><span class="token punctuation">(</span><span class="token string">'hello!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">say</span><span class="token punctuation">(</span><span class="token string">'goodbye!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

customElements<span class="token punctuation">.</span><span class="token function">define</span><span class="token punctuation">(</span><span class="token string">'my-element'</span><span class="token punctuation">,</span> MyElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>And I&#39;m going to create an instance of the element, and put it in an iframe:</p>
<div class="code-example"><pre class="language-js"><code><span class="token comment">// Create the iframe</span>
<span class="token keyword">const</span> iframe <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'iframe'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>iframe<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// Create the element, and put it in the iframe</span>
<span class="token keyword">const</span> myElement <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'my-element'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
iframe<span class="token punctuation">.</span>contentDocument<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>myElement<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>This fails in Firefox with &quot;<code>this.say</code> is not a function&quot;, within <code>connectedCallback</code>. In fact, Firefox has lost all of the instance methods of the custom element. It&#39;s &#39;downgraded&#39; to an instance of <code>HTMLElement</code> in the iframe Realm, rather than <code>MyElement</code>.</p>
<p>It&#39;s kinda funny, because the error happens within <code>connectedCallback</code>, which is an instance method, but even <em>that</em> instance method has gone. I assume the calling of <code>connectedCallback</code> was queued up before the prototype was lost.</p>
<p>If I put the element in the main document before moving it to the iframe, it fails in <code>disconnectedCallback</code> for the same reason.</p>
<p>Unfortunately this has been <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1502814">a known issue for 6 years</a>.</p>
<h2 id="the-fix"><a href="#the-fix">The fix</a></h2>
<p>The fix is kinda simple. Just… put the prototype back. As in, make it an instance of <code>MyElement</code> again.</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">class</span> <span class="token class-name">CustomElementBase</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLElement</span> <span class="token punctuation">{</span>
  <span class="token comment">// This is called when the element is moved to a new document.</span>
  <span class="token comment">// This is where we solve the bug if the element is moved to an iframe</span>
  <span class="token comment">// without first being put into the main document.</span>
  <span class="token function">adoptedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token function">rescueElementPrototype</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token comment">// This is called when the element is disconnected from a document.</span>
  <span class="token comment">// This happens whenever the element is moved around the DOM,</span>
  <span class="token comment">// but it also happens when the element is moved to a new document.</span>
  <span class="token comment">// This happens before adoptedCallback,</span>
  <span class="token comment">// so we need to fix it here,</span>
  <span class="token comment">// to avoid the bug in subclass disconnectedCallback calls.</span>
  <span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token function">rescueElementPrototype</span><span class="token punctuation">(</span><span class="token keyword">this</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">function</span> <span class="token function">rescueElementPrototype</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token comment">// Return if everything looks as expected.</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span>element <span class="token keyword">instanceof</span> <span class="token class-name">CustomElementBase</span><span class="token punctuation">)</span> <span class="token keyword">return</span><span class="token punctuation">;</span>

  <span class="token comment">// Otherwise, get the intended constructor…</span>
  <span class="token keyword">const</span> constructor <span class="token operator">=</span> customElements<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>element<span class="token punctuation">.</span>tagName<span class="token punctuation">.</span><span class="token function">toLowerCase</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">// …and set the prototype.</span>
  Object<span class="token punctuation">.</span><span class="token function">setPrototypeOf</span><span class="token punctuation">(</span>element<span class="token punctuation">,</span> constructor<span class="token punctuation">.</span>prototype<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div><p>Thanks to my colleague <a href="https://github.com/frehner">Anthony Frehner</a> who realised <code>customElements.get</code> is a simple way to get the original constructor back, rather than the mad <code>WeakMap</code> hack I was using.</p>
<p>Now, make sure your custom elements extend this base class, and ensure you call <code>super</code> methods:</p>
<div class="code-example"><pre class="language-js"><code><span class="token keyword">class</span> <span class="token class-name">MyElement</span> <span class="token keyword">extends</span> <span class="token class-name">CustomElementBase</span> <span class="token punctuation">{</span>
  <span class="token function">say</span><span class="token punctuation">(</span><span class="token parameter">message</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>message<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token function">connectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">super</span><span class="token punctuation">.</span>connectedCallback<span class="token operator">?.</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">say</span><span class="token punctuation">(</span><span class="token string">'hello!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token function">disconnectedCallback</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">super</span><span class="token punctuation">.</span>disconnectedCallback<span class="token operator">?.</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">say</span><span class="token punctuation">(</span><span class="token string">'goodbye!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre></div><p>And that&#39;s it! The bug is undone, and everything works as expected.</p>
]]></content>
    </entry>
</feed>