<?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: Parsa Jiravand</title>
    <description>The latest articles on DEV Community by Parsa Jiravand (@parsajiravand).</description>
    <link>https://dev.to/parsajiravand</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%2F3831018%2Ff09b70fc-3b0d-4ce2-bb7e-d78ee6f7d701.jpg</url>
      <title>DEV Community: Parsa Jiravand</title>
      <link>https://dev.to/parsajiravand</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/parsajiravand"/>
    <language>en</language>
    <item>
      <title>Your fetch() Is Still Running After the User Left</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Sun, 05 Jul 2026 09:12:47 +0000</pubDate>
      <link>https://dev.to/parsajiravand/your-fetch-is-still-running-after-the-user-left-cmf</link>
      <guid>https://dev.to/parsajiravand/your-fetch-is-still-running-after-the-user-left-cmf</guid>
      <description>&lt;p&gt;When you fire a &lt;code&gt;fetch()&lt;/code&gt; and the component that triggered it unmounts, the request keeps going. The server still processes it. When the response arrives, it calls back into whatever JavaScript it finds — a stale closure, a dead state setter, a global store that has already moved on. React's "Can't perform a state update on an unmounted component" warning is the polite version of this. The silent version is worse: results from an old query overwriting the current UI.&lt;/p&gt;

&lt;p&gt;These aren't mysterious race conditions. They're the predictable result of starting async work and never telling it to stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The race condition hiding in every search box
&lt;/h2&gt;

&lt;p&gt;The search input is the clearest example. The user types "reac", your debounce fires a request. Before it lands, they finish typing "react" and you fire another. Two requests, in flight at the same time, and no guarantee about which one finishes first.&lt;/p&gt;

&lt;p&gt;If the "reac" request happens to be slower — network jitter, a cache miss, a heavier result set — it will land after "react" and overwrite the correct results with the wrong ones. The bug reproduces maybe one time in twenty on a local dev server, and consistently in production on a slow connection.&lt;/p&gt;

&lt;p&gt;The fix isn't smarter debouncing. It's cancelling the previous request when a new one starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  AbortController in plain terms
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AbortController&lt;/code&gt; is a browser-native API for cancelling async work. You create a controller, pass its &lt;code&gt;signal&lt;/code&gt; to &lt;code&gt;fetch()&lt;/code&gt;, and call &lt;code&gt;controller.abort()&lt;/code&gt; to cancel. If the response hasn't arrived yet, the fetch promise rejects with an &lt;code&gt;AbortError&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&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;AbortController&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="s1"&gt;/api/search?q=react&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;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&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;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&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;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setResults&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="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="nx"&gt;err&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&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="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// expected — not a real error&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Somewhere else, when we no longer need this request:&lt;/span&gt;
&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to internalize: &lt;code&gt;signal&lt;/code&gt; is how the controller knows which request to cancel, and &lt;code&gt;AbortError&lt;/code&gt; is intentional — catching and ignoring it is correct behavior, not a smell.&lt;/p&gt;

&lt;h2&gt;
  
  
  The React pattern: cancel in the cleanup function
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt;'s cleanup runs when the component unmounts &lt;em&gt;and&lt;/em&gt; before the effect re-runs due to a dependency change. That makes it the natural place to cancel.&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;useEffect&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;controller&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;AbortController&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="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&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="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&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;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&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;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setResults&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="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="nx"&gt;err&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&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;query&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;query&lt;/code&gt; changes, React runs the cleanup (aborting the in-flight request) before starting the next effect (firing a new one). The stale "reac" request is cancelled the moment "react" is typed — it will never call back into state.&lt;/p&gt;

&lt;p&gt;When the component unmounts — navigation, modal close, conditional render — the cleanup fires and the pending request dies. No stale updates, no console warnings.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not just for fetch()
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;signal&lt;/code&gt; property on an &lt;code&gt;AbortController&lt;/code&gt; isn't coupled to &lt;code&gt;fetch&lt;/code&gt;. Any API that accepts an &lt;code&gt;AbortSignal&lt;/code&gt; can participate, and you can check &lt;code&gt;signal.aborted&lt;/code&gt; in your own async loops:&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;processItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&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;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;items&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="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&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;// bail out before the next iteration&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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;const&lt;/span&gt; &lt;span class="nx"&gt;controller&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;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;processItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;largeList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// A cancel button, a timeout, a navigation — any of these can call:&lt;/span&gt;
&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check &lt;code&gt;signal.aborted&lt;/code&gt; at each yield point. This pattern replaces global boolean flags and ad-hoc "is this still relevant?" tracking with a single, standard primitive that any caller can trigger.&lt;/p&gt;

&lt;p&gt;For timeouts specifically, &lt;code&gt;AbortSignal.timeout(ms)&lt;/code&gt; is cleaner than the manual approach:&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;// Auto-aborts after 5 seconds — no controller to hold, no setTimeout to clear&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="s1"&gt;/api/slow-endpoint&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;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&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="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TimeoutError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;handleTimeout&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;Note that timed-out requests throw &lt;code&gt;TimeoutError&lt;/code&gt;, not &lt;code&gt;AbortError&lt;/code&gt; — they're distinct cases, and it's useful to handle them differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about React Query, SWR, or TanStack?
&lt;/h2&gt;

&lt;p&gt;Data-fetching libraries handle this for you. React Query wires up an &lt;code&gt;AbortController&lt;/code&gt; under the hood and cancels outgoing requests when a query key changes or a component unmounts. Understanding the pattern still matters: it's why the library behaves correctly, and you'll need the manual version any time you write custom &lt;code&gt;fetch&lt;/code&gt; logic outside the library's boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Async code doesn't stop running because you stopped caring about it. A &lt;code&gt;fetch()&lt;/code&gt; in flight is a real resource — a server-side computation, a response that will arrive and call something — and if nothing cancels it, it will complete against whatever state it finds.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AbortController&lt;/code&gt; is the correct primitive for this. The cost is three lines: create a controller, pass &lt;code&gt;signal&lt;/code&gt; to &lt;code&gt;fetch&lt;/code&gt;, return &lt;code&gt;controller.abort&lt;/code&gt; from the cleanup. The return is eliminating an entire class of subtle, non-deterministic bugs.&lt;/p&gt;

&lt;p&gt;If you've ever filed a race condition as "can't reliably reproduce," there's a good chance the unfiled fix is a missing &lt;code&gt;controller.abort()&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 &lt;strong&gt;LinkedIn&lt;/strong&gt; — &lt;a href="https://www.linkedin.com/in/parsa-jiravand/" rel="noopener noreferrer"&gt;linkedin.com/in/parsa-jiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✉️ &lt;strong&gt;Email&lt;/strong&gt; (work &amp;amp; contract inquiries): &lt;a href="mailto:bestpractice2026@gmail.com"&gt;bestpractice2026@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>react</category>
    </item>
    <item>
      <title>You reached for !important again. @layer is the real fix.</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Sat, 04 Jul 2026 08:44:01 +0000</pubDate>
      <link>https://dev.to/parsajiravand/you-reached-for-important-again-layer-is-the-real-fix-g8f</link>
      <guid>https://dev.to/parsajiravand/you-reached-for-important-again-layer-is-the-real-fix-g8f</guid>
      <description>&lt;p&gt;You typed &lt;code&gt;!important&lt;/code&gt;. Be honest — you did. The third-party reset was winning, your override wasn't sticking, the clock was ticking, and &lt;code&gt;!important&lt;/code&gt; made the red squiggle go away.&lt;/p&gt;

&lt;p&gt;I've done it too. I've added an ID selector I didn't want, inlined a style I'd be ashamed of, and stacked &lt;code&gt;!important&lt;/code&gt; on &lt;code&gt;!important&lt;/code&gt; like sandbags against a flood.&lt;/p&gt;

&lt;p&gt;Here's what took me too long to see: none of that was a selector problem. It was an architecture problem wearing a selector costume. And &lt;code&gt;@layer&lt;/code&gt; is the fix that's been in every browser since 2022 while we kept reaching for the sandbags. Let me show you why the fight was unwinnable — and then how a single line at the top of your stylesheet ends it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you keep losing the specificity war
&lt;/h2&gt;

&lt;p&gt;Picture your stylesheet's contents thrown into one room: the third-party reset, your base typography, component styles, utility classes, that library you imported. They're all in the same cascade, all negotiating through one currency — selector weight. Whoever writes the heavier selector wins.&lt;/p&gt;

