<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Danny Holloran</title>
    <description>The latest articles on DEV Community by Danny Holloran (@grimicorn).</description>
    <link>https://dev.to/grimicorn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3951431%2Fab822d20-286b-4190-86ce-c6a0bcbb8319.jpeg</url>
      <title>DEV Community: Danny Holloran</title>
      <link>https://dev.to/grimicorn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grimicorn"/>
    <language>en</language>
    <item>
      <title>Uint8Array toBase64 and toHex: Stop Round-Tripping Bytes Through btoa</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Fri, 25 Sep 2026 10:13:48 +0000</pubDate>
      <link>https://dev.to/grimicorn/uint8array-tobase64-and-tohex-stop-round-tripping-bytes-through-btoa-193l</link>
      <guid>https://dev.to/grimicorn/uint8array-tobase64-and-tohex-stop-round-tripping-bytes-through-btoa-193l</guid>
      <description>&lt;p&gt;Every codebase that touches binary data has the same little helper buried in a &lt;code&gt;utils&lt;/code&gt; folder. It takes a &lt;code&gt;Uint8Array&lt;/code&gt;, spreads it into &lt;code&gt;String.fromCharCode&lt;/code&gt;, hands the result to &lt;code&gt;btoa&lt;/code&gt;, and hopes for the best. Its sibling goes the other way with &lt;code&gt;atob&lt;/code&gt; and a &lt;code&gt;charCodeAt&lt;/code&gt; loop. And somewhere nearby there's a third helper that maps each byte to &lt;code&gt;toString(16).padStart(2, '0')&lt;/code&gt; so you can print a hash.&lt;/p&gt;

&lt;p&gt;None of those helpers are wrong, exactly. They're just working around the fact that &lt;code&gt;btoa&lt;/code&gt; and &lt;code&gt;atob&lt;/code&gt; were designed for strings, not bytes. That gap is now closed: &lt;code&gt;Uint8Array&lt;/code&gt; has native methods for base64 and hex, in both directions, and they've been Baseline across current Chrome, Firefox, and Safari since September 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new methods at a glance
&lt;/h2&gt;

&lt;p&gt;There are six of them, split into three pairs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bytes.toBase64(options)&lt;/code&gt; and &lt;code&gt;Uint8Array.fromBase64(string, options)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bytes.toHex()&lt;/code&gt; and &lt;code&gt;Uint8Array.fromHex(string)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;target.setFromBase64(string, options)&lt;/code&gt; and &lt;code&gt;target.setFromHex(string)&lt;/code&gt; for writing into an existing buffer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the old helper next to its replacement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: works, but only by pretending bytes are Latin-1 characters&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bytesToBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;base64ToBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBase64&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "after" version isn't just shorter. The spread in &lt;code&gt;String.fromCharCode(...bytes)&lt;/code&gt; passes every byte as a separate function argument, so a large file can blow past the engine's argument limit and throw a &lt;code&gt;RangeError&lt;/code&gt;. The native methods don't have that ceiling, and they skip the intermediate "binary string" entirely.&lt;/p&gt;

&lt;p&gt;Hex gets the same treatment, which makes Web Crypto output a one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHex&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Uint8Array.fromHex()&lt;/code&gt; goes the other direction and throws a &lt;code&gt;SyntaxError&lt;/code&gt; on odd-length strings or non-hex characters, so you get validation for free instead of silently producing &lt;code&gt;NaN&lt;/code&gt; bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  base64url and padding without string surgery
&lt;/h2&gt;

&lt;p&gt;Standard base64 uses &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;, which break in URLs and filenames. The usual fix is a chain of &lt;code&gt;.replace()&lt;/code&gt; calls to swap in &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; and strip the trailing &lt;code&gt;=&lt;/code&gt;. Now it's an option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBase64&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;alphabet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;omitPadding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// e.g. 'q3N8y0Zp1VbX-7k2_JmR4g'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding takes the same &lt;code&gt;alphabet&lt;/code&gt; option. That makes peeking inside a JWT payload pleasantly boring, since JWT segments are base64url with the padding removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readJwtPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;alphabet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works because the default &lt;code&gt;lastChunkHandling&lt;/code&gt; mode, &lt;code&gt;"loose"&lt;/code&gt;, accepts a final chunk of two or three characters without padding. If you're validating input you don't control and want to reject anything non-canonical, pass &lt;code&gt;lastChunkHandling: 'strict'&lt;/code&gt;: the last chunk must then be padded to four characters, and any leftover "overflow bits" must be zero. Decoding also ignores ASCII whitespace, so base64 that was wrapped across lines in a PEM file or an email decodes without a cleanup pass.&lt;/p&gt;

&lt;p&gt;One more quiet win: because you're decoding to bytes, not to a string, there's no more &lt;code&gt;InvalidCharacterError&lt;/code&gt; from feeding &lt;code&gt;btoa&lt;/code&gt; a string with an emoji in it. Encode text with &lt;code&gt;TextEncoder&lt;/code&gt; first, then call &lt;code&gt;toBase64()&lt;/code&gt;, and UTF-8 just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoding into memory you already have
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;setFrom*&lt;/code&gt; methods write into an existing &lt;code&gt;Uint8Array&lt;/code&gt; and return &lt;code&gt;{ read, written }&lt;/code&gt; so you know how much input was consumed and how many bytes landed. That's handy when you're reusing a buffer or decoding base64 that arrives in pieces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;written&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFromBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lastChunkHandling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stop-before-partial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;written&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;"stop-before-partial"&lt;/code&gt;, an incomplete trailing chunk is left alone instead of being decoded early or throwing, so the next network chunk can finish it. The &lt;code&gt;read&lt;/code&gt; count tells you exactly where to resume.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for them (and when not yet)
&lt;/h2&gt;

&lt;p&gt;If your app targets current evergreen browsers, you can use these today and delete the helpers. On the server, check your runtime version before you rip out &lt;code&gt;Buffer.from(x, 'base64')&lt;/code&gt;. Node's &lt;code&gt;Buffer&lt;/code&gt; still works fine, but these methods give you one API that runs the same in the browser, Deno, Bun, and edge runtimes.&lt;/p&gt;

&lt;p&gt;For older targets, feature-detect and fall back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toBase64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toBase64&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBase64&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or pull in a polyfill: &lt;code&gt;core-js&lt;/code&gt; covers the full set, and the &lt;code&gt;es-arraybuffer-base64&lt;/code&gt; shim does too.&lt;/p&gt;

&lt;p&gt;The takeaway is small but satisfying: bytes can finally stay bytes. The next time you reach for &lt;code&gt;btoa&lt;/code&gt;, check whether what you actually have is a &lt;code&gt;Uint8Array&lt;/code&gt;. If it is, &lt;code&gt;toBase64()&lt;/code&gt; is almost certainly the better call. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64" rel="noopener noreferrer"&gt;MDN reference for &lt;code&gt;Uint8Array.fromBase64()&lt;/code&gt;&lt;/a&gt; covers every option in detail, including the edge cases around padding and overflow bits.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webapis</category>
      <category>node</category>
      <category>performance</category>
    </item>
    <item>
      <title>import defer: Lazy Modules Without Going Async</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Tue, 22 Sep 2026 10:12:03 +0000</pubDate>
      <link>https://dev.to/grimicorn/import-defer-lazy-modules-without-going-async-22mf</link>
      <guid>https://dev.to/grimicorn/import-defer-lazy-modules-without-going-async-22mf</guid>
      <description>&lt;p&gt;Every codebase has that one import. A syntax highlighter, a PDF renderer, a date-formatting library with a locale table the size of a small novel. It sits at the top of a file, it gets evaluated the moment the module graph runs, and it is needed on exactly one code path that most users never hit.&lt;/p&gt;

&lt;p&gt;The usual fix is &lt;code&gt;await import()&lt;/code&gt;. It works, but it charges rent: the function that needed the module becomes async, and so does its caller, and its caller's caller. You wanted to move some work later in time, and instead you rewrote a call stack. &lt;code&gt;import defer&lt;/code&gt; is the piece that was missing, and it is riding along with ES2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linked up front, evaluated on first touch
&lt;/h2&gt;

&lt;p&gt;The syntax is a single keyword in one position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;typescript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compileFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The typescript module graph evaluates here, on first property access.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createProgram&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing from &lt;code&gt;typescript&lt;/code&gt; runs at startup. The module and its dependencies are still resolved, fetched, parsed, and linked, so a missing file or a bad named import still blows up immediately rather than lurking. What is deferred is evaluation: the top-level code only runs the first time you read a property off &lt;code&gt;ts&lt;/code&gt;. If &lt;code&gt;compileFile&lt;/code&gt; is never called, that graph never executes.&lt;/p&gt;

&lt;p&gt;That distinction is the whole value proposition. &lt;code&gt;import()&lt;/code&gt; defers everything, which is why it has to be async. &lt;code&gt;import defer&lt;/code&gt; defers only the synchronous part that can safely be moved, so property access stays synchronous and nothing above it needs to change colour.&lt;/p&gt;

&lt;p&gt;Only the namespace form works. There is no &lt;code&gt;import defer { createProgram } from "typescript"&lt;/code&gt; and no default form, because the namespace object is the thing doing the work — evaluation is triggered by a property read, and a destructured binding has nothing left to intercept.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that will surprise you
&lt;/h2&gt;

&lt;p&gt;The deferred namespace is effectively a proxy, and the list of operations that trip evaluation is broader than "read a property". Reading a key triggers it, but so does &lt;code&gt;"value" in ns&lt;/code&gt;, &lt;code&gt;Object.keys(ns)&lt;/code&gt;, a &lt;code&gt;for...in&lt;/code&gt;, &lt;code&gt;Object.getOwnPropertyDescriptor&lt;/code&gt;, and even &lt;code&gt;delete ns.value&lt;/code&gt; or &lt;code&gt;Object.defineProperty&lt;/code&gt; on it — the operation fails, since the namespace is sealed, but evaluation happens anyway. &lt;code&gt;Object.isSealed()&lt;/code&gt; and &lt;code&gt;Object.isFrozen()&lt;/code&gt; enumerate keys, so those count too.&lt;/p&gt;

&lt;p&gt;Which also means this defeats the whole thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;squares&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./squares.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getSquare&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;squares&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// evaluates immediately — you read a property&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three behaviours are worth committing to memory:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top-level &lt;code&gt;await&lt;/code&gt; opts out.&lt;/strong&gt; Reading a property is synchronous, so it cannot wait on an async module. A module containing top-level &lt;code&gt;await&lt;/code&gt; is evaluated eagerly, along with whatever its evaluation requires. The rest of the graph can still stay deferred, but do not assume &lt;code&gt;defer&lt;/code&gt; bought you anything if the target awaits at the top level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Side effects move.&lt;/strong&gt; If a module installs a polyfill, registers a custom element, or patches a global, deferring it means that side effect now happens at an unpredictable moment, or never. Those modules should stay on a plain &lt;code&gt;import&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation errors are cached and synchronous.&lt;/strong&gt; If the deferred module throws while initializing, the throw comes out of whatever expression touched the namespace, catchable with a normal &lt;code&gt;try...catch&lt;/code&gt;. Every later access rethrows the same error rather than retrying.&lt;/p&gt;

&lt;p&gt;One more detail that saves a debugging session: the namespace deliberately does not expose an export named &lt;code&gt;then&lt;/code&gt;, even after evaluation. Without that carve-out, handing the namespace to &lt;code&gt;Promise.resolve()&lt;/code&gt; would look like a thenable and force evaluation as a side effect of promise resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you can actually use it
&lt;/h2&gt;

&lt;p&gt;Tooling got there first, as usual. TypeScript has understood the syntax since 5.9, and Babel, webpack, and Prettier all handle it. Bun and Deno ship it enabled by default. In browsers, V8 has had it behind a flag with an Intent to Ship filed in September 2026, WebKit has it implemented, and Gecko has signalled support — so treat it as landing rather than landed, and check the compatibility table before you rely on it in production.&lt;/p&gt;

&lt;p&gt;The honest use case today is server-side and CLI code, where Bun, Deno, and a bundler give you a straight path. Go find the heaviest module on your cold-start path that is only needed conditionally, swap the import, and measure. If it turns out the module has top-level &lt;code&gt;await&lt;/code&gt;, or installs something on import, you have learned something useful about your dependency either way.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>performance</category>
      <category>tooling</category>
      <category>node</category>
    </item>
    <item>
      <title>Turbopack's Persistent Build Cache Only Helps If Your CI Keeps It</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Mon, 21 Sep 2026 10:06:01 +0000</pubDate>
      <link>https://dev.to/grimicorn/turbopacks-persistent-build-cache-only-helps-if-your-ci-keeps-it-4dme</link>
      <guid>https://dev.to/grimicorn/turbopacks-persistent-build-cache-only-helps-if-your-ci-keeps-it-4dme</guid>
      <description>&lt;p&gt;You upgrade to Next.js 16.3, read that Turbopack's persistent file system cache is now on by default for &lt;code&gt;next build&lt;/code&gt;, see a chart claiming up to 5.5x faster builds, push to CI, and watch your pipeline take exactly as long as it did yesterday. Nothing is broken. The feature is doing precisely what it says. The problem is that "persistent" means persisted to a directory, and your build container throws that directory away the moment it exits.&lt;/p&gt;

&lt;p&gt;This is the least glamorous kind of performance work: the speedup is real, it's free, and collecting it is an infrastructure chore rather than a code change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cache is a directory, not a feature flag
&lt;/h2&gt;

&lt;p&gt;Turbopack has been persisting its incremental compilation cache to disk for &lt;code&gt;next dev&lt;/code&gt; since 16.1. In 16.3 the same mechanism graduated for &lt;code&gt;next build&lt;/code&gt;, and both are enabled by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;turbopackFileSystemCacheForDev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default&lt;/span&gt;
    &lt;span class="na"&gt;turbopackFileSystemCacheForBuild&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dev writes to &lt;code&gt;.next/dev/cache/turbopack&lt;/code&gt;, builds write to &lt;code&gt;.next/cache/turbopack&lt;/code&gt;. On the second build Turbopack reads those entries off disk before compiling anything new, so you only pay for what changed. Vercel's published numbers span a wide range depending on how much of the route graph the change touches: &lt;code&gt;nextjs.org&lt;/code&gt; went from 21s cold to 9.2s warm, &lt;code&gt;vercel.com/home&lt;/code&gt; from 66s to 46s, and &lt;code&gt;vercel.com/geist&lt;/code&gt; from 30s to 5.5s.&lt;/p&gt;