&lt;p&gt;That's the whole problem. There's no &lt;em&gt;ordering of concerns&lt;/em&gt;, just a brawl refereed by specificity. So when an imported &lt;code&gt;section &amp;gt; main .btn&lt;/code&gt; outweighs your &lt;code&gt;.btn&lt;/code&gt;, your only moves are to out-specify it or detonate &lt;code&gt;!important&lt;/code&gt;. You're not fixing anything. You're escalating an arms race you started by having concerns share a room with no rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  One line that changes the currency
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@layer&lt;/code&gt; lets you name layers and fix their priority order &lt;em&gt;up front&lt;/em&gt;. A rule in a higher layer beats a rule in a lower layer &lt;strong&gt;regardless of specificity&lt;/strong&gt;.&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="c"&gt;/* later layers win — declare the order once, at the top */&lt;/span&gt;
&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read what that buys you. A bare &lt;code&gt;.btn&lt;/code&gt; in &lt;code&gt;utilities&lt;/code&gt; now beats &lt;code&gt;section &amp;gt; main &amp;gt; article .btn&lt;/code&gt; in &lt;code&gt;base&lt;/code&gt; — not because you found a heavier selector, but because the &lt;em&gt;layer order&lt;/em&gt; says utilities outrank base, and the cascade checks layer order &lt;strong&gt;before&lt;/strong&gt; it ever counts specificity.&lt;/p&gt;

&lt;p&gt;Specificity still breaks ties &lt;em&gt;within&lt;/em&gt; a layer. Between layers, your declared order is law. The currency changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The third-party reset problem, gone
&lt;/h2&gt;

&lt;p&gt;This is where it pays for itself on day one. Every project that pulls in a reset, Tailwind's preflight, or a component library has fought this exact fight: some imported rule outweighs yours, and you write overrides to undo work you didn't author.&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="c"&gt;/* shove third-party styles into low layers */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="sx"&gt;url("reset.css")&lt;/span&gt; &lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="sx"&gt;url("ui-library.css")&lt;/span&gt; &lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thirdparty&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c"&gt;/* yours, higher — wins automatically */&lt;/span&gt;
&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;a&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-link&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--radius-sm&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;!important&lt;/code&gt;. No escalation. Third party goes low, your code goes high, low always loses. And the rule is written &lt;em&gt;once&lt;/em&gt;, at the top, where the next person can read the whole hierarchy in five seconds instead of reverse-engineering it from forty scattered overrides.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four-layer pattern that fits most apps
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* lowest → highest */&lt;/span&gt;

&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;reset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;*,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&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;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&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="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--font-sans&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;a&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-link&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="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--radius-md&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.btn&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;inline-flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt; &lt;span class="m"&gt;1rem&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="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.hidden&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;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.sr-only&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="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&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;hidden&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;When &lt;code&gt;.card&lt;/code&gt; and &lt;code&gt;.hidden&lt;/code&gt; collide on one element, &lt;code&gt;utilities&lt;/code&gt; wins — not because of a cleverer selector, but because the architecture says so. The layer boundary is the referee you never had.&lt;/p&gt;

&lt;p&gt;Quick — before you scroll, predict this one: you write a plain &lt;code&gt;a { color: red }&lt;/code&gt; &lt;em&gt;outside&lt;/em&gt; every layer. Does it beat the &lt;code&gt;a&lt;/code&gt; rule inside your &lt;code&gt;base&lt;/code&gt; layer, or lose to it?&lt;/p&gt;