&lt;p&gt;Every one of those numbers assumes &lt;code&gt;.next/cache&lt;/code&gt; survives between runs. Restoring it in GitHub Actions is a handful of lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;~/.npm&lt;/span&gt;
      &lt;span class="s"&gt;${{ github.workspace }}/.next/cache&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.[jt]s', '**/*.[jt]sx') }}&lt;/span&gt;
    &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;restore-keys&lt;/code&gt; fallback matters more than the exact key. If your lockfile is unchanged but source files moved, you still want the previous cache as a starting point instead of a cold compile.&lt;/p&gt;

&lt;p&gt;Docker is the case that quietly eats this. A containerized build starts from a clean layer, so unless you explicitly mount a cache or use &lt;code&gt;RUN --mount=type=cache&lt;/code&gt;, &lt;code&gt;.next/cache&lt;/code&gt; is empty on every single build and Turbopack spends time writing a cache that nothing will ever read. If that describes your setup and you don't plan to fix it, turn the thing off rather than paying for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;turbopackFileSystemCacheForBuild&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the actual decision in front of you: persist the directory, or opt out. Leaving it on with nothing to restore is the one configuration that costs you something and returns nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dev-memory win needs no setup at all
&lt;/h2&gt;

&lt;p&gt;The other half of the release is the part you'll notice without touching any config. Because cached results are now safely on disk, Turbopack can evict them from memory instead of holding every visited route forever. After compiling 50 routes, Vercel measured its own dashboard app dropping from 21.5 GB to 2 GB, and &lt;code&gt;nextjs.org&lt;/code&gt; from 4,600 MB to 840 MB.&lt;/p&gt;

&lt;p&gt;That's roughly a 90% and 82% reduction, and it lands in a moment when your dev machine is more crowded than it used to be. A coding agent, a TypeScript server, a linter, and a test watcher are all competing for the same RAM as your bundler. Eviction requires the dev file system cache to be enabled, and both are on by default. The escape hatch exists if you're debugging cache behavior itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;turbopackMemoryEviction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default is 'auto'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your mileage genuinely varies here. The reduction depends on the size of your route graph, how much of it you touched, and how long the session ran. A three-route side project has nothing to evict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two smaller things worth a look
&lt;/h2&gt;

&lt;p&gt;The React Compiler has been stable in Next.js since 16.0, but only as a Babel transform, which meant large apps waited on JS execution during builds. The React team shipped a native Rust port, and Turbopack now wires it up behind an experimental flag with early tests on large apps showing 20-50% compilation wins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;reactCompiler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;turbopackRustReactCompiler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turbopack also picked up Vite's &lt;code&gt;import.meta.glob&lt;/code&gt;, which is a small quality-of-life upgrade for anyone hand-maintaining a list of content files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./posts/*.mdx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass &lt;code&gt;eager: true&lt;/code&gt; to import each match immediately. It's Turbopack-only, so it won't work if you're still building with &lt;code&gt;--webpack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Of the four, the build cache is the one with a real chance of being invisible to you. Before you file the 16.3 upgrade as done, go look at whether &lt;code&gt;.next/cache&lt;/code&gt; actually survives a CI run. If it doesn't, you've been paying for a cache you never read.&lt;/p&gt;

&lt;p&gt;Sources: the &lt;a href="https://nextjs.org/blog/next-16-3-turbopack" rel="noopener noreferrer"&gt;Turbopack 16.3 release post&lt;/a&gt;, the &lt;a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopackFileSystemCache" rel="noopener noreferrer"&gt;&lt;code&gt;turbopackFileSystemCache&lt;/code&gt; reference&lt;/a&gt;, and the &lt;a href="https://nextjs.org/docs/app/guides/ci-build-caching" rel="noopener noreferrer"&gt;CI build caching guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>tooling</category>
      <category>performance</category>
      <category>bundlers</category>
    </item>
    <item>
      <title>CSS reading-flow: Fix the Tab Order Your Layout Broke</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Sat, 19 Sep 2026 10:06:24 +0000</pubDate>
      <link>https://dev.to/grimicorn/css-reading-flow-fix-the-tab-order-your-layout-broke-2j2k</link>
      <guid>https://dev.to/grimicorn/css-reading-flow-fix-the-tab-order-your-layout-broke-2j2k</guid>
      <description>&lt;p&gt;Put a card grid on a page, give one card &lt;code&gt;grid-row: 1 / 3&lt;/code&gt; so it sits up top as the hero, and the layout looks exactly right. Then press Tab. Focus lands somewhere in the middle of the page, jumps back up to the hero, skips sideways, and generally behaves like it is reading a different document than the one on screen. It is, in a way: the browser follows DOM order, and you just spent an afternoon making the visual order disagree with it.&lt;/p&gt;

&lt;p&gt;This has been the standing tax on grid and flexbox since they shipped. &lt;code&gt;order&lt;/code&gt;, &lt;code&gt;row-reverse&lt;/code&gt;, &lt;code&gt;grid-area&lt;/code&gt;, and auto-placement all move boxes around without moving a single node, and sequential focus navigation never got the memo. The usual workaround was positive &lt;code&gt;tabindex&lt;/code&gt;, which trades one bug for a worse one. &lt;code&gt;reading-flow&lt;/code&gt; and &lt;code&gt;reading-order&lt;/code&gt; are the CSS Display Level 4 answer, and they are in browsers now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The disconnect, in eight lines
&lt;/h2&gt;

&lt;p&gt;Here is the smallest version of the problem. Three links in a flex container, reversed, with one of them reordered on top of that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.box&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row-reverse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.box&lt;/span&gt; &lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visually you read &lt;strong&gt;One, Three, Two&lt;/strong&gt;. Tab through it and you get One, Two, Three, because that is the source order. Nothing is broken according to the spec, and everything is broken according to the person using a keyboard.&lt;/p&gt;

&lt;p&gt;One property fixes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.box&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;reading-flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex-visual&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Focus order becomes One, Three, Two, matching what is on screen in a left-to-right writing mode. If you would rather keep the reversed intent instead of the visual left-to-right sweep, &lt;code&gt;reading-flow: flex-flow&lt;/code&gt; gives you Two, Three, One. Both respect &lt;code&gt;order&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking the right keyword
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;reading-flow&lt;/code&gt; is set on the &lt;strong&gt;container&lt;/strong&gt;, and the value you want depends on the layout it creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;normal&lt;/code&gt; (the default) keeps DOM order, exactly as before.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flex-visual&lt;/code&gt; follows the visual order of flex items, accounting for writing mode.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flex-flow&lt;/code&gt; follows the flex-flow direction.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grid-rows&lt;/code&gt; walks the grid visually, row by row.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grid-columns&lt;/code&gt; walks it column by column.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grid-order&lt;/code&gt; follows the modified order when &lt;code&gt;order&lt;/code&gt; is applied to grid items, and behaves like &lt;code&gt;normal&lt;/code&gt; when it is not.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;source-order&lt;/code&gt; works in grid, flex, and block containers, and is the one that enables manual overrides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one pairs with &lt;code&gt;reading-order&lt;/code&gt;, which is set on the &lt;strong&gt;item&lt;/strong&gt; and takes an integer. It is the escape hatch for the case no keyword describes, such as an absolutely positioned item in a block container that visually sits above everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;reading-flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;source-order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.wrapper&lt;/span&gt; &lt;span class="nc"&gt;.top&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;reading-order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Focus visits &lt;code&gt;.top&lt;/code&gt; first, then falls back to source order for the rest. &lt;code&gt;reading-order&lt;/code&gt; only does anything inside a container whose &lt;code&gt;reading-flow&lt;/code&gt; is not &lt;code&gt;normal&lt;/code&gt;, so the two are always used together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not just tabindex with extra steps
&lt;/h2&gt;

&lt;p&gt;You could approximate the flex example with &lt;code&gt;tabindex="1"&lt;/code&gt;, &lt;code&gt;tabindex="2"&lt;/code&gt;, &lt;code&gt;tabindex="3"&lt;/code&gt; plus an &lt;code&gt;aria-owns&lt;/code&gt; attribute to keep the accessibility tree in sync. People have been doing exactly that for years, and it is fragile for a specific reason: positive &lt;code&gt;tabindex&lt;/code&gt; values are document-global. Another component elsewhere on the page with &lt;code&gt;tabindex="1"&lt;/code&gt; joins the same bucket, and focus starts teleporting between unrelated regions of the page.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;reading-flow&lt;/code&gt; avoids that by making the container a &lt;strong&gt;focus scope owner&lt;/strong&gt;. Sequential navigation visits every focusable element inside the container before moving on, and the direct children are ordered by the property rather than by any positive &lt;code&gt;tabindex&lt;/code&gt; on them, which is ignored for ordering purposes. The reordering also applies to how the container's children are exposed to assistive tech, not just to the Tab key, so the screen reader and the keyboard agree.&lt;/p&gt;

&lt;p&gt;One sharp edge worth knowing: an element with &lt;code&gt;display: contents&lt;/code&gt; inherits &lt;code&gt;reading-flow&lt;/code&gt; from its layout parent and becomes a valid reading flow container itself. If you use &lt;code&gt;display: contents&lt;/code&gt; as a wrapper, check your focus order after adding this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where support stands
&lt;/h2&gt;

&lt;p&gt;Chrome has shipped both properties since version 137, Safari added support in 26.4, and Firefox still has it behind a flag as of September 2026 — so this is not Baseline yet. It degrades cleanly, though: browsers that do not understand &lt;code&gt;reading-flow&lt;/code&gt; simply keep using DOM order, which is what they do today.&lt;/p&gt;

&lt;p&gt;That makes it a real progressive enhancement, with one caveat. &lt;code&gt;reading-flow&lt;/code&gt; is a fix for the cases where source order genuinely cannot match the visual layout, not a license to stop caring about markup order. Get the DOM close to right first, then reach for this to close the gap that grid and flexbox opened. The MDN pages for &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/reading-flow" rel="noopener noreferrer"&gt;reading-flow&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/reading-order" rel="noopener noreferrer"&gt;reading-order&lt;/a&gt; have the full value tables, and Chrome's &lt;a href="https://chrome.dev/reading-flow-examples/" rel="noopener noreferrer"&gt;reading-flow examples&lt;/a&gt; are worth tabbing through with your eyes closed.&lt;/p&gt;

</description>
      <category>css</category>
      <category>a11y</category>
    </item>
    <item>
      <title>WebNN: The Only Web API That Can Reach Your NPU</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Tue, 15 Sep 2026 10:06:00 +0000</pubDate>
      <link>https://dev.to/grimicorn/webnn-the-only-web-api-that-can-reach-your-npu-2pbg</link>
      <guid>https://dev.to/grimicorn/webnn-the-only-web-api-that-can-reach-your-npu-2pbg</guid>
      <description>&lt;p&gt;There is a piece of silicon in your laptop that your web app has never once used. If you bought a machine in the last couple of years, it almost certainly has an NPU: a small, power-efficient accelerator built specifically for running neural networks. Your OS uses it. Native apps use it. The browser, until recently, had no way to reach it at all.&lt;/p&gt;

&lt;p&gt;WebGPU got us closer. Running a model through WebGPU means real hardware acceleration instead of grinding through WASM on the CPU, and for a lot of workloads that is the right answer. But WebGPU targets the GPU, which is the part of your machine that is also busy compositing the page, decoding video, and draining the battery. The NPU exists precisely because inference on a GPU is fast but expensive. WebNN is the standard that closes that gap, and it is currently the only web API that provides access to an NPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the spec actually stands
&lt;/h2&gt;

&lt;p&gt;The W3C published an updated &lt;a href="https://www.w3.org/TR/webnn/" rel="noopener noreferrer"&gt;Candidate Recommendation of the Web Neural Network API&lt;/a&gt; on 22 January 2026, following more than a hundred significant changes since the previous snapshot in April 2024. That is a meaningful signal: the operator set and the graph model have stopped churning enough that browser vendors are being formally invited to implement and test.&lt;/p&gt;

&lt;p&gt;Support today is Chromium-only, across ChromeOS, Linux, macOS, Windows, and Android. Chrome and Edge have working implementations. Firefox and Safari do not, and the spec needs two independent implementations passing the test suite before it can advance to a full Recommendation. So this is a progressive-enhancement story, not a "rewrite your inference layer" story. Feature-detect and fall back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pickBackend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wasm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deviceType&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;deviceType&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`webnn:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;deviceType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// context creation throws when that device isn't available&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webgpu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webgpu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wasm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The graph model, and why it suits an NPU
&lt;/h2&gt;

&lt;p&gt;WebNN is not a tensor library. You do not write a training loop against it. You describe a computational graph once, hand it to the browser to compile, and then execute it repeatedly with different inputs. &lt;code&gt;MLGraphBuilder&lt;/code&gt; is the factory that builds that graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;deviceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MLGraphBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;descriptor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;float32&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;float32&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Float32Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;weightData&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;conv2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build step is where the value lives. Because the browser sees the whole graph before it runs anything, it can fuse operations, pick layouts, and hand the result to whatever accelerator the platform exposes — DirectML on Windows, Core ML on Apple platforms, NNAPI-style paths on Android. That whole-graph view is exactly what NPU drivers want, and it is why an imperative, op-by-op API could never have targeted this hardware well.&lt;/p&gt;

&lt;h2&gt;
  
  
  You probably want this through ONNX Runtime Web
&lt;/h2&gt;

&lt;p&gt;Writing graphs by hand is fine for a demo and miserable for a real model. In practice you reach WebNN through a runtime. ONNX Runtime Web exposes it as an execution provider, so switching backends is a config change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InferenceSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./model.onnx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;executionProviders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webnn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deviceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;powerPreference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webgpu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wasm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;freeDimensionOverrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;224&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two caveats worth internalizing before you ship this. First, operator coverage: all ONNX operators are supported by the WASM backend, but only a subset are supported by WebGL, WebGPU, and WebNN. Unsupported ops fall back to WASM, which means a model that looks like it is running on the NPU may be silently ping-ponging between backends and performing worse than either pure path. Profile the real model on real hardware; do not trust the backend name in your config. Second, &lt;code&gt;freeDimensionOverrides&lt;/code&gt; is not optional decoration — WebNN wants static shapes, and a model with dynamic dimensions will often refuse to compile until you pin them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it worth it yet?
&lt;/h2&gt;

&lt;p&gt;If you are already shipping in-browser inference, WebNN is worth wiring in as the first entry in your execution-provider list. The fallback chain costs you a few lines and the win on a machine with a real NPU is not just speed, it is power: inference that does not spin up the GPU and does not tank battery life on a laptop that is not plugged in.&lt;/p&gt;

&lt;p&gt;If you are not shipping inference yet, this is not the reason to start. Both the WebGPU and WebNN backends are still described as experimental, coverage is Chromium-only, and the operator gaps are real. But the direction is clear enough that it belongs on your radar. The &lt;a href="https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html" rel="noopener noreferrer"&gt;ONNX Runtime WebNN docs&lt;/a&gt; are the fastest way to try it on a model you already have.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webapis</category>
      <category>performance</category>
      <category>aimlinthebrowser</category>
    </item>
    <item>
      <title>GraphQL @oneOf: Exactly One Input, Enforced by the Schema</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Mon, 14 Sep 2026 10:05:35 +0000</pubDate>
      <link>https://dev.to/grimicorn/graphql-oneof-exactly-one-input-enforced-by-the-schema-1j1a</link>
      <guid>https://dev.to/grimicorn/graphql-oneof-exactly-one-input-enforced-by-the-schema-1j1a</guid>
      <description>&lt;p&gt;Every GraphQL schema I've worked on eventually grows a field that can be looked up more than one way. You want a user by ID, or by email, or by username. The type system has no way to say "exactly one of these," so you pick one of two bad options: three root fields that do the same thing, or one field with three nullable arguments and a pile of validation at the top of the resolver.&lt;/p&gt;

&lt;p&gt;The second option is the one most teams land on, and it's the one that rots. The schema advertises three optional arguments, which is a lie — two of the three combinations are errors. Your introspection-driven tooling can't see that. Your generated TypeScript types can't see it either, so the client happily compiles code that sends all three and finds out at runtime.&lt;/p&gt;

&lt;p&gt;OneOf Input Objects fix this, and as of the &lt;a href="https://spec.graphql.org/September2025/#sec-OneOf-Input-Objects" rel="noopener noreferrer"&gt;September 2025 edition of the spec&lt;/a&gt; they are no longer an experiment you have to opt into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The directive is the whole feature
&lt;/h2&gt;

&lt;p&gt;You mark an input object with &lt;code&gt;@oneOf&lt;/code&gt; and the executor enforces that callers supply exactly one field, with a non-null value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserBy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;oneOf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserBy&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three root fields collapse into one. The constraint lives in the schema instead of in a guard clause, and validation happens before your resolver runs.&lt;/p&gt;

&lt;p&gt;Two rules matter when you're writing these. Fields on a &lt;code&gt;@oneOf&lt;/code&gt; input must be nullable, and they must not declare defaults. Both fall out of the semantics: a non-null field would be required, which contradicts "pick one," and a default would silently supply a second value. If you try it, your server will reject the schema at build time rather than at query time.&lt;/p&gt;

&lt;p&gt;It's worth noting the constraint is about the field being &lt;em&gt;provided&lt;/em&gt;, not about it being truthy. Passing &lt;code&gt;{ id: null }&lt;/code&gt; is a validation error, not a lookup for a null ID. That distinction bites people migrating from hand-rolled validation, where &lt;code&gt;null&lt;/code&gt; and "absent" usually got collapsed into the same branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not just scalars
&lt;/h2&gt;

&lt;p&gt;The more interesting use is polymorphic input. GraphQL has had union types on the output side since forever and nothing equivalent on the input side. &lt;code&gt;@oneOf&lt;/code&gt; is the closest thing we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;createPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PostElementInput&lt;/span&gt;&lt;span class="p"&gt;!]!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostElementInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;oneOf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ParagraphInput&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;blockquote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BlockQuoteInput&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;gallery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GalleryInput&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ParagraphInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GalleryInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;imageUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A block editor sends a heterogeneous list of elements, each one tagged by which field it occupies, and every branch keeps its own required fields. Before this, that shape was a &lt;code&gt;JSON&lt;/code&gt; scalar with a comment above it apologizing.&lt;/p&gt;

&lt;p&gt;One sharp edge: recursive &lt;code&gt;@oneOf&lt;/code&gt; inputs are only valid if some branch can terminate. An input whose single field points back at itself has no finite value a client could ever send, and a spec-compliant server will reject it. If you need recursion, give it an escape hatch — a scalar branch, or route the cycle through a regular nullable input field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client story is still catching up
&lt;/h2&gt;

&lt;p&gt;Server support is broad. GraphQL.js v16+, GraphQL Ruby v2.0.21+, GraphQL Java v21.2+, GraphQL.NET v8+, HotChocolate v16+, Strawberry v0.230.0+, graphql-core v3.3.0+, and webonyx/graphql-php v15.21.0+ all ship it. GraphQL.js v17 tightened coercion further, so schemas that quietly relied on ambiguous inputs will now fail earlier and with better messages.&lt;/p&gt;

&lt;p&gt;Codegen is the weaker link. The ideal output is a discriminated union:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what you want, because TypeScript will then reject the two-field call at compile time. Whether you actually get it depends on your generator and its config — support has been uneven, and combinations like &lt;code&gt;@oneOf&lt;/code&gt; plus the &lt;code&gt;interface&lt;/code&gt; output setting have known rough edges. Check what your pipeline emits before assuming the guarantee reaches your client code.&lt;/p&gt;

&lt;p&gt;If you're designing a new lookup or mutation input this week, reach for &lt;code&gt;@oneOf&lt;/code&gt; first. It's a backward-compatible addition, existing clients keep working, and it moves a rule out of your resolver and into the one place every consumer of your API can already see.&lt;/p&gt;

</description>
      <category>graphql</category>
      <category>typescript</category>
      <category>webapis</category>
      <category>tooling</category>
    </item>
    <item>
      <title>node --test: The Test Runner You Already Have Installed</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Sat, 12 Sep 2026 10:05:43 +0000</pubDate>
      <link>https://dev.to/grimicorn/node-test-the-test-runner-you-already-have-installed-544a</link>
      <guid>https://dev.to/grimicorn/node-test-the-test-runner-you-already-have-installed-544a</guid>
      <description>&lt;p&gt;Every new Node library starts the same way. &lt;code&gt;npm init&lt;/code&gt;, write two functions, and then spend twenty minutes deciding between Vitest and Jest for a package that has no browser code, no JSX, and no transform pipeline worth speaking of. You install a test framework, a config file, and a transitive dependency tree that dwarfs the thing you're actually testing.&lt;/p&gt;

&lt;p&gt;Node has shipped a test runner since v18. It went stable in v20. Most of us kept reaching for the npm install anyway, mostly out of habit, partly because the early version really was thin. That's no longer a fair read of it. The 2026 version has mocking, fake timers, watch mode, coverage output, global setup hooks, and it runs your &lt;code&gt;.ts&lt;/code&gt; files without a build step. For a backend library, it's frequently enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get for zero dependencies
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;node --test&lt;/code&gt; in a project with no arguments and it walks the tree looking for files matching a fixed set of patterns: &lt;code&gt;**/*.test.js&lt;/code&gt;, &lt;code&gt;**/*-test.js&lt;/code&gt;, &lt;code&gt;**/*_test.js&lt;/code&gt;, &lt;code&gt;**/test-*.js&lt;/code&gt;, &lt;code&gt;**/test.js&lt;/code&gt;, and anything under a &lt;code&gt;**/test/&lt;/code&gt; directory. The &lt;code&gt;.cjs&lt;/code&gt; and &lt;code&gt;.mjs&lt;/code&gt; variants are included too. So are the TypeScript equivalents (&lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.cts&lt;/code&gt;, &lt;code&gt;.mts&lt;/code&gt;), unless you pass &lt;code&gt;--no-strip-types&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That last part matters more than it sounds. You can write this and run it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parseDuration&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./duration.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parseDuration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;handles compound units&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1h30m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_400_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;throws on garbage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;soon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sr"&gt;/invalid duration/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;ts-node&lt;/code&gt;, no &lt;code&gt;tsx&lt;/code&gt;, no build step. Node strips the type annotations and runs the result. The important caveat: it strips, it does not check. You still need &lt;code&gt;tsc --noEmit&lt;/code&gt; in CI if you want type errors to fail the build. Type stripping is an execution strategy, not a type checker.&lt;/p&gt;