&lt;h2&gt;
  
  
  The answer trips everyone (and it's the migration path)
&lt;/h2&gt;

&lt;p&gt;It &lt;strong&gt;wins&lt;/strong&gt;. Styles written outside any named layer sit in an implicit unlayered group that outranks &lt;em&gt;every&lt;/em&gt; named layer. Feels backwards the first time. It's deliberate — and it's exactly what makes adoption safe.&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="k"&gt;@import&lt;/span&gt; &lt;span class="sx"&gt;url("reset.css")&lt;/span&gt; &lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c"&gt;/* unlayered — still beats the reset, exactly like before */&lt;/span&gt;
&lt;span class="nt"&gt;a&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-link&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;Your existing, un-migrated CSS keeps behaving precisely as it does today. So you adopt incrementally: wrap the third-party imports first, confirm the reset stops fighting you, then move your own styles into layers whenever you're ready. Nothing breaks on the day you start, because unlayered code keeps winning until you choose otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new question in DevTools
&lt;/h2&gt;

&lt;p&gt;One honest cost: debugging gains a dimension. "Why is this rule losing?" used to mean "find the higher-specificity rule." Now it can also mean "which layer is it in?" Chrome DevTools already groups the Styles panel by layer, so it's visible — but it's a new place to look.&lt;/p&gt;

&lt;p&gt;The upside outweighs it. Once layers are declared, you stop tracing specificity chains and start asking "is this rule in the right layer?" — a question with a clear, checkable answer instead of a math problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  So put the &lt;code&gt;!important&lt;/code&gt; down
&lt;/h2&gt;

&lt;p&gt;That sandbag you reached for? It treats a structural problem as a selector problem, which is why it never actually holds. Specificity battles are the &lt;em&gt;symptom&lt;/em&gt;. The disease is a stylesheet where every rule negotiates with every other rule through selector weight alone — and &lt;code&gt;@layer&lt;/code&gt; adds the dimension the cascade was always missing: explicit, author-controlled ordering of concerns.&lt;/p&gt;

&lt;p&gt;It's in Chrome 99, Firefox 97, and Safari 15.4 — shipping since early 2022. No polyfill, no build step. The migration is incremental and the safety net (unlayered wins) is built in.&lt;/p&gt;

&lt;p&gt;The takeaway for tomorrow: &lt;strong&gt;the next time your finger hovers over &lt;code&gt;!important&lt;/code&gt;, ask whether a layer boundary is the real fix.&lt;/strong&gt; It usually is.&lt;/p&gt;

&lt;p&gt;What's the worst specificity hack still load-bearing in your codebase right now — the &lt;code&gt;#app #main .thing.thing&lt;/code&gt; chain, the &lt;code&gt;!important&lt;/code&gt; nobody dares touch? Confess it in the comments and let's figure out which layer it belongs in.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 &lt;strong&gt;LinkedIn&lt;/strong&gt; — &lt;a href="https://www.linkedin.com/in/parsa-jiravand/" rel="noopener noreferrer"&gt;linkedin.com/in/parsa-jiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✉️ &lt;strong&gt;Email&lt;/strong&gt; (work &amp;amp; contract inquiries): &lt;a href="mailto:bestpractice2026@gmail.com"&gt;bestpractice2026@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The page transition you pay a library for is now free</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Fri, 03 Jul 2026 09:26:36 +0000</pubDate>
      <link>https://dev.to/parsajiravand/the-page-transition-you-pay-a-library-for-is-now-free-2e5d</link>
      <guid>https://dev.to/parsajiravand/the-page-transition-you-pay-a-library-for-is-now-free-2e5d</guid>
      <description>&lt;p&gt;Picture the last time you wired up a page transition. The leaving page's exit. The entering page's entrance. The awkward in-between where both exist at once. Maybe a &lt;code&gt;getBoundingClientRect()&lt;/code&gt; here, a hand-tuned &lt;code&gt;transform&lt;/code&gt; there, a library install to coordinate it all.&lt;/p&gt;

&lt;p&gt;That whole apparatus was for an effect a native app gets by existing.&lt;/p&gt;

&lt;p&gt;Here's the part that'll annoy you: the browser does it now. The simplest version — a smooth cross-fade across your entire site — costs exactly one CSS rule. The fancy version, where a thumbnail morphs into a full hero image as you navigate, costs two. And neither one needs a single line of JavaScript. Let me show you the one-rule version first, then build up to the morph.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule. The whole site cross-fades.
&lt;/h2&gt;

&lt;p&gt;If you've got a regular multi-page app — separate HTML documents per page — opt into automatic transitions with this:&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="k"&gt;@view-transition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;navigation&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it in your stylesheet. Navigate. Every page change is now a gentle cross-fade. No JS, no library, no event listeners, no coordination.&lt;/p&gt;

&lt;p&gt;What's the browser actually doing? It screenshots the outgoing page, runs the navigation, then animates from that screenshot to the new page — a 300ms opacity cross-fade by default. Exactly the baseline you'd have built by hand. And it's strictly opt-in: without that rule, navigation stays instant, same as always.&lt;/p&gt;

&lt;p&gt;That's the freebie. But "everything cross-fades" isn't why this API matters. Hold that thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model that makes the rest click
&lt;/h2&gt;

&lt;p&gt;Every view transition is the same three beats:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture&lt;/strong&gt; — the browser snapshots the current page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change&lt;/strong&gt; — your navigation (or DOM update) runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animate&lt;/strong&gt; — the browser builds two pseudo-elements, &lt;code&gt;::view-transition-old&lt;/code&gt; and &lt;code&gt;::view-transition-new&lt;/code&gt;, and tweens between them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the opposite of how a motion library thinks. A library makes you &lt;em&gt;describe the motion&lt;/em&gt;: this exits left, that enters right, here's the easing. View Transitions make you describe &lt;em&gt;what should transition&lt;/em&gt; — the browser figures out &lt;em&gt;how&lt;/em&gt; by diffing before against after.&lt;/p&gt;

&lt;p&gt;Want something other than a cross-fade? Style those pseudo-elements like anything 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="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;250ms&lt;/span&gt; &lt;span class="n"&gt;ease-in&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250ms&lt;/span&gt; &lt;span class="n"&gt;ease-in&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;250ms&lt;/span&gt; &lt;span class="n"&gt;ease-out&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;fade-in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;250ms&lt;/span&gt; &lt;span class="n"&gt;ease-out&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt; &lt;span class="n"&gt;slide-in&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-out&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;to&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-30px&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="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;slide-in&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30px&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;Now every navigation is a subtle directional slide. Still zero JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick worth the price of admission
&lt;/h2&gt;

&lt;p&gt;Here's the thought you parked earlier. The real payoff isn't the cross-fade — it's telling the browser &lt;em&gt;"this element on page A is the same element as that one on page B."&lt;/em&gt; Give them a shared name and the browser morphs one into the other.&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="c"&gt;/* list page */&lt;/span&gt;
&lt;span class="nc"&gt;.product-card&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"42"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product-hero-42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* detail page */&lt;/span&gt;
&lt;span class="nc"&gt;.product-hero-image&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product-hero-42&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;Navigate from the list to the detail page and the thumbnail flies into place as the full hero — position, size, shape, all interpolated. You wrote no animation. You declared an &lt;em&gt;identity&lt;/em&gt;, and the browser handled every pixel of geometry.&lt;/p&gt;

&lt;p&gt;Be honest: how much code did this take last time? &lt;code&gt;getBoundingClientRect()&lt;/code&gt; on both elements, the delta math, a &lt;code&gt;transform&lt;/code&gt; to fake the start position, timing to undo it. A few hundred lines, easy. Here it's two rules whose names match.&lt;/p&gt;

&lt;p&gt;One gotcha: &lt;code&gt;view-transition-name&lt;/code&gt; must be unique in the DOM at any instant. Rendering a list of cards? Set the name dynamically so only the card being navigated carries it, or scope it with &lt;code&gt;:has()&lt;/code&gt; to the active state.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPAs get the same engine, behind a function
&lt;/h2&gt;

&lt;p&gt;If you own navigation programmatically, the JS API exposes that same capture-change-animate cycle:&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:&lt;/span&gt;
&lt;span class="nf"&gt;updatePageContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// after:&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;startViewTransition&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;updatePageContent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass a callback that mutates the DOM. The browser captures before, runs your function, captures after, plays the transition. Your update logic doesn't change — you wrap it. React Router and the Next.js App Router are threading this into their navigation primitives, but underneath, the primitive never changes: snapshot, update, animate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding it can't break anything
&lt;/h2&gt;

&lt;p&gt;This is the part that should make you comfortable shipping it today: it's purely additive. Browsers without support — and the holdouts are thinning, with Chrome, Firefox, and Safari all shipping it — just navigate instantly, like they always did. The guard is 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;if &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="nx"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;)&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;startViewTransition&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;updatePageContent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;updatePageContent&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 polyfill, no fallback CSS, no broken state. The browser plays the transition or it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits — and where it doesn't
&lt;/h2&gt;

&lt;p&gt;Reach for View Transitions on &lt;strong&gt;navigational context changes&lt;/strong&gt;: moving between pages, opening a detail from a list, expanding an inline panel. "You're going from here to there" maps onto the API naturally.&lt;/p&gt;

&lt;p&gt;Don't reach for it on &lt;strong&gt;micro-interactions inside a view&lt;/strong&gt;: hovers, spinners, a toggle's state change, a choreographed timeline. Those want CSS &lt;code&gt;transition&lt;/code&gt;/&lt;code&gt;animation&lt;/code&gt; or a real motion library where you control every keyframe.&lt;/p&gt;

&lt;p&gt;The line: animation driven by &lt;em&gt;navigation between views&lt;/em&gt; → View Transitions. Animation driven by &lt;em&gt;interaction within a view&lt;/em&gt; → regular CSS or a library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to that library you were about to install
&lt;/h2&gt;

&lt;p&gt;The honest scope: View Transitions won't replace intricate choreography — that still needs more. What they replace is the &lt;em&gt;default&lt;/em&gt; case. The clean cross-fade. The hero morph. The transition every multi-page app should have and usually skips, because nobody wanted to pull in Framer Motion for something that basic.&lt;/p&gt;

&lt;p&gt;Takeaway for tomorrow: &lt;strong&gt;before you add an animation library for navigation, ask what one CSS rule already buys you.&lt;/strong&gt; Often it's most of what you needed — and the rest is two matching names.&lt;/p&gt;

&lt;p&gt;What's the page transition you abandoned because wiring it up wasn't worth it? Try the one-rule version on it tonight and tell me how close it got in the comments.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>:has() isn't a parent selector. It deletes JavaScript.</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Thu, 02 Jul 2026 07:18:03 +0000</pubDate>
      <link>https://dev.to/parsajiravand/has-isnt-a-parent-selector-it-deletes-javascript-4hej</link>
      <guid>https://dev.to/parsajiravand/has-isnt-a-parent-selector-it-deletes-javascript-4hej</guid>
      <description>&lt;p&gt;Open your codebase and search for &lt;code&gt;classList.toggle&lt;/code&gt;. Go ahead, I'll wait.&lt;/p&gt;

&lt;p&gt;I'd bet a real chunk of those hits are the same little chore: the user checks a box, focuses a field, or makes a selection, and you respond by toggling a class on some &lt;em&gt;parent&lt;/em&gt; element so its styling can react. A listener, a class, matching CSS for that class. You've written that dance dozens of times.&lt;/p&gt;

&lt;p&gt;Most of it can be deleted. Not refactored — deleted. The reason it survives is that everyone learned &lt;code&gt;:has()&lt;/code&gt; as "the parent selector," and that label quietly hides what it actually is. Stick with me and you'll never write that class-toggle the same way again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "parent selector" is the wrong name
&lt;/h2&gt;

&lt;p&gt;Yes, &lt;code&gt;:has()&lt;/code&gt; can style a parent based on a child. But pin it to that one trick and you'll miss the whole feature. &lt;code&gt;:has()&lt;/code&gt; tests a &lt;em&gt;condition&lt;/em&gt; — any selector you'd normally write, evaluated against an element's contents instead of the element itself. The thing making it true can be a descendant, a sibling, a count, or the &lt;em&gt;absence&lt;/em&gt; of something.&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="c"&gt;/* a parent based on a child — the famous case */&lt;/span&gt;
&lt;span class="nc"&gt;.card&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* focus state buried deep in the subtree */&lt;/span&gt;
&lt;span class="nc"&gt;.form-group&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* checked state */&lt;/span&gt;
&lt;span class="nc"&gt;.option-row&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--selected-bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* absence — the logical complement */&lt;/span&gt;
&lt;span class="nc"&gt;.card&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* count — restyle a grid by how many children it holds */&lt;/span&gt;
&lt;span class="nc"&gt;.grid&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&amp;gt;&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;4&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&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;Stare at that last one. You're changing a grid's layout &lt;em&gt;based on how many items it contains&lt;/em&gt; — in pure CSS, no JavaScript, no server-rendered class. That's not "selecting a parent." That's a conditional reading live DOM state. Different mental model entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The class of JavaScript that just disappears
&lt;/h2&gt;

&lt;p&gt;Here's the pattern &lt;code&gt;:has()&lt;/code&gt; retires. You've written this:&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;// The old bridge: JS exists only to move a class up the tree&lt;/span&gt;
&lt;span class="nx"&gt;checkbox&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;change&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-checked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;checkbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&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;Why does this code exist at all? Because CSS selectors only ever flowed &lt;em&gt;down&lt;/em&gt; — a parent could style its children, but a child could never trigger a style on its ancestor. So we hired JavaScript as a messenger. &lt;code&gt;:has()&lt;/code&gt; makes the messenger redundant:&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="c"&gt;/* The whole listener, gone */&lt;/span&gt;
&lt;span class="nc"&gt;.option-row&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"checkbox"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--selected-bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&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 listener is gone. The class is gone. And — this is the part that's easy to undervalue — the style is now &lt;em&gt;true whenever the DOM is in that state&lt;/em&gt;. On first render. After a back-navigation. When state is restored from a URL. There's no window where the DOM has updated but the class hasn't caught up yet, because there's no class to catch up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three you can paste in today
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Field-group validation.&lt;/strong&gt; Light up the whole group — label, input, hint — when it holds an input the user touched and left invalid:&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;.field-group&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="nd"&gt;:invalid:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:placeholder-shown&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.field-group&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="nd"&gt;:invalid:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:placeholder-shown&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="nt"&gt;label&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-error&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 used to be a &lt;code&gt;blur&lt;/code&gt; listener, a wrapper class, and matching CSS. Now: two rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty state.&lt;/strong&gt; Show a placeholder when a list has no items:&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;.todo-list&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;.empty-state&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No JS tracking item counts. The empty state appears the moment the last &lt;code&gt;li&lt;/code&gt; leaves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkbox-driven panel.&lt;/strong&gt; A toggle reveals its settings panel:&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;.settings-section&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"checkbox"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;.settings-panel&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;grid&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 browser's own form state is the source of truth, and CSS reads it directly. No syncing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one place to slow down
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;:has()&lt;/code&gt; isn't free. Matching a class is the browser reading an attribute; matching &lt;code&gt;:has()&lt;/code&gt; means evaluating the inner selector against descendants. For static content and low-frequency state — validation, toggles, checked state — the cost is imperceptible. Ship it.&lt;/p&gt;

&lt;p&gt;Where to be deliberate: high-frequency, continuously-changing inputs. &lt;code&gt;:has()&lt;/code&gt; keyed off scroll position, or nested inside &lt;code&gt;:hover&lt;/code&gt; on a list of thousands of rows. The rule of thumb: anywhere you'd have reached for a &lt;code&gt;debounce&lt;/code&gt; on the JS side, give the &lt;code&gt;:has()&lt;/code&gt; version a second look and profile it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which JS is still pretending to be CSS?
&lt;/h2&gt;

&lt;p&gt;Back to that &lt;code&gt;classList.toggle&lt;/code&gt; search you ran. The framing that unlocks the cleanup isn't "where can I select a parent" — it's this: &lt;strong&gt;whenever you catch yourself toggling a class purely to drive styling off some child's state, stop and ask whether &lt;code&gt;:has()&lt;/code&gt; expresses that condition directly.&lt;/strong&gt; More often than you'd guess, it does. When it does, the CSS shrinks, the JS evaporates, and the styling is correct from the very first paint because it's &lt;em&gt;reading&lt;/em&gt; state instead of &lt;em&gt;reacting&lt;/em&gt; to it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;:has()&lt;/code&gt; is in Chrome, Firefox, Safari, and Edge — no flag, no polyfill, no waiting. It's not a parent selector. It's a stylesheet that can finally read its own document.&lt;/p&gt;

&lt;p&gt;So go run the search. How many of your &lt;code&gt;classList.toggle&lt;/code&gt; calls are really a &lt;code&gt;:has()&lt;/code&gt; rule in a trench coat? Drop the gnarliest one in the comments and let's rewrite it.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your Save button waits 300ms for nothing. Stop it.</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Wed, 01 Jul 2026 07:42:41 +0000</pubDate>
      <link>https://dev.to/parsajiravand/your-save-button-waits-300ms-for-nothing-stop-it-4954</link>
      <guid>https://dev.to/parsajiravand/your-save-button-waits-300ms-for-nothing-stop-it-4954</guid>
      <description>&lt;p&gt;Tap the heart on Instagram. It fills in red before your finger leaves the glass. Swipe to archive in Gmail — gone, instantly. Neither app waited for a server to say "okay." They assumed success and moved on.&lt;/p&gt;

&lt;p&gt;Now open the app you're building and click "Save." Watch the button freeze. One-Mississippi. The spinner. &lt;em&gt;Then&lt;/em&gt; the UI updates.&lt;/p&gt;

&lt;p&gt;That lag isn't the network being slow. It's a decision your code made: wait for the server before showing the user anything. You can make the other decision. The instant version is maybe four lines of code — but if you stop there, you've built a beautiful lie that desyncs the first time the network hiccups.&lt;/p&gt;

&lt;p&gt;The fast part is easy. The part everyone gets wrong is what happens when the server says no.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the slow version actually costs
&lt;/h2&gt;

&lt;p&gt;Here's the pattern in nearly every codebase. Wait, then reflect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pessimistic: nothing happens on screen until the server answers&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;handleLike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&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;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;likePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 300ms of dead air&lt;/span&gt;
  &lt;span class="nf"&gt;setLiked&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="nf"&gt;setLoading&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;The user taps. Nothing. Then the heart turns red. Technically correct, feels sluggish — and ~300ms is roughly the point where an interaction stops feeling like a direct response and starts feeling like a &lt;em&gt;request you submitted&lt;/em&gt;. Worse, this fires on every single tap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Act first, apologize later
&lt;/h2&gt;

&lt;p&gt;Flip the order. Update the UI as if it already worked, then quietly reconcile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Optimistic: change now, roll back only if the server disagrees&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;handleLike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setLiked&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;// instant&lt;/span&gt;
  &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&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="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="c1"&gt;// instant&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="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;likePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&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="nf"&gt;setLiked&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;// rollback&lt;/span&gt;
    &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&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="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="c1"&gt;// rollback&lt;/span&gt;
    &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;Couldn't save — try again.&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 heart fills the instant you tap. On the 99% happy path, the &lt;code&gt;await&lt;/code&gt; resolves and nothing visible happens — the optimistic state was right. On the rare failure, the heart goes grey and a toast explains why.&lt;/p&gt;

&lt;p&gt;Spot what's carrying the whole pattern? It's not the two &lt;code&gt;set&lt;/code&gt; calls at the top. Anyone writes those. It's the &lt;code&gt;catch&lt;/code&gt; block — the part that knows exactly how to undo what it just did. That's the half people skip, and it's the half that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where manual rollback falls apart
&lt;/h2&gt;

&lt;p&gt;A toggle is easy: one boolean to flip back. But the moment you're optimistically editing a &lt;em&gt;list&lt;/em&gt; — adding, removing, reordering — manual rollback turns into a bookkeeping nightmare. What was the array before? What if a background refetch lands mid-mutation?&lt;/p&gt;

&lt;p&gt;This is exactly the mess TanStack Query's mutation lifecycle is shaped to clean up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;newTodo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Todo&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTodo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

  &lt;span class="na"&gt;onMutate&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;newTodo&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;// Stop in-flight refetches from clobbering our optimistic write&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&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;todos&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;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQueryData&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;          &lt;span class="c1"&gt;// snapshot&lt;/span&gt;
    &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQueryData&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;todos&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;old&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Todo&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;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newTodo&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;                                          &lt;span class="c1"&gt;// apply&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;                                           &lt;span class="c1"&gt;// stash for rollback&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_newTodo&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQueryData&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// restore&lt;/span&gt;
    &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;Failed to add todo.&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;onSettled&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="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&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;todos&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="c1"&gt;// re-sync&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as four jobs, one each: &lt;strong&gt;snapshot → apply → restore-if-error → sync-always.&lt;/strong&gt; &lt;code&gt;onMutate&lt;/code&gt; saves and applies. &lt;code&gt;onError&lt;/code&gt; puts the snapshot back. &lt;code&gt;onSettled&lt;/code&gt; re-fetches no matter what, so the screen and the server agree at the end.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;cancelQueries&lt;/code&gt; line is the one everyone forgets, and it bites. Without it, a refetch that was already in flight can finish &lt;em&gt;after&lt;/em&gt; your optimistic write and overwrite it with stale data — a flicker that's maddening to debug because it only shows up under the right timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actions you should never fake
&lt;/h2&gt;

&lt;p&gt;Optimistic UI is not free confidence. It fits &lt;strong&gt;low-stakes, high-frequency&lt;/strong&gt; actions where failure is rare and undo is clean: likes, favorites, checking off a task, toggling a setting.&lt;/p&gt;

&lt;p&gt;It's the wrong tool when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The action is irreversible.&lt;/strong&gt; Placing an order, sending a message, deleting an account. Make the user wait and confirm — don't paper over real consequences with a fake "done."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server computes the result.&lt;/strong&gt; A generated ID, a recalculated total, a merged state — you can't honestly predict it, so don't pretend to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries aren't safe.&lt;/strong&gt; If a duplicate call could double-charge, slow down on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My one-line test: would the user be upset to learn it silently failed? Spinner. Would they just shrug and tap again? Go optimistic.&lt;/p&gt;

&lt;h2&gt;
  
  
  So why does the Save button still wait?
&lt;/h2&gt;

&lt;p&gt;Because the happy path was the easy 90% and the team never built the other 10%. That's the whole story. Every implementation nails "update the UI fast." The rollback — snapshot, restore, tell the user — is what separates a polished app from one that quietly lies to people.&lt;/p&gt;

&lt;p&gt;The takeaway for tomorrow's standup: &lt;strong&gt;optimistic updates aren't a library, they're a discipline — always handle the failure path explicitly.&lt;/strong&gt; Snapshot before you touch state. Restore on error. Say something went wrong. Most users will never see those three steps. The ones who do will trust you more, not less.&lt;/p&gt;

&lt;p&gt;What's the worst optimistic-update desync you've shipped — the ghost item, the double-count, the flicker that wouldn't reproduce? I'll go first in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>react</category>
    </item>
    <item>
      <title>Your card breaks in the sidebar. Here's the real fix.</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Tue, 30 Jun 2026 07:30:00 +0000</pubDate>
      <link>https://dev.to/parsajiravand/container-queries-the-responsive-primitive-we-should-have-had-all-along-46dn</link>
      <guid>https://dev.to/parsajiravand/container-queries-the-responsive-primitive-we-should-have-had-all-along-46dn</guid>
      <description>&lt;p&gt;You built a card. Image, title, a line of copy. In the main content column it's gorgeous — image left, text right, everything breathing.&lt;/p&gt;

&lt;p&gt;Then someone drops it into the sidebar.&lt;/p&gt;

&lt;p&gt;Now the image is squashed next to three words per line, the title wraps four times, and the whole thing looks broken. You open DevTools, check your breakpoints, and they're all firing correctly. The viewport &lt;em&gt;is&lt;/em&gt; wide. So why does your card look like it's having a stroke?&lt;/p&gt;

&lt;p&gt;Here's the punchline up front: the bug isn't in your CSS. It's in the question your CSS is allowed to ask. And the fix is one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question media queries can't answer
&lt;/h2&gt;

&lt;p&gt;For fifteen years, the only responsive question we could ask was "how wide is the window?" That's what a media query is. Resize the viewport, cross a breakpoint, restyle.&lt;/p&gt;

&lt;p&gt;Which works fine — right up until you build a component meant to live in more than one place.&lt;/p&gt;

&lt;p&gt;Your card doesn't care how wide the &lt;em&gt;window&lt;/em&gt; is. It cares how wide &lt;em&gt;it&lt;/em&gt; is. And those two numbers have nothing to do with each other. A 1400px desktop can hand your card a 220px sidebar. The media query sees "1400px — go wide!" and lays out side-by-side in a slot that's narrower than the image.&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="c"&gt;/* The card asks the window. The window has no idea where the card is. */&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200px&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&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;Quick gut check before you scroll: how have you solved this before? Be honest.&lt;/p&gt;

&lt;p&gt;If you're like most of us, the answer is one of three hacks: a &lt;code&gt;compact&lt;/code&gt; prop threaded down through five components, a &lt;code&gt;ResizeObserver&lt;/code&gt; wired up in JavaScript, or two near-identical card variants you swap by hand. I've shipped all three. They're all workarounds for the same missing primitive — a way for a component to ask &lt;em&gt;its own container&lt;/em&gt;, not the window, how much room it's got.&lt;/p&gt;

&lt;h2&gt;
  
  
  One declaration changes the question
&lt;/h2&gt;

&lt;p&gt;That primitive shipped. It's called a container query, and it's in every major browser — Chrome, Firefox, Safari, Edge — no flag, no polyfill.&lt;/p&gt;

&lt;p&gt;You do two things. Mark a parent as a container. Query &lt;em&gt;that&lt;/em&gt; instead of the viewport.&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="c"&gt;/* 1. Tell the parent it's a query container */&lt;/span&gt;
&lt;span class="nc"&gt;.card-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;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* 2. The compact default — image on top */&lt;/span&gt;
&lt;span class="nc"&gt;.card&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;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* 3. Go side-by-side only when MY container has the room */&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200px&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;grid-template-rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unset&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;Read that &lt;code&gt;@container&lt;/code&gt; line out loud: "when &lt;em&gt;my container&lt;/em&gt; is at least 500px, go wide." Not the window. The container.&lt;/p&gt;

&lt;p&gt;Now drop the exact same card into the 220px sidebar and it stays stacked, because its container is 220px. Drop it into the wide grid and it opens up. One component, two layouts, zero JavaScript, zero props. The card finally knows where it lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the layout nests, name the container
&lt;/h2&gt;

&lt;p&gt;Sometimes the thing you want to respond to isn't the immediate parent — it's a layout region three levels up. Name your containers and query the one you mean:&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;.sidebar&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;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;container-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.main&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;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;container-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* This title responds to the sidebar specifically */&lt;/span&gt;
&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;sidebar&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card-title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25rem&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 old way to make a deeply nested element aware of a high-level region was a &lt;code&gt;ResizeObserver&lt;/code&gt;, a React context, and a prop-drilling session. Here it's two declarations. That's the whole feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  So is &lt;code&gt;@media&lt;/code&gt; dead? No — and here's the line
&lt;/h2&gt;

&lt;p&gt;This is the part people get wrong: container queries don't replace media queries. They have different jobs, and mixing them up is its own mess.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Media query&lt;/strong&gt; → page-level structure. One column vs. two. Collapsing the nav. A full-bleed hero. Anything that genuinely answers to the &lt;em&gt;viewport&lt;/em&gt; — including print and &lt;code&gt;prefers-reduced-motion&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container query&lt;/strong&gt; → anything reusable that gets dropped into varying space. Cards, tables, form fields, media objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The heuristic that's never failed me: &lt;strong&gt;the layout skeleton is a media query; everything you hang on it is a container query.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bonus you'll notice in Storybook
&lt;/h2&gt;

&lt;p&gt;Here's a side effect I didn't expect. Build a card with media queries inside it and your Storybook preview &lt;em&gt;lies&lt;/em&gt; — it shows the component at the story frame's viewport width, not the width it'll get in production. The preview looks fine; the real layout breaks.&lt;/p&gt;

&lt;p&gt;Swap to container queries and the preview tells the truth. Resize the story panel and the card adapts, because "context" now lives in the CSS instead of being inferred from the window. What you see in isolation is what you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix was one line — and a better question
&lt;/h2&gt;

&lt;p&gt;Remember the broken sidebar card? &lt;code&gt;container-type: inline-size&lt;/code&gt; on its wrapper. That's it. The layout you already wrote starts working the moment the card can see its own container instead of guessing from the window.&lt;/p&gt;

&lt;p&gt;The takeaway you can repeat to a teammate tomorrow: &lt;strong&gt;stop asking "how wide is the window?" and start asking "how wide am I?"&lt;/strong&gt; Once the question changes, the component designs itself.&lt;/p&gt;

&lt;p&gt;So — how many &lt;code&gt;compact&lt;/code&gt; props are still threaded through your codebase right now? Go count. I bet most of them want to be a single &lt;code&gt;@container&lt;/code&gt; rule. Tell me what you find in the comments.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The URL Is Your Best State Manager</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Mon, 29 Jun 2026 07:59:09 +0000</pubDate>
      <link>https://dev.to/parsajiravand/the-url-is-your-best-state-manager-5hl8</link>
      <guid>https://dev.to/parsajiravand/the-url-is-your-best-state-manager-5hl8</guid>
      <description>&lt;p&gt;There's a category of bug I see in almost every SPA I touch: the user applies a filter, sorts a table, pages through results — and then refreshes, or pastes the link to a colleague, and all of it is gone. The bug report says "it doesn't remember my state." The root cause is almost always the same: that state lived in &lt;code&gt;useState&lt;/code&gt;, when it should have lived in the URL.&lt;/p&gt;

&lt;p&gt;This is a mental model, not a framework feature. It applies equally in React, Vue, Svelte, or plain JS. The URL is a state management primitive you already have — built into every browser, shared for free — and most frontend teams underuse it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What belongs in the URL
&lt;/h2&gt;

&lt;p&gt;Not all state belongs there. The key question is: &lt;strong&gt;would a user want to share or bookmark this?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;URL?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Active tab / current view&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Direct-linkable, shareable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search query&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Back button works, shareable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filters, sort order&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Reproducible results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pagination cursor&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;"Send me the page you're on"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form draft in progress&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Ephemeral, user-session only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hover state, tooltip open&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Ephemeral UI state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth tokens&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Security hazard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The filter and the modal-open state are both "UI state," but they're different in kind. One the user would share; one they wouldn't. Put the first in the URL and leave the second in component state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you gain for free
&lt;/h2&gt;

&lt;p&gt;When search/filter/sort state lives in the URL, you get three things without writing a single extra line of code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Refresh survival.&lt;/strong&gt; The user filters a table, refreshes — the filter is still there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shareable links.&lt;/strong&gt; "Here's what I'm looking at" works via copy-paste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser history.&lt;/strong&gt; Back and forward navigate state transitions, which is what users expect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are not small wins. "Back button broke my filters" is a real UX bug, and moving to URL state fixes it completely and permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern in React
&lt;/h2&gt;

&lt;p&gt;Here's the before/after for a search input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: state dies on refresh&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchPage&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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setQuery&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After: state lives in the URL&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;useSearchParams&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;react-router-dom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// or Next.js's useSearchParams&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchPage&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;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setParams&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSearchParams&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&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;q&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;setParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q&lt;/span&gt;&lt;span class="dl"&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="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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;The component is roughly the same size. The URL is now &lt;code&gt;?q=typescript&lt;/code&gt;. Refresh — still there. Copy the URL — the recipient sees the same results. The back button undoes the last keystroke (or use &lt;code&gt;replace&lt;/code&gt; instead of &lt;code&gt;push&lt;/code&gt; to keep history clean for every keystroke).&lt;/p&gt;

&lt;p&gt;For more complex state — multiple filters, sort columns, numeric values — a library like &lt;a href="https://nuqs.47ng.com/" rel="noopener noreferrer"&gt;nuqs&lt;/a&gt; handles serialization, type coercion, and history mode in a &lt;code&gt;useState&lt;/code&gt;-compatible API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useQueryState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parseAsInteger&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;nuqs&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;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSort&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sort&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;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;date&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;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parseAsInteger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withDefault&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typed URL params, familiar API, zero custom serialization logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode to watch for
&lt;/h2&gt;

&lt;p&gt;Don't put too much in the URL. It's for state the user would meaningfully share — not for every piece of transient UI. A deeply nested JSON blob in a query string is the wrong answer. Keep it flat, keep it readable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;?status=active&amp;amp;sort=name&amp;amp;page=2&lt;/code&gt; is good. &lt;code&gt;?state=eyJzdGF0dXMiOiJhY3RpdmUi...&lt;/code&gt; is a red flag.&lt;/p&gt;

&lt;p&gt;If your state is too complex to flatten into a few readable params, it may genuinely belong in a database or session store, not the URL. The URL should be a &lt;em&gt;narrow&lt;/em&gt; representation of "what the user is looking at" — not a full snapshot of the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The URL is a first-class state management layer that every browser already implements, every user already understands, and most SPAs leave mostly unused. Before you reach for &lt;code&gt;useState&lt;/code&gt;, ask: would the user want to share or bookmark this? If yes, put it in the URL — you get refresh survival, deep links, and a working back button for free.&lt;/p&gt;

&lt;p&gt;That's not a third-party library or a framework feature. It's how the web was designed. Use it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>react</category>
    </item>
    <item>
      <title>Server Components Without the Hype: A Mental Model That Sticks</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:41:34 +0000</pubDate>
      <link>https://dev.to/parsajiravand/server-components-without-the-hype-a-mental-model-that-sticks-4k5e</link>
      <guid>https://dev.to/parsajiravand/server-components-without-the-hype-a-mental-model-that-sticks-4k5e</guid>
      <description>&lt;p&gt;React Server Components confused a lot of people, and most of the confusion comes from pattern-matching them onto things we already knew — "oh, it's just SSR," or "it's like &lt;code&gt;getServerSideProps&lt;/code&gt;." It isn't, quite. Here's a mental model that actually sticks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one distinction that matters
&lt;/h2&gt;

&lt;p&gt;Forget rendering for a second. The real split is &lt;strong&gt;where a component's code lives and runs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Server Component&lt;/strong&gt; runs &lt;em&gt;only&lt;/em&gt; on the server. Its code never ships to the browser. It can read your database, touch the filesystem, use secrets — and then it disappears, leaving behind only the UI it produced.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Client Component&lt;/strong&gt; is the React you already know: its code ships to the browser, and it can use state, effects, and event handlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole core idea. Server Components are components whose JavaScript the user never downloads. Everything else follows from that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is different from SSR
&lt;/h2&gt;

&lt;p&gt;Classic SSR renders your &lt;em&gt;entire&lt;/em&gt; app to HTML on the server, then ships &lt;em&gt;all&lt;/em&gt; the JS to the browser to "hydrate" it — make it interactive. You paid for the HTML &lt;em&gt;and&lt;/em&gt; the full bundle.&lt;/p&gt;

&lt;p&gt;RSC lets you say: this part of the tree is just display — render it on the server and &lt;strong&gt;don't send its code at all.&lt;/strong&gt; Only the genuinely interactive bits become Client Components and ship their JS. The static 80% of a typical page stops costing you bundle size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server Component — runs on the server, ships zero JS to the client.&lt;/span&gt;
&lt;span class="c1"&gt;// It can await data directly. No useEffect, no loading spinner plumbing.&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;ArticlePage&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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="c1"&gt;// safe: never runs in the browser&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LikeButton&lt;/span&gt; &lt;span class="na"&gt;articleId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* the ONE interactive island */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// this directive marks the boundary — code below ships to the browser&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LikeButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;articleId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;articleId&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="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;liked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLiked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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="nf"&gt;setLiked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;liked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;liked&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;♡&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;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 page's data-fetching and markup rendering cost zero client JS. Only &lt;code&gt;LikeButton&lt;/code&gt; — the part that needs to &lt;em&gt;do&lt;/em&gt; something on click — gets shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that trip people up (and why they exist)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server Components can't use &lt;code&gt;useState&lt;/code&gt;/&lt;code&gt;useEffect&lt;/code&gt;/event handlers.&lt;/strong&gt; They don't exist on the client, so there's no "interactive" lifecycle to hook into. If you need interactivity, you've found a Client Component.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can pass a Server Component &lt;em&gt;into&lt;/em&gt; a Client Component as &lt;code&gt;children&lt;/code&gt;, but you can't import one into a client module.&lt;/strong&gt; Because the moment you &lt;code&gt;import&lt;/code&gt; it into client code, its server-only code would have to ship — defeating the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;"use client"&lt;/code&gt; marks a boundary, not a file-by-file choice.&lt;/strong&gt; Everything imported below that boundary is in client-land too. Put the directive as deep in the tree as you can.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you internalize "Server Components are the code that never ships," every one of those rules stops feeling arbitrary and starts feeling obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to actually reach for it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default to Server Components.&lt;/strong&gt; Make a component a Client Component only when it needs state, effects, browser APIs, or event handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push the &lt;code&gt;"use client"&lt;/code&gt; boundary down.&lt;/strong&gt; A whole page marked client-side throws away the benefit; an interactive leaf node keeps it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't fight it for purely static sites.&lt;/strong&gt; If there's barely any interactivity, a simpler static-generation setup may be all you need — RSC earns its complexity on data-heavy, partially-interactive apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Server Components aren't "SSR with extra steps." They're a way to write parts of your UI whose code &lt;em&gt;never leaves the server&lt;/em&gt; — so the browser downloads only the JavaScript that genuinely needs to run there. Hold onto that one sentence and the rest of the model assembles itself.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>TypeScript Won. Here's What That Actually Bought Us.</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Sun, 28 Jun 2026 07:31:53 +0000</pubDate>
      <link>https://dev.to/parsajiravand/typescript-won-heres-what-that-actually-bought-us-53lo</link>
      <guid>https://dev.to/parsajiravand/typescript-won-heres-what-that-actually-bought-us-53lo</guid>
      <description>&lt;p&gt;Nobody seriously argues about adopting TypeScript anymore. New frontend projects default to it; the holdouts are legacy codebases and the occasional throwaway script. The debate is over, TypeScript won.&lt;/p&gt;

&lt;p&gt;But "won" is the boring part. The interesting part is &lt;em&gt;what types turned out to be good for&lt;/em&gt; — which is more, and different, than the original pitch of "catch typos before runtime."&lt;/p&gt;

&lt;h2&gt;
  
  
  Types are the cheapest documentation you'll never have to update
&lt;/h2&gt;

&lt;p&gt;A function signature is documentation that can't go stale, because the compiler fails the build the moment it lies.&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;function&lt;/span&gt; &lt;span class="nf"&gt;scheduleReminder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;userId&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="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ReminderId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You already know almost everything about calling this without reading a single comment: what it needs, what it returns, that &lt;code&gt;channel&lt;/code&gt; is one of exactly three strings. A comment claiming the same things could rot the next time someone adds a &lt;code&gt;"slack"&lt;/code&gt; channel and forgets to update it. The type &lt;em&gt;can't&lt;/em&gt; — adding &lt;code&gt;"slack"&lt;/code&gt; to the union forces every call site to be reconsidered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types make refactoring a mechanical activity instead of an act of courage
&lt;/h2&gt;

&lt;p&gt;In a large untyped codebase, renaming a widely-used field is genuinely scary — you're grepping strings and praying. In a typed one, you change the type and the compiler hands you the complete to-do list of everything that broke. Refactoring stops being "risky" and becomes "tedious but safe," which is exactly the trade you want. Whole categories of "we can't touch that, it's too entangled" simply dissolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff nobody planned: types are the contract AI codegen needs
&lt;/h2&gt;

&lt;p&gt;Here's the part that wasn't in the original sales pitch. The more machine-readable your boundaries are, the more reliably an AI assistant can operate inside them.&lt;/p&gt;

&lt;p&gt;Ask a model to "add a field to this object" in untyped JavaScript and it's guessing at shape from usage. Ask it in TypeScript and the type &lt;em&gt;is&lt;/em&gt; the spec — it knows precisely what's allowed, and its mistakes surface as compile errors instead of 2am production incidents. Types turn "generate plausible code" into "generate code that provably fits."&lt;/p&gt;

&lt;p&gt;This flips an old objection on its head. People used to say types slowed them down. In an AI-assisted workflow, types &lt;em&gt;speed you up&lt;/em&gt;, because they're the guardrail that lets you accept generated code with confidence instead of auditing every line by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few habits that compound
&lt;/h2&gt;

&lt;p&gt;If types are this leverage-rich, it's worth writing them with intent rather than appeasing the compiler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer unions over booleans + optionals.&lt;/strong&gt; &lt;code&gt;status: "loading" | "error" | "ready"&lt;/code&gt; beats three independent boolean flags that can contradict each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name your domain types.&lt;/strong&gt; &lt;code&gt;type Cents = number&lt;/code&gt; documents intent at every use site and lets you tighten it later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid &lt;code&gt;any&lt;/code&gt;; reach for &lt;code&gt;unknown&lt;/code&gt; and narrow.&lt;/strong&gt; &lt;code&gt;any&lt;/code&gt; is a hole in exactly the safety net you're paying for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let inference work.&lt;/strong&gt; You don't need to annotate everything — annotate the &lt;em&gt;boundaries&lt;/em&gt; (function signatures, exported APIs) and let the rest flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;TypeScript's real win wasn't catching typos. It was turning your codebase into something with &lt;em&gt;explicit, enforced contracts&lt;/em&gt; — and contracts turn out to be exactly what fearless refactoring, reliable tooling, and trustworthy AI assistance all quietly depend on.&lt;/p&gt;

&lt;p&gt;We adopted types to prevent a class of bugs. We're keeping them because they're the substrate everything else now builds on. Worth investing in writing them well.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>TypeScript Won. Here's What That Actually Bought Us.</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Sat, 27 Jun 2026 07:15:23 +0000</pubDate>
      <link>https://dev.to/parsajiravand/typescript-won-heres-what-that-actually-bought-us-12m8</link>
      <guid>https://dev.to/parsajiravand/typescript-won-heres-what-that-actually-bought-us-12m8</guid>
      <description>&lt;p&gt;Nobody seriously argues about adopting TypeScript anymore. New frontend projects default to it; the holdouts are legacy codebases and the occasional throwaway script. The debate is over, TypeScript won.&lt;/p&gt;

&lt;p&gt;But "won" is the boring part. The interesting part is &lt;em&gt;what types turned out to be good for&lt;/em&gt; — which is more, and different, than the original pitch of "catch typos before runtime."&lt;/p&gt;

&lt;h2&gt;
  
  
  Types are the cheapest documentation you'll never have to update
&lt;/h2&gt;

&lt;p&gt;A function signature is documentation that can't go stale, because the compiler fails the build the moment it lies.&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;function&lt;/span&gt; &lt;span class="nf"&gt;scheduleReminder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;userId&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="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sms&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ReminderId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You already know almost everything about calling this without reading a single comment: what it needs, what it returns, that &lt;code&gt;channel&lt;/code&gt; is one of exactly three strings. A comment claiming the same things could rot the next time someone adds a &lt;code&gt;"slack"&lt;/code&gt; channel and forgets to update it. The type &lt;em&gt;can't&lt;/em&gt; — adding &lt;code&gt;"slack"&lt;/code&gt; to the union forces every call site to be reconsidered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types make refactoring a mechanical activity instead of an act of courage
&lt;/h2&gt;

&lt;p&gt;In a large untyped codebase, renaming a widely-used field is genuinely scary — you're grepping strings and praying. In a typed one, you change the type and the compiler hands you the complete to-do list of everything that broke. Refactoring stops being "risky" and becomes "tedious but safe," which is exactly the trade you want. Whole categories of "we can't touch that, it's too entangled" simply dissolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff nobody planned: types are the contract AI codegen needs
&lt;/h2&gt;

&lt;p&gt;Here's the part that wasn't in the original sales pitch. The more machine-readable your boundaries are, the more reliably an AI assistant can operate inside them.&lt;/p&gt;

&lt;p&gt;Ask a model to "add a field to this object" in untyped JavaScript and it's guessing at shape from usage. Ask it in TypeScript and the type &lt;em&gt;is&lt;/em&gt; the spec — it knows precisely what's allowed, and its mistakes surface as compile errors instead of 2am production incidents. Types turn "generate plausible code" into "generate code that provably fits."&lt;/p&gt;

&lt;p&gt;This flips an old objection on its head. People used to say types slowed them down. In an AI-assisted workflow, types &lt;em&gt;speed you up&lt;/em&gt;, because they're the guardrail that lets you accept generated code with confidence instead of auditing every line by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few habits that compound
&lt;/h2&gt;

&lt;p&gt;If types are this leverage-rich, it's worth writing them with intent rather than appeasing the compiler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer unions over booleans + optionals.&lt;/strong&gt; &lt;code&gt;status: "loading" | "error" | "ready"&lt;/code&gt; beats three independent boolean flags that can contradict each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name your domain types.&lt;/strong&gt; &lt;code&gt;type Cents = number&lt;/code&gt; documents intent at every use site and lets you tighten it later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid &lt;code&gt;any&lt;/code&gt;; reach for &lt;code&gt;unknown&lt;/code&gt; and narrow.&lt;/strong&gt; &lt;code&gt;any&lt;/code&gt; is a hole in exactly the safety net you're paying for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let inference work.&lt;/strong&gt; You don't need to annotate everything — annotate the &lt;em&gt;boundaries&lt;/em&gt; (function signatures, exported APIs) and let the rest flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;TypeScript's real win wasn't catching typos. It was turning your codebase into something with &lt;em&gt;explicit, enforced contracts&lt;/em&gt; — and contracts turn out to be exactly what fearless refactoring, reliable tooling, and trustworthy AI assistance all quietly depend on.&lt;/p&gt;

&lt;p&gt;We adopted types to prevent a class of bugs. We're keeping them because they're the substrate everything else now builds on. Worth investing in writing them well.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your Design System Is Now an API for Machines</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:29:18 +0000</pubDate>
      <link>https://dev.to/parsajiravand/your-design-system-is-now-an-api-for-machines-49mo</link>
      <guid>https://dev.to/parsajiravand/your-design-system-is-now-an-api-for-machines-49mo</guid>
      <description>&lt;p&gt;We've always justified design systems with human reasons: consistency, faster onboarding, fewer one-off buttons. All true. But there's a new reason that quietly outweighs the rest — &lt;strong&gt;your design system is becoming the API that machines build UI against.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When an AI assistant generates a screen, the quality of its output is bounded almost entirely by the vocabulary you give it. A loose, undocumented pile of components produces loose, inconsistent output. A tight, well-typed, well-named system produces output that looks like your team wrote it. The design system stops being documentation and becomes a &lt;em&gt;constraint surface&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "good rails" actually look like
&lt;/h2&gt;

&lt;p&gt;If a model (or a junior dev, honestly — same requirements) is going to compose your components correctly without hand-holding, the system has to be legible &lt;em&gt;from the types and names alone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Names that describe intent, not appearance.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Weak: the model has to guess when to use which&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BlueButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SmallButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RoundButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Strong: intent is in the name, variants are props&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sm"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Props that make illegal states unrepresentable.&lt;/strong&gt; If a component can be configured into a broken combination, something will eventually configure it that way. Push the constraints into the type system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A toast is either transient (auto-dismiss) OR action-required — never both.&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToastProps&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;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;onAction&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="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;actionLabel&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;A union like this is worth more than a paragraph of docs, because it's &lt;em&gt;enforced&lt;/em&gt;. Neither a human nor a model can pass an &lt;code&gt;onAction&lt;/code&gt; to a transient toast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One obvious way to do each thing.&lt;/strong&gt; Three different ways to render a modal is three ways to get it subtly wrong. Every redundant path is a fork where generated code can drift from your conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shift in where you spend effort
&lt;/h2&gt;

&lt;p&gt;Old priority order for a design system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visual polish&lt;/li&gt;
&lt;li&gt;Documentation / Storybook&lt;/li&gt;
&lt;li&gt;Type safety (nice to have)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;New priority order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Type safety and clear contracts&lt;/strong&gt; — the machine-legible layer&lt;/li&gt;
&lt;li&gt;Clear, intent-based naming&lt;/li&gt;
&lt;li&gt;Visual polish&lt;/li&gt;
&lt;li&gt;Docs (increasingly, the types &lt;em&gt;are&lt;/em&gt; the docs)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn't because polish stopped mattering — it's because polish is now the cheap part. What's expensive and high-leverage is the part that lets &lt;em&gt;everything built on top&lt;/em&gt; be correct by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete test
&lt;/h2&gt;

&lt;p&gt;Want to know if your design system is ready for this world? Try this: hand an AI assistant &lt;em&gt;only&lt;/em&gt; your component type definitions (no screenshots, no prose) and ask it to build a settings page. Then look at the output.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If it composed sensible, on-brand UI from the types alone — your contracts are doing their job.&lt;/li&gt;
&lt;li&gt;If it invented props that don't exist, or reached for raw &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s and inline styles — that's a map of exactly where your system is ambiguous or incomplete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gap &lt;em&gt;is&lt;/em&gt; your design-system backlog now. The components it had to fake are the ones missing a clear contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Build your design system as if its primary consumer can't see — because increasingly, one of its primary consumers can't. It reads types, names, and constraints, not Figma frames. The systems that thrive in the next few years won't be the prettiest; they'll be the ones whose rules are so clearly encoded that neither a tired engineer at 5pm nor a model at scale can hold them wrong.&lt;/p&gt;

&lt;p&gt;A design system used to be a favor you did for your teammates. Now it's the interface your whole org — humans and machines — programs against. Build it like an API, because that's what it is.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>designsystems</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Frontend Is Becoming a Conversation: Where UI Engineering Goes Next</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Thu, 25 Jun 2026 12:39:39 +0000</pubDate>
      <link>https://dev.to/parsajiravand/the-frontend-is-becoming-a-conversation-where-ui-engineering-goes-next-98l</link>
      <guid>https://dev.to/parsajiravand/the-frontend-is-becoming-a-conversation-where-ui-engineering-goes-next-98l</guid>
      <description>&lt;p&gt;For a decade, "what's your frontend stack?" was a loaded question. jQuery vs. Backbone. Angular vs. React. Webpack vs. everything. The churn was exhausting, and a non-trivial chunk of our job was just keeping up.&lt;/p&gt;

&lt;p&gt;That era is quietly ending — not because we won the framework wars, but because the questions moved up a layer. The interesting problems in frontend today aren't about which library renders a list. They're about how rendering, data, and increasingly &lt;em&gt;generation&lt;/em&gt; fit together. And AI is sitting right in the middle of that shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack consolidated more than we admit
&lt;/h2&gt;

&lt;p&gt;Look at what most new production apps actually reach for in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React or Svelte/Vue&lt;/strong&gt; for the component model, with the framework wars settling into "pick one, they're all fine."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A meta-framework&lt;/strong&gt; — Next, Remix/React Router, SvelteKit, Nuxt — because nobody hand-rolls routing, data loading, and SSR anymore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript by default.&lt;/strong&gt; Not a debate. The plain-JS greenfield project is now the exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-first rendering&lt;/strong&gt; (RSC, islands, streaming) as the baseline, with the client bundle treated as a cost to minimize rather than the center of the universe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The center of gravity moved back toward the server — but a &lt;em&gt;smarter&lt;/em&gt; server that streams HTML, hydrates selectively, and treats the network boundary as a first-class design concern. The pendulum didn't swing back to 2010; it spiraled forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI actually changed (and what it didn't)
&lt;/h2&gt;

&lt;p&gt;The hype says "AI writes the frontend now." The reality on the ground is more specific and more interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It collapsed the cost of the first 80%.&lt;/strong&gt; Scaffolding a component, wiring a form, translating a Figma frame into JSX, writing the Tailwind for a layout — these used to be hours of work and are now minutes. That's real, and it's already changed how teams estimate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It did &lt;em&gt;not&lt;/em&gt; collapse the last 20%.&lt;/strong&gt; Accessibility edge cases, focus management, race conditions in async state, the weird Safari bug, the design-system invariant that isn't written down anywhere — this is still where senior engineers earn their keep. AI gets you a plausible draft; it doesn't get you a &lt;em&gt;correct&lt;/em&gt; one. The skill that's appreciating in value is &lt;strong&gt;judgment&lt;/strong&gt;: knowing what "done" means and being able to tell when the generated code is subtly wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI will happily generate this.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&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="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// It will not, on its own, ask:&lt;/span&gt;
&lt;span class="c1"&gt;//  - What about currencies that aren't cents-based (JPY)?&lt;/span&gt;
&lt;span class="c1"&gt;//  - What locale formats this for the user?&lt;/span&gt;
&lt;span class="c1"&gt;//  - What happens when `cents` is a float from a bad API?&lt;/span&gt;
&lt;span class="c1"&gt;// That question-asking is the job now.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The shift that matters: UI as something you &lt;em&gt;generate&lt;/em&gt;, not just &lt;em&gt;write&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the genuinely new idea. For years, "server-driven UI" meant the backend sending a layout description that the client renders. AI pushes that further — toward interfaces that are &lt;strong&gt;assembled on demand&lt;/strong&gt; from intent rather than authored ahead of time.&lt;/p&gt;