&lt;p&gt;By default each test file runs in its own child process, which gives you real isolation between files without any config. The programmatic &lt;code&gt;run()&lt;/code&gt; API exposes this as &lt;code&gt;isolation: 'process' | 'none'&lt;/code&gt; if you need to flip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The subtest gotcha that catches everyone
&lt;/h2&gt;

&lt;p&gt;This is the one thing worth internalizing before you migrate anything. Tests created inside a bare &lt;code&gt;test()&lt;/code&gt; do not wait for their subtests. Suites do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Broken: the parent finishes before the subtest resolves,&lt;/span&gt;
&lt;span class="c1"&gt;// and the outstanding subtest is cancelled and marked as a failure.&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user flow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;creates the user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: await each subtest.&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user flow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;creates the user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;describe()&lt;/code&gt;, siblings are enqueued together and you don't need the &lt;code&gt;await&lt;/code&gt;. If you're coming from Jest or Vitest, where nesting always just works, this asymmetry will bite you exactly once and then never again. &lt;code&gt;describe&lt;/code&gt;/&lt;code&gt;it&lt;/code&gt; is an alias pair for &lt;code&gt;suite&lt;/code&gt;/&lt;code&gt;test&lt;/code&gt;, so picking the suite style sidesteps the problem entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mocking and fake timers are already there
&lt;/h2&gt;

&lt;p&gt;The part people most often assume is missing. &lt;code&gt;mock.fn()&lt;/code&gt; gives you a spy with call metadata, and &lt;code&gt;t.mock.method()&lt;/code&gt; patches an object method and auto-restores it when the test ends, which is the behavior you want and rarely get for free.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retries on failure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;setTimeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchSpy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fetchSpy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockImplementationOnce&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;503&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withRetry&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetchSpy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callCount&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One sharp edge on timers: destructured imports like &lt;code&gt;import { setTimeout } from 'node:timers'&lt;/code&gt; are not mockable. Reference the timer functions off the global or the module namespace and it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it still isn't Vitest
&lt;/h2&gt;

&lt;p&gt;Coverage is real but still behind &lt;code&gt;--experimental-test-coverage&lt;/code&gt;. It works, and the lcov reporter plugs straight into Codecov or SonarQube:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--test&lt;/span&gt; &lt;span class="nt"&gt;--experimental-test-coverage&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--test-reporter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lcov &lt;span class="nt"&gt;--test-reporter-destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lcov.info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the lcov reporter emits no human-readable results, so pair it with a second reporter in CI. Global setup and teardown landed in v24 via &lt;code&gt;--test-global-setup &amp;lt;path&amp;gt;&lt;/code&gt;, pointing at a module that exports &lt;code&gt;globalSetup&lt;/code&gt; and &lt;code&gt;globalTeardown&lt;/code&gt; functions, but it's still marked early development. &lt;code&gt;--watch&lt;/code&gt; is likewise experimental.&lt;/p&gt;

&lt;p&gt;And there's no JSDOM, no browser mode, no snapshot ecosystem to speak of, no plugin API. If you're testing React components, keep Vitest.&lt;/p&gt;

&lt;p&gt;The honest heuristic: for a server-side library, a CLI, or anything where the test story is "call a function, assert on the result," &lt;code&gt;node --test&lt;/code&gt; is probably enough, and every dependency you don't add is one you don't have to audit later. Start with the built-in runner and let the project tell you when it's outgrown it.&lt;/p&gt;

</description>
      <category>node</category>
      <category>testing</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Svelte Snippets: Reuse Markup Without a New Component</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Tue, 08 Sep 2026 10:05:15 +0000</pubDate>
      <link>https://dev.to/grimicorn/svelte-snippets-reuse-markup-without-a-new-component-575i</link>
      <guid>https://dev.to/grimicorn/svelte-snippets-reuse-markup-without-a-new-component-575i</guid>
      <description>&lt;p&gt;You have a card layout that appears twice in the same component: once wrapped in a link, once bare. The markup is identical apart from the wrapper. For years the Svelte answer was to extract &lt;code&gt;Card.svelte&lt;/code&gt;, import it, thread props through it, and accept a new file in your tree for six lines of HTML.&lt;/p&gt;

&lt;p&gt;That works, but it is a heavy tool for a light problem. A component brings its own module scope, its own props contract, and its own place in the file system. Sometimes you just want the markup twice. Svelte 5's snippets are the smaller tool, and once you see them as functions that return markup, most of the awkwardness in Svelte's old component-composition story goes away.&lt;/p&gt;

&lt;h2&gt;
  
  
  A snippet is a function, and &lt;code&gt;{@render}&lt;/code&gt; calls it
&lt;/h2&gt;