&lt;p&gt;We're not fully there, and a lot of "generative UI" demos are toys. But the direction is clear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Static UI&lt;/strong&gt; — you write every screen by hand. (Where we've been.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-driven UI&lt;/strong&gt; — the backend describes screens; the client renders from a schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generative UI&lt;/strong&gt; — a model produces the component tree for a given user and context, constrained by your design system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The teams getting value from #3 today aren't letting a model freestyle pixels. They're giving it a &lt;strong&gt;tightly constrained vocabulary&lt;/strong&gt; — a fixed set of audited components and tokens — and letting it compose &lt;em&gt;within&lt;/em&gt; those rails. The design system stops being documentation and becomes the &lt;em&gt;guardrail an AI plans against.&lt;/em&gt; That reframes a lot of frontend architecture work: your component API is now also a prompt surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for the next couple of years
&lt;/h2&gt;

&lt;p&gt;A few predictions I'd actually bet on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The component library becomes the most valuable asset on the team&lt;/strong&gt; — because it's what both humans and models build against. Investment in a clean, well-typed, accessible design system pays off twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Types and contracts win.&lt;/strong&gt; The more machine-readable your boundaries (TS types, schemas, OpenAPI), the more reliably AI can operate inside them. Ambiguity is the enemy of generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The job title blurs.&lt;/strong&gt; "Frontend engineer" increasingly means &lt;em&gt;product engineer who owns the experience end-to-end&lt;/em&gt; — data fetching, the rendering strategy, the AI-assisted authoring loop, and the taste to know when it's wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewing replaces typing.&lt;/strong&gt; The bottleneck shifts from producing code to &lt;em&gt;evaluating&lt;/em&gt; it. If you can't read code critically and fast, you'll be slower in an AI world, not faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The frontend isn't being automated away — it's being &lt;em&gt;re-leveraged&lt;/em&gt;. The mechanical parts are getting cheap, and the parts that were always the actual hard work — architecture, correctness, accessibility, taste — are getting more valuable, not less.&lt;/p&gt;

&lt;p&gt;The engineers who thrive won't be the ones who type the fastest or memorize the most APIs. They'll be the ones who can hold a clear picture of what &lt;em&gt;good&lt;/em&gt; looks like, express it in constrained, machine-legible building blocks, and tell — instantly — when the draft in front of them is wrong.&lt;/p&gt;

&lt;p&gt;The frontend is becoming a conversation. Worth getting good at the half of it that's still yours.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