&lt;p&gt;The syntax is &lt;code&gt;{#snippet name(params)}...{/snippet}&lt;/code&gt; to define, &lt;code&gt;{@render name(args)}&lt;/code&gt; to call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;figure&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;caption&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;figcaption&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;caption&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/figcaption&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;#each&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;#if&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;render&lt;/span&gt; &lt;span class="nf"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;render&lt;/span&gt; &lt;span class="nf"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;/if&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;/each&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parameters behave like a normal function signature: any number of them, destructuring, default values. The one exception is rest parameters, which are not supported.&lt;/p&gt;

&lt;p&gt;Scope is lexical, and this is the part worth internalizing. A snippet can read anything in scope where it was declared — &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; variables, the current &lt;code&gt;{#each}&lt;/code&gt; item — and it is visible to its siblings and their children, but not to anything above it. Declare a snippet inside a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; and you cannot render it outside that &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. Snippets can also reference themselves, which makes recursive markup pleasant instead of a &lt;code&gt;&amp;lt;svelte:self&amp;gt;&lt;/code&gt; puzzle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;countdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;#if&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;render&lt;/span&gt; &lt;span class="nf"&gt;countdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;🚀&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;/if&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sr"&gt;/snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  They replace slots, and they take the &lt;code&gt;let:&lt;/code&gt; directive with them
&lt;/h2&gt;

&lt;p&gt;Snippets are values, so passing markup into a component is just passing a prop. There are three shapes for this. You can pass a snippet explicitly like any other prop, declare snippets directly inside the component's tags (Svelte turns those into props automatically), or write plain content inside the tags, which becomes the implicit &lt;code&gt;children&lt;/code&gt; snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Table&lt;/span&gt; &lt;span class="na"&gt;data=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fruits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&lt;/span&gt;fruit&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;qty&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;price&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Table&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;Table.svelte&lt;/code&gt;, those arrive as ordinary props: &lt;code&gt;let { data, header, row } = $props()&lt;/code&gt;, then &lt;code&gt;{@render row(d)}&lt;/code&gt; in the loop. Optional ones use &lt;code&gt;{@render children?.()}&lt;/code&gt;, or an &lt;code&gt;{#if}&lt;/code&gt; block when you want fallback content.&lt;/p&gt;

&lt;p&gt;Compare that to the Svelte 4 version, where &lt;code&gt;&amp;lt;slot name="row" let:item /&amp;gt;&lt;/code&gt; introduced a variable through a directive whose colon meant the opposite of every other colon in the template, and where the variable from one slot was invisible inside a sibling slot. Snippets are functions, so their parameters are just parameters. Slots still work in Svelte 5 but are deprecated.&lt;/p&gt;

&lt;p&gt;If you are migrating, budget for two rough edges. &lt;code&gt;&amp;lt;slot name="header" /&amp;gt;&lt;/code&gt; becomes &lt;code&gt;{@render header?.()}&lt;/code&gt;, but the corresponding &lt;code&gt;&amp;lt;div slot="header"&amp;gt;&lt;/code&gt; on the consumer side is silently ignored rather than erroring — the content simply vanishes. And &lt;code&gt;&amp;lt;Component let:prop&amp;gt;&lt;/code&gt; now throws, as does forwarding a slot with directives attached (&lt;code&gt;&amp;lt;slot name="a" slot="a" let:abc&amp;gt;&lt;/code&gt;), which was legal in Svelte 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typing and the edges
&lt;/h2&gt;

&lt;p&gt;Snippets have a real type. Import &lt;code&gt;Snippet&lt;/code&gt; from &lt;code&gt;svelte&lt;/code&gt; and give it a tuple of its parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Snippet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svelte&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Snippet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Snippet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;generics="T"&lt;/code&gt; to the &lt;code&gt;&amp;lt;script lang="ts"&amp;gt;&lt;/code&gt; tag and you can tie &lt;code&gt;data: T[]&lt;/code&gt; to &lt;code&gt;row: Snippet&amp;lt;[T]&amp;gt;&lt;/code&gt;, so the consumer gets a type error for a mismatched row. That is strictly better than what slots ever offered.&lt;/p&gt;

&lt;p&gt;Two extras worth knowing. Top-level snippets can be exported from a &lt;code&gt;&amp;lt;script module&amp;gt;&lt;/code&gt; block and imported into other components, as long as they do not touch instance-level state (Svelte 5.5.0 and up). And &lt;code&gt;createRawSnippet&lt;/code&gt; builds one programmatically, which you will almost never need but which exists when a library has to.&lt;/p&gt;

&lt;p&gt;The rule of thumb: reach for a snippet when you are repeating markup, and for a component when the chunk has its own state, its own styles, or its own reason to be tested. The &lt;a href="https://svelte.dev/docs/svelte/snippet" rel="noopener noreferrer"&gt;snippet docs&lt;/a&gt; cover the remaining corners.&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>javascript</category>
      <category>typescript</category>
    </item>
    <item>
      <title>CSS scroll-state() Queries: Styling Stuck, Snapped, and Scrollable</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Mon, 07 Sep 2026 10:05:29 +0000</pubDate>
      <link>https://dev.to/grimicorn/css-scroll-state-queries-styling-stuck-snapped-and-scrollable-2oga</link>
      <guid>https://dev.to/grimicorn/css-scroll-state-queries-styling-stuck-snapped-and-scrollable-2oga</guid>
      <description>&lt;p&gt;Every codebase I have worked in has the same file somewhere. It is called &lt;code&gt;stickyHeader.js&lt;/code&gt; or &lt;code&gt;useIsStuck.ts&lt;/code&gt;, and it exists because CSS could tell you an element was &lt;code&gt;position: sticky&lt;/code&gt; but never whether it was currently &lt;em&gt;stuck&lt;/em&gt;. So you wire up an IntersectionObserver against a one-pixel sentinel div, toggle a class, and hope nobody asks why the shadow flickers on iOS.&lt;/p&gt;

&lt;p&gt;The browser has always known the answer. It has to know: it is the thing doing the sticking, the snapping, and the overflowing. Scroll-state container queries are the API that finally exposes that knowledge to the stylesheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three states, and the container that owns them
&lt;/h2&gt;

&lt;p&gt;The mental model is the one you already have from container queries. You mark an element as a container, then style its &lt;strong&gt;descendants&lt;/strong&gt; based on the container's state. The only new part is a &lt;code&gt;container-type&lt;/code&gt; value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.header-wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scroll-state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.header-wrapper&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;nav&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;box-shadow&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="err"&gt;@container&lt;/span&gt; &lt;span class="err"&gt;scroll-state(&lt;/span&gt;&lt;span class="py"&gt;stuck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="nb"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole sentinel-div dance, gone. Three descriptors cover most of what you would reach for JavaScript to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;stuck: top | right | bottom | left&lt;/code&gt; — the sticky element is currently pinned to that edge&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snapped: x | y | inline | block&lt;/code&gt; — this scroll-snap target is the one the scroller has snapped to&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scrollable: top | right | bottom | left&lt;/code&gt; — there is more content to scroll in that direction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a fourth, &lt;code&gt;scrolled&lt;/code&gt;, which reports the direction of the most recent scroll and shipped later than the other three (Chrome 144, January 2026).&lt;/p&gt;

&lt;p&gt;The rule that trips everyone up on their first attempt: &lt;strong&gt;the container cannot style itself&lt;/strong&gt;. The element carrying &lt;code&gt;container-type: scroll-state&lt;/code&gt; is the sticky or snapping element, and the thing that reacts has to be a child of it. For a snap carousel that means three levels, not two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scroll container      → scroll-snap-type: x mandatory
  └ snap target       → scroll-snap-align + container-type: scroll-state
      └ child element → @container scroll-state(snapped: x) { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fading the unsnapped items
&lt;/h2&gt;

&lt;p&gt;Carousels where the centered item is full-strength and its neighbours dim used to require a scroll listener or the &lt;code&gt;scrollsnapchange&lt;/code&gt; event. With &lt;code&gt;not&lt;/code&gt; in the query you can invert it and write the effect as a single rule on the inactive state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.testimonials&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scroll-snap-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.testimonials&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scroll-state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.testimonials&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt; &lt;span class="m"&gt;0.4s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="err"&gt;@container&lt;/span&gt; &lt;span class="err"&gt;not&lt;/span&gt; &lt;span class="err"&gt;scroll-state(&lt;/span&gt;&lt;span class="py"&gt;snapped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth knowing: the snapped query fires like &lt;code&gt;scrollsnapchanging&lt;/code&gt;, not &lt;code&gt;scrollsnapchange&lt;/code&gt;. It flips as soon as the browser decides which target it is heading for, before the scroll settles. That is usually what you want for visual feedback, and occasionally too eager, in which case the JavaScript event is still the right tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scroll affordances that actually know
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;scrollable&lt;/code&gt; descriptor solves a problem that was genuinely hard: knowing whether an overflow area has anywhere left to go. Lea Verou's &lt;code&gt;background-attachment: local&lt;/code&gt; trick approximated it, and scroll-driven animations approximated it differently, but both were workarounds. Now the gradient just asks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.pane&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scroll-state&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.pane&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* ...pinned overlay spanning the scrollport... */&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="err"&gt;@container&lt;/span&gt; &lt;span class="err"&gt;scroll-state(&lt;/span&gt;&lt;span class="py"&gt;scrollable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in there are easy to miss. A pseudo-element can query its own originating element's container, which is what makes the single-overlay pattern work. And a single element can be both a &lt;code&gt;size&lt;/code&gt; and a &lt;code&gt;scroll-state&lt;/code&gt; container, so you are not forced to pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually stands
&lt;/h2&gt;

&lt;p&gt;Support is Chromium-only. As of August 2026, caniuse puts global coverage around 72% with Chrome and Edge 133+, Opera 118+, and Samsung Internet 29+ shipping it. Safari 27 and Firefox 158 still have not, so this is not Baseline and you should not read a blog post that tells you it is.&lt;/p&gt;

&lt;p&gt;That is fine for the use cases above, because every one of them is decoration. A shadow that never appears, a dimmed neighbour that stays bright, a gradient that stays hidden: nothing breaks, the carousel still scrolls, the header still sticks. Wrap the enhancement in &lt;code&gt;@supports (container-type: scroll-state)&lt;/code&gt; when you need the fallback to be explicit rather than merely harmless, and put &lt;code&gt;@media (prefers-reduced-motion: no-preference)&lt;/code&gt; around anything that moves.&lt;/p&gt;

&lt;p&gt;What I would not do yet is delete the JavaScript from a component where the stuck state changes behaviour rather than appearance. For the other 90% of cases, this is a stylesheet-sized replacement for a file you have been maintaining for years.&lt;/p&gt;

</description>
      <category>css</category>
      <category>scroll</category>
      <category>webapis</category>
      <category>animation</category>
    </item>
    <item>
      <title>Async Svelte: Using await Directly in Your Components</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Mon, 31 Aug 2026 08:05:37 +0000</pubDate>
      <link>https://dev.to/grimicorn/async-svelte-using-await-directly-in-your-components-1bgf</link>
      <guid>https://dev.to/grimicorn/async-svelte-using-await-directly-in-your-components-1bgf</guid>
      <description>&lt;p&gt;Every Svelte codebase eventually grows a little pile of scaffolding around asynchronous data. A &lt;code&gt;let data = $state(null)&lt;/code&gt;, an &lt;code&gt;$effect&lt;/code&gt; that fetches and assigns, a &lt;code&gt;loading&lt;/code&gt; flag, an &lt;code&gt;error&lt;/code&gt; flag, and a &lt;code&gt;{#if loading}&lt;/code&gt; in the template. Or the &lt;code&gt;{#await}&lt;/code&gt; block, which is fine for one promise but nests badly the moment you need two. Either way, you end up writing plumbing rather than describing your UI.&lt;/p&gt;

&lt;p&gt;Since Svelte 5.36, you can skip most of that. The &lt;code&gt;await&lt;/code&gt; keyword works in three places it previously did not: at the top level of a component's &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, inside &lt;code&gt;$derived(...)&lt;/code&gt;, and directly in your markup. It is still behind an experimental flag, but the design is worth understanding now because it changes how you think about loading states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it on, and what changes
&lt;/h2&gt;

&lt;p&gt;Async Svelte is opt-in. Add &lt;code&gt;experimental.async&lt;/code&gt; wherever you configure the compiler, which usually means &lt;code&gt;svelte.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;compilerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs say this flag disappears in Svelte 6, so today's opt-in is tomorrow's default. Once it is on, this is legal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;bind:value=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;bind:value=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; + &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; = &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is what does &lt;em&gt;not&lt;/em&gt; happen. Bump &lt;code&gt;a&lt;/code&gt; to 2 and the paragraph does not flash &lt;code&gt;2 + 2 = 3&lt;/code&gt; while the promise is in flight. Svelte holds the whole update until &lt;code&gt;add(a, b)&lt;/code&gt; resolves, then swaps everything at once. That is the headline feature: &lt;strong&gt;synchronized updates&lt;/strong&gt;. You never render a torn UI where half the values are new and half are stale, which is exactly the bug that &lt;code&gt;loading&lt;/code&gt; flags exist to paper over.&lt;/p&gt;

&lt;p&gt;Updates can also overlap. A fast update lands while a slower earlier one is still running, so a quick keystroke is not stuck behind a slow one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency is automatic, waterfalls are not
&lt;/h2&gt;

&lt;p&gt;Two independent &lt;code&gt;await&lt;/code&gt; expressions in markup run in parallel, even though they read as sequential:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both kick off immediately. The same is &lt;em&gt;not&lt;/em&gt; true inside your &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, where &lt;code&gt;await&lt;/code&gt; behaves like ordinary JavaScript and runs top to bottom. Svelte will warn you about this with an &lt;code&gt;await_waterfall&lt;/code&gt; warning when you write something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$derived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$derived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;b&lt;/code&gt; is not created until &lt;code&gt;a&lt;/code&gt; resolves. Once both exist they update independently, but that first pass is a waterfall. If you have seen the same class of bug in a React &lt;code&gt;useEffect&lt;/code&gt; chain, this is the familiar shape with a compiler warning attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading states move into boundaries
&lt;/h2&gt;

&lt;p&gt;With no &lt;code&gt;loading&lt;/code&gt; variable to hang a spinner on, placeholder UI moves to &lt;code&gt;&amp;lt;svelte:boundary&amp;gt;&lt;/code&gt; and its &lt;code&gt;pending&lt;/code&gt; snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svelte:boundary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;delayed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="nf"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;loading...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svelte:boundary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pending&lt;/code&gt; snippet shows when the boundary is first created and stays until every &lt;code&gt;await&lt;/code&gt; inside it resolves. It deliberately does not reappear for later updates, since those are globally coordinated and rendering a full-page skeleton on every keystroke would be worse than useless.&lt;/p&gt;

&lt;p&gt;For subsequent async work, &lt;code&gt;$effect.pending()&lt;/code&gt; tells you how many promises are outstanding in the current boundary, not counting child boundaries. That is what you reach for when you want a small "validating..." spinner next to a form field rather than blanking the section. There is also &lt;code&gt;settled()&lt;/code&gt;, a promise that resolves once state changes and their async consequences have been flushed to the DOM.&lt;/p&gt;

&lt;p&gt;Errors get the same treatment. Anything thrown inside an &lt;code&gt;await&lt;/code&gt; expression bubbles to the nearest boundary, where a &lt;code&gt;failed&lt;/code&gt; snippet receives the &lt;code&gt;error&lt;/code&gt; and a &lt;code&gt;reset&lt;/code&gt; function. Worth remembering: boundaries catch errors during rendering and effects, not errors from event handlers or a stray &lt;code&gt;setTimeout&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts still in motion
&lt;/h2&gt;

&lt;p&gt;This is experimental, and the docs are direct about it: the details of &lt;code&gt;await&lt;/code&gt; handling and &lt;code&gt;$effect.pending()&lt;/code&gt; can change outside a semver major. Effect ordering already shifts when the flag is on, with block effects like &lt;code&gt;{#if}&lt;/code&gt; and &lt;code&gt;{#each}&lt;/code&gt; running before &lt;code&gt;$effect.pre&lt;/code&gt; in the same component.&lt;/p&gt;

&lt;p&gt;Server rendering works through an awaited &lt;code&gt;render(...)&lt;/code&gt;, though frameworks handle that for you. Today a boundary with a &lt;code&gt;pending&lt;/code&gt; snippet renders that snippet during SSR and skips its contents, with streaming planned but not shipped. Svelte 5.42 also added &lt;code&gt;fork(...)&lt;/code&gt;, which speculatively runs async work you expect to need soon, and SvelteKit is the intended consumer for preloading on hover or focus.&lt;/p&gt;

&lt;p&gt;If you maintain a Svelte app, the useful move right now is not a rewrite. Turn the flag on in a branch, pick one component with the most &lt;code&gt;loading&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; bookkeeping, and see how much of it disappears. The &lt;a href="https://svelte.dev/docs/svelte/await-expressions" rel="noopener noreferrer"&gt;await expressions docs&lt;/a&gt; and the &lt;a href="https://svelte.dev/docs/svelte/svelte-boundary" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;svelte:boundary&amp;gt;&lt;/code&gt; reference&lt;/a&gt; are short enough to read in one sitting.&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>The Web Locks API: One Tab Does the Work, the Rest Wait</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Sat, 29 Aug 2026 08:06:30 +0000</pubDate>
      <link>https://dev.to/grimicorn/the-web-locks-api-one-tab-does-the-work-the-rest-wait-12om</link>
      <guid>https://dev.to/grimicorn/the-web-locks-api-one-tab-does-the-work-the-rest-wait-12om</guid>
      <description>&lt;p&gt;A user opens your dashboard, then opens it again in a second tab, then leaves a third one parked on another monitor from yesterday. Their access token expires. All three tabs notice at roughly the same instant, and all three fire a refresh request against your auth endpoint. Two of them get back a rotated refresh token that the third has already invalidated, and now the user is staring at a login screen they did nothing to deserve.&lt;/p&gt;

&lt;p&gt;The usual fix is a pile of &lt;code&gt;localStorage&lt;/code&gt; flags with timestamps, a &lt;code&gt;BroadcastChannel&lt;/code&gt; message, and a comment that says &lt;code&gt;// TODO: this is racy&lt;/code&gt;. It is racy. &lt;code&gt;localStorage&lt;/code&gt; has no atomic compare-and-set, so two tabs can read "no refresh in progress" in the same tick and both write "refresh in progress." What you actually want is a mutex, and the browser has shipped one since March 2022. It is called the Web Locks API, it is Baseline Widely available, and almost nobody reaches for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  navigator.locks.request is the whole API
&lt;/h2&gt;

&lt;p&gt;There is one method that matters. You give it a name, a callback, and the browser guarantees that no other code on the same origin — any tab, any iframe, any worker — runs inside a lock with that name at the same time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token-refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// someone else already did it&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;writeToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lock is held for exactly as long as the callback's promise is pending, and it is released when the callback returns or throws. There is no &lt;code&gt;unlock()&lt;/code&gt; to forget, and there is no leaked lock if your fetch rejects. That alone makes it safer than any flag-in-storage scheme you would write by hand.&lt;/p&gt;

&lt;p&gt;The re-check inside the callback is the part people skip. Three tabs queue on &lt;code&gt;'token-refresh'&lt;/code&gt;. The first one does the network round trip. The second and third get the lock afterward, see a token that is no longer expired, and return immediately. One request, three happy tabs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared locks, and not waiting at all
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;request()&lt;/code&gt; takes an options object, and two of the options carry most of the value.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mode: 'shared'&lt;/code&gt; gives you the readers-writer pattern. Any number of shared holders can hold the same name at once, but an exclusive holder blocks all of them. This is the same semantics IndexedDB uses for &lt;code&gt;readonly&lt;/code&gt; versus &lt;code&gt;readwrite&lt;/code&gt; transactions, and it is the right shape when many tabs read a cached dataset while one occasionally rewrites it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Many of these can run concurrently.&lt;/span&gt;
&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;catalog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shared&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;readCatalogFromIDB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// This one waits for every reader to finish, then blocks new ones.&lt;/span&gt;
&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;catalog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exclusive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rewriteCatalogFromIDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchCatalog&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ifAvailable: true&lt;/code&gt; flips the behavior from "wait your turn" to "tell me no." The callback still runs, but it receives &lt;code&gt;null&lt;/code&gt; instead of a &lt;code&gt;Lock&lt;/code&gt; when the lock was already held. That is the leader-election primitive: whichever tab gets the lock becomes the one that owns the WebSocket, or the polling interval, or the background sync, and the others quietly stand down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync-leader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ifAvailable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// another tab is the leader&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt; &lt;span class="c1"&gt;// hold it for the lifetime of this tab&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That never-resolving promise looks alarming and is actually the idiom. The lock is held until the tab closes or navigates, at which point the browser releases it and a queued tab is promoted automatically. You get failover for free.&lt;/p&gt;

&lt;p&gt;There is also &lt;code&gt;signal&lt;/code&gt;, which takes an &lt;code&gt;AbortSignal&lt;/code&gt; so you can give up after 200ms instead of queueing forever, and &lt;code&gt;steal: true&lt;/code&gt;, which forcibly releases whoever holds the lock. Treat &lt;code&gt;steal&lt;/code&gt; as a recovery tool for a wedged tab, not a normal control flow — the stolen-from code keeps running and has no idea it lost the lock. Note that &lt;code&gt;signal&lt;/code&gt; cannot be combined with &lt;code&gt;steal&lt;/code&gt; or &lt;code&gt;ifAvailable&lt;/code&gt;; the request rejects with a &lt;code&gt;NotSupportedError&lt;/code&gt; if you try.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edges worth knowing
&lt;/h2&gt;

&lt;p&gt;Locks are scoped per origin and require a secure context, so &lt;code&gt;https://&lt;/code&gt; or &lt;code&gt;localhost&lt;/code&gt; only. They do not survive a reload — every lock a document holds is released when that document goes away, which is the behavior you want but also means a lock is never a durable record of anything. Store the actual state in IndexedDB and use the lock only to serialize who writes it.&lt;/p&gt;

&lt;p&gt;Deadlock is still your problem. If tab A holds &lt;code&gt;a&lt;/code&gt; and waits on &lt;code&gt;b&lt;/code&gt; while tab B does the reverse, they both wait forever. Acquire locks in a consistent order, keep the critical section short, and avoid nesting &lt;code&gt;request()&lt;/code&gt; calls when you can flatten them.&lt;/p&gt;

&lt;p&gt;For debugging, &lt;code&gt;navigator.locks.query()&lt;/code&gt; returns &lt;code&gt;{ held, pending }&lt;/code&gt; arrays with the name, mode, and a &lt;code&gt;clientId&lt;/code&gt; for each. Logging that when something feels stuck is usually faster than reasoning about it.&lt;/p&gt;

&lt;p&gt;If your app has any of the classic multi-tab bugs — duplicated token refreshes, four WebSockets where you wanted one, an IndexedDB migration that runs twice — this is a smaller fix than the workaround you are currently maintaining. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request" rel="noopener noreferrer"&gt;MDN's LockManager reference&lt;/a&gt; covers every option in a page you can read in five minutes.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webapis</category>
      <category>performance</category>
    </item>
    <item>
      <title>Form-Associated Custom Elements: Web Components That Belong in a Form</title>
      <dc:creator>Danny Holloran</dc:creator>
      <pubDate>Sat, 29 Aug 2026 08:06:24 +0000</pubDate>
      <link>https://dev.to/grimicorn/form-associated-custom-elements-web-components-that-belong-in-a-form-19kd</link>
      <guid>https://dev.to/grimicorn/form-associated-custom-elements-web-components-that-belong-in-a-form-19kd</guid>
      <description>&lt;p&gt;Custom elements have been shippable for years, but the illusion falls apart the moment you drop one inside a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;. The value never shows up in &lt;code&gt;FormData&lt;/code&gt;. &lt;code&gt;required&lt;/code&gt; does nothing. Hitting reset leaves your control sitting there with stale state, and the browser's validation bubble refuses to point at it. So most of us reach for the same workaround: render a hidden &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; inside the component and keep it in sync by hand, forever.&lt;/p&gt;

&lt;p&gt;That workaround has been unnecessary for a while now. Form-associated custom elements are Baseline — Chromium, Firefox, and Safari 16.4 and up — and they let a component participate in a form as a first-class control instead of a decoration sitting next to one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two lines make it a form control
&lt;/h2&gt;

&lt;p&gt;The whole thing hinges on a static property and one method call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RatingInput&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;formAssociated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachInternals&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachShadow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;connectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
      &amp;lt;div role="radiogroup" aria-label="Rating"&amp;gt;
        &lt;/span&gt;&lt;span class="p"&gt;${[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
              &lt;span class="s2"&gt;`&amp;lt;button part="star" type="button" value="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;&amp;amp;#9733;&amp;lt;/button&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
      &amp;lt;/div&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFormValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rating-input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RatingInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;static formAssociated = true&lt;/code&gt; tells the browser to treat the element like a form control: it gets picked up by the owning form, it inherits &lt;code&gt;name&lt;/code&gt;, and it becomes eligible for validation. &lt;code&gt;attachInternals()&lt;/code&gt; hands back an &lt;code&gt;ElementInternals&lt;/code&gt; object, which is the private channel your component uses to talk to the form. Guard it — anything you can do through internals is something you probably do not want page scripts doing on your behalf, which is why it lives in a private field.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setFormValue()&lt;/code&gt; is the part that ends the hidden-input era. Pass it a string, a &lt;code&gt;File&lt;/code&gt;, or a whole &lt;code&gt;FormData&lt;/code&gt; object when one control needs to contribute several named values, and it lands in the submission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"review"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;rating-input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"score"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/rating-input&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The lifecycle you get for free
&lt;/h2&gt;

&lt;p&gt;Being form-associated also opts you into callbacks the browser fires at the right moments, so you stop wiring up listeners for things the platform already knows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;formResetCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;formDisabledCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggleAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;formStateRestoreCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;formResetCallback&lt;/code&gt; runs on &lt;code&gt;form.reset()&lt;/code&gt;. &lt;code&gt;formDisabledCallback&lt;/code&gt; fires when the element or its enclosing &lt;code&gt;&amp;lt;fieldset&amp;gt;&lt;/code&gt; gets disabled, which is the case almost everyone forgets. &lt;code&gt;formStateRestoreCallback&lt;/code&gt; is the one that quietly wins arguments in code review: it restores state on back-navigation and session restore, using the optional second argument to &lt;code&gt;setFormValue(value, state)&lt;/code&gt;. If your control's submission value differs from what the user actually typed — a formatted currency field, say — pass the raw input as that second argument and you get real state restoration instead of an empty box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation the browser actually understands
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;setValidity()&lt;/code&gt; is where custom controls finally stop being second-class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValidity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;empty&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;valueMissing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="nx"&gt;empty&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please choose a rating.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first argument is a &lt;code&gt;ValidityStateFlags&lt;/code&gt; dictionary using the same flag names as native inputs (&lt;code&gt;valueMissing&lt;/code&gt;, &lt;code&gt;rangeUnderflow&lt;/code&gt;, &lt;code&gt;customError&lt;/code&gt;, and so on). The second is the message. The third — the anchor — is the one people skip and then wonder why nothing appears: it is the element the browser points its validation bubble at. Without an anchor inside your shadow root, Chromium has nowhere to render the message and silently gives up.&lt;/p&gt;

&lt;p&gt;Get this right and &lt;code&gt;form.reportValidity()&lt;/code&gt;, implicit submit blocking, and the &lt;code&gt;:invalid&lt;/code&gt; pseudo-class all work against your component exactly as they do against &lt;code&gt;&amp;lt;input required&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling states without attribute soup
&lt;/h2&gt;

&lt;p&gt;The same &lt;code&gt;ElementInternals&lt;/code&gt; object carries a &lt;code&gt;states&lt;/code&gt; set, so internal state no longer has to leak out as a reflected attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;rating-input&lt;/span&gt;&lt;span class="nd"&gt;:state&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;rated&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nd"&gt;::part&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;star&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;:state()&lt;/code&gt; has been Baseline since 2024 and composes with &lt;code&gt;:host()&lt;/code&gt; and &lt;code&gt;::part()&lt;/code&gt;, which means consumers can style your component's states without you publishing a contract of magic class names.&lt;/p&gt;

&lt;p&gt;None of this is new enough to be risky anymore, and it collapses a surprising amount of glue code. Next time you are about to add a hidden input to a component, open the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals" rel="noopener noreferrer"&gt;&lt;code&gt;ElementInternals&lt;/code&gt; docs on MDN&lt;/a&gt; instead and delete it before it exists.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>a11y</category>
      <category>css</category>
      <category>webapis</category>
    </item>
  </channel>
</rss>
