<?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: Saad Ahmad</title>
    <description>The latest articles on DEV Community by Saad Ahmad (@saadahmad).</description>
    <link>https://dev.to/saadahmad</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%2F2812503%2Fec30f77c-bb77-41e6-9c05-4666fb7bbcff.jpg</url>
      <title>DEV Community: Saad Ahmad</title>
      <link>https://dev.to/saadahmad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saadahmad"/>
    <language>en</language>
    <item>
      <title>I built a chat app that forgets 🔥</title>
      <dc:creator>Saad Ahmad</dc:creator>
      <pubDate>Mon, 14 Sep 2026 15:15:29 +0000</pubDate>
      <link>https://dev.to/saadahmad/i-built-a-chat-app-that-forgets-4fal</link>
      <guid>https://dev.to/saadahmad/i-built-a-chat-app-that-forgets-4fal</guid>
      <description>&lt;p&gt;There's a kind of conversation that doesn't belong in WhatsApp. 💬&lt;/p&gt;

&lt;p&gt;You need to send someone a document. Or say something that shouldn't sit in a thread forever. Or talk to a person you'd rather not add to your contacts. Every option feels wrong — message them somewhere that keeps a copy of everything, email them and leave a permanent paper trail, or dig out a file-sharing site that looks like it hasn't been touched since 2009.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;CipherDrop&lt;/strong&gt;. Open a room, share the ID, talk, delete. 🗑️&lt;/p&gt;

&lt;p&gt;Nobody signs up. Nobody hands over a phone number or an email. And when the room goes, it takes everything with it.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://cipherdrop.saadbhatti888.workers.dev" rel="noopener noreferrer"&gt;https://cipherdrop.saadbhatti888.workers.dev&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Who is this actually for?
&lt;/h2&gt;

&lt;p&gt;Not everyone needs this. But if any of these sound familiar, it was built for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📄 &lt;strong&gt;Sending a document to someone you barely know.&lt;/strong&gt; A contract, an invoice, a CV. You need it to arrive — you don't need it living in a chat thread for the next five years.&lt;/li&gt;
&lt;li&gt;🧑‍💻 &lt;strong&gt;Freelancers and clients.&lt;/strong&gt; A quick channel for one project, closed the day it ships.&lt;/li&gt;
&lt;li&gt;🛒 &lt;strong&gt;Buying or selling something online.&lt;/strong&gt; Talk to a stranger without giving them a way to reach you afterwards.&lt;/li&gt;
&lt;li&gt;🏥 &lt;strong&gt;Anything sensitive but ordinary.&lt;/strong&gt; A medical result, a bank detail, a password. Things you'd rather not leave sitting in someone's message history.&lt;/li&gt;
&lt;li&gt;🎓 &lt;strong&gt;Group work with people you'll never speak to again.&lt;/strong&gt; One room, everyone in, delete when done.&lt;/li&gt;
&lt;li&gt;🚪 &lt;strong&gt;Just wanting a door you can close.&lt;/strong&gt; Sometimes that's the whole reason.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚡ How it works
&lt;/h2&gt;

&lt;p&gt;Create a room and you get an ID. Send it to one person or a whole group — text it, WhatsApp it, read it down the phone. However you like.&lt;/p&gt;

&lt;p&gt;Rooms come in two shapes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👥 Just the two of us&lt;/strong&gt; — you approve one person, and the room is then closed. A third person who somehow gets hold of the ID is simply told the room is full. No waiting list, no peeking in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🌐 Group&lt;/strong&gt; — anyone holding the ID walks straight in.&lt;/p&gt;

&lt;p&gt;When someone knocks on a private room, a card appears over your chat with their name and two buttons: let them in, or don't. They're inside a second later, no refreshing on either end. ✨&lt;/p&gt;




&lt;h2&gt;
  
  
  🎁 What's inside
&lt;/h2&gt;

&lt;p&gt;💬 &lt;strong&gt;Everything arrives instantly&lt;/strong&gt; — text, photos, video, documents and voice notes, with no refreshing and no waiting.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Read receipts you can trust&lt;/strong&gt; — one tick when it's sent, two when it's delivered, two blue when it's actually been read.&lt;/p&gt;

&lt;p&gt;😂 &lt;strong&gt;Reactions&lt;/strong&gt; — tap a message, pick an emoji, tap again to take it back.&lt;/p&gt;

&lt;p&gt;🎙️ &lt;strong&gt;Voice notes done properly&lt;/strong&gt; — tap the mic and you get a real recorder with a timer and bars that move with your voice. Bin it or send it. Playback is a waveform you can scrub through, not a grey browser box.&lt;/p&gt;

&lt;p&gt;📎 &lt;strong&gt;Attachments that don't bury the conversation&lt;/strong&gt; — a photo arrives as a neat chip rather than a wall of image. Tap to open it full screen, with download and close right there. Videos open in a player. Documents show you the name, type and size, with one button to save them.&lt;/p&gt;

&lt;p&gt;📤 &lt;strong&gt;Uploads you can watch happen&lt;/strong&gt; — the message appears the moment you pick a file, with a live percentage, then settles into the real thing.&lt;/p&gt;

&lt;p&gt;🔒 &lt;strong&gt;A lock, when you want one&lt;/strong&gt; — flip it on, choose a key, and your message is scrambled in your own browser before it's sent anywhere. The other person sees "Locked" until they tap it and enter the same key. Share that key any way you like — out loud is fine.&lt;/p&gt;

&lt;p&gt;🧹 &lt;strong&gt;Delete, and mean it&lt;/strong&gt; — one press removes the room, every message in it, and every file anyone uploaded. Anyone still inside is sent home immediately.&lt;/p&gt;

&lt;p&gt;📱 &lt;strong&gt;It installs like an app&lt;/strong&gt; — open it on your phone, tap install, and it lives on your home screen with its own icon and no address bar in sight.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ How safe is it, really?
&lt;/h2&gt;

&lt;p&gt;Fair question, and I'd rather answer it plainly than hide behind buzzwords.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rooms are genuinely closed.&lt;/strong&gt; 🚪 Knowing a room's address isn't enough to get into it. If you weren't let in, you see a locked door — not the conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody can pretend to be someone else.&lt;/strong&gt; 🎭 Your name in a room is fixed the moment you're approved, and two people can't use the same name in one room.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing is broadcast to the open internet.&lt;/strong&gt; 📡 Live messages travel on a private channel that has to be unlocked for each person individually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can scramble a message before it leaves your device.&lt;/strong&gt; 🔐 Use the lock and only someone with your key can read it — including in the unlikely event anyone ever got to the storage behind the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleted means deleted.&lt;/strong&gt; 🗑️ Not hidden, not archived. The messages and the files both go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No account means nothing to leak.&lt;/strong&gt; 👤 There's no email, no password, no phone number, because there's nothing to sign up for.&lt;/p&gt;

&lt;h3&gt;
  
  
  And now the honest part 🙂
&lt;/h3&gt;

&lt;p&gt;I'd rather undersell this than have someone trust it too far.&lt;/p&gt;

&lt;p&gt;CipherDrop is &lt;strong&gt;not&lt;/strong&gt; Signal. It's built with care, but it's a young app made by one person, and it hasn't been reviewed by a security team. Messages you don't lock are stored normally on the way through. And anyone who gets your room ID can ask to join — so treat that ID like a password and only send it to the person you mean.&lt;/p&gt;

&lt;p&gt;For a document you'd rather not leave lying around, a conversation you want to end cleanly, or a chat with someone you'd prefer not to keep in your contacts — it does that job well. ✅&lt;/p&gt;

&lt;p&gt;If your physical safety depends on a conversation staying private, use a tool that's been audited by people whose full-time job that is. 🙏&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 What's next
&lt;/h2&gt;

&lt;p&gt;Rooms currently sit there until someone deletes them. Automatic clean-up is next — rooms that quietly disappear after a day, or after a couple of hours of silence, files included. Then a Play Store version. 📲&lt;/p&gt;




&lt;p&gt;If you try it and something feels off, I'd genuinely like to hear about it. 💭&lt;/p&gt;

&lt;p&gt;Built by &lt;strong&gt;Saad Ahmad&lt;/strong&gt; — &lt;a href="https://isaadahmad.com" rel="noopener noreferrer"&gt;isaadahmad.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Try CipherDrop:&lt;/strong&gt; &lt;a href="https://cipherdrop.saadbhatti888.workers.dev" rel="noopener noreferrer"&gt;https://cipherdrop.saadbhatti888.workers.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>privacy</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Scroll animations in React that never touch the main thread</title>
      <dc:creator>Saad Ahmad</dc:creator>
      <pubDate>Fri, 04 Sep 2026 08:12:31 +0000</pubDate>
      <link>https://dev.to/saadahmad/scroll-animations-in-react-that-never-touch-the-main-thread-473b</link>
      <guid>https://dev.to/saadahmad/scroll-animations-in-react-that-never-touch-the-main-thread-473b</guid>
      <description>&lt;p&gt;Every scroll animation library I have reached for over the last few years does the same thing: it listens to the scroll event, reads the element's position, and writes a new transform. Sixty times a second, on the main thread, next to your hydration and your data fetching and your own click handlers.&lt;/p&gt;

&lt;p&gt;The browser has had a better way to do this since Chrome 115 shipped in July 2023. &lt;code&gt;ScrollTimeline&lt;/code&gt; and &lt;code&gt;ViewTimeline&lt;/code&gt; let you hand your keyframes to the browser's own animation engine and say &lt;em&gt;"this animation's playhead is the scroll position"&lt;/em&gt;. For &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;opacity&lt;/code&gt; and &lt;code&gt;filter&lt;/code&gt;, the compositor takes it from there. No JavaScript runs per frame. None.&lt;/p&gt;

&lt;p&gt;Almost nobody uses it. Not because it is obscure — because it is awkward:&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;@keyframes&lt;/span&gt; &lt;span class="n"&gt;reveal&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;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt;&lt;span class="p"&gt;;&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;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&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="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reveal&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="n"&gt;cover&lt;/span&gt; &lt;span class="m"&gt;40%&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 is fine in a stylesheet. Inside a component, where the distance is a prop and the range depends on the layout, it means generating CSS strings or writing a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag by hand. And you still need to answer the question &lt;em&gt;"what happens in Firefox?"&lt;/em&gt; — which, as of today, still has scroll-driven animations behind a flag in stable.&lt;/p&gt;

&lt;p&gt;Those two problems are exactly what a hook should hide. So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  use-scroll-timeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i use-scroll-timeline
&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useScrollReveal&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;use-scroll-timeline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Card&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useScrollReveal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fade-up&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;I reveal myself as I scroll into view&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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;3.7 kB min+gzip. Zero dependencies. Six hooks. No provider, no context, no CSS import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://use-scroll-timeline-demo.vercel.app/" rel="noopener noreferrer"&gt;Live demo — every hook, with live read-outs →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Anything more specific goes through the core hook, which takes plain Web Animations keyframes — the same object you would pass to &lt;code&gt;element.animate()&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useScrollTimeline&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;keyframes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;scale&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;0.8&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;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;filter&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;blur(8px)&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;blur(0px)&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;range&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;entry 0%&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;cover 40%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part I like most: progress as a CSS variable
&lt;/h2&gt;

&lt;p&gt;Most scroll libraries hand you a number and let you put it in state. That number arrives every frame, so your component re-renders every frame, and now you have a JavaScript scroll animation again with extra steps.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useScrollProgress&lt;/code&gt; writes progress into a CSS custom property instead and re-renders nothing:&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useScrollProgress&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;"&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;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"reading-bar"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.reading-bar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transform-origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scale&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;--progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where the native API exists, the hook registers that property with &lt;code&gt;CSS.registerProperty&lt;/code&gt; and lets the browser animate it. So the reading progress bar at the top of a page — the thing every blog implements with a scroll listener — costs zero main-thread work.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useScrollProgressValue&lt;/code&gt; still exists for when you genuinely need the number in React. It is throttled by a &lt;code&gt;precision&lt;/code&gt; option, defaulting to about 100 renders across the whole range instead of one per frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fallback is the interesting bit
&lt;/h2&gt;

&lt;p&gt;This is the part I expected to be ugly and turned out clean.&lt;/p&gt;

&lt;p&gt;The naive fallback is to reimplement keyframe interpolation in JavaScript — parse the values, lerp them, write styles. That is a lot of code and a lot of ways to be subtly wrong about &lt;code&gt;translate&lt;/code&gt; versus &lt;code&gt;transform&lt;/code&gt; versus &lt;code&gt;filter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead: build the exact same WAAPI animation, pause it, and scrub it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyframes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;both&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// later, from the scroll driver:&lt;/span&gt;
&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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 is already an excellent keyframe interpolator. The fallback borrows it and only replaces the clock. Same keyframes, same easing, same fill behaviour, no interpolation code of my own — and the difference between the native path and the fallback becomes &lt;em&gt;where the playhead comes from&lt;/em&gt;, nothing else.&lt;/p&gt;

&lt;p&gt;The driver itself is one &lt;code&gt;IntersectionObserver&lt;/code&gt; per element to gate the work, and &lt;strong&gt;one&lt;/strong&gt; shared &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop plus &lt;strong&gt;one&lt;/strong&gt; capture-phase &lt;code&gt;scroll&lt;/code&gt; listener for the entire page. Ten animated elements cost one frame callback, not ten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Named ranges, which are the actual API
&lt;/h2&gt;

&lt;p&gt;The variants are the marketing; the ranges are the feature. This is the vocabulary from the CSS spec, and the hooks take it verbatim as strings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;Starts when&lt;/th&gt;
&lt;th&gt;Ends when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cover&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;subject's leading edge touches the scrollport&lt;/td&gt;
&lt;td&gt;subject has completely left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;entry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;subject starts entering&lt;/td&gt;
&lt;td&gt;subject is fully in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;subject starts leaving&lt;/td&gt;
&lt;td&gt;subject has completely left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;contain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;subject is fully inside&lt;/td&gt;
&lt;td&gt;subject starts leaving&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;range&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;entry 25%&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;contain 50%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the thing that cost me an afternoon and is worth knowing whether or not you use this package: &lt;strong&gt;the &lt;code&gt;entry&lt;/code&gt; range is exactly as long as the element's own height.&lt;/strong&gt; A 150px card finishes its entire reveal in 150px of scrolling. It technically works, it is spec-correct, and it looks like nothing happened.&lt;/p&gt;

&lt;p&gt;End the range later and the same animation becomes something you can actually see:&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="nf"&gt;useScrollReveal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;range&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;entry 0%&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;cover 40%&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;// ~400px of travel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I only understood this properly after building a panel that watches one element through &lt;code&gt;entry&lt;/code&gt;, &lt;code&gt;contain&lt;/code&gt; and &lt;code&gt;exit&lt;/code&gt; simultaneously and prints all three numbers. Watching them hand over to each other is the fastest way to internalise the model — it is the first section of the demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reduced motion, and why your page might look broken
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prefers-reduced-motion: reduce&lt;/code&gt; is respected by default: the hook skips the animation and lands the element on its final keyframe.&lt;/p&gt;

&lt;p&gt;This is correct behaviour that looks exactly like a bug. If you have "Animation effects" turned off in Windows or "Reduce motion" on in macOS, every reveal on the page appears already revealed, parallax layers sit still, and changing a prop seems to do nothing. I lost a good twenty minutes to this on my own library. Both demos now detect it and say so in a banner, and you can opt out per hook with &lt;code&gt;respectReducedMotion: false&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it runs natively
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome / Edge 115+&lt;/td&gt;
&lt;td&gt;Native &lt;code&gt;ViewTimeline&lt;/code&gt; / &lt;code&gt;ScrollTimeline&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safari 26+&lt;/td&gt;
&lt;td&gt;Native, threaded since 26.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox&lt;/td&gt;
&lt;td&gt;Fallback — still behind a flag in stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anything older&lt;/td&gt;
&lt;td&gt;Fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly 80% of users get the compositor path today, and the number only goes up. The remainder get an animation that is visually identical and still cheaper than a typical scroll-listener implementation, because the whole page shares one loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things shipping it taught me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;tsup silently ate my &lt;code&gt;"use client"&lt;/code&gt; directive.&lt;/strong&gt; The banner was configured correctly, but tsup's rollup treeshaking pass strips module-level directives from the bundle — so the built file had no directive and the package would have broken in the App Router. Fixed with a tiny post-build script that re-attaches it on the same line as the first statement, which keeps source-map line numbers accurate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;publint&lt;/code&gt; found a real bug in my exports map.&lt;/strong&gt; I had a single &lt;code&gt;types&lt;/code&gt; field, which means TypeScript resolves the ESM &lt;code&gt;.d.ts&lt;/code&gt; even when the consumer uses &lt;code&gt;require()&lt;/code&gt;. CJS users would have had types that only work under dynamic &lt;code&gt;import()&lt;/code&gt;. Split into &lt;code&gt;import&lt;/code&gt; and &lt;code&gt;require&lt;/code&gt; conditions pointing at &lt;code&gt;.d.ts&lt;/code&gt; and &lt;code&gt;.d.cts&lt;/code&gt;. Run &lt;code&gt;publint --strict&lt;/code&gt; and &lt;code&gt;attw --pack .&lt;/code&gt; before your first publish — always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;"provenance": true&lt;/code&gt; in &lt;code&gt;publishConfig&lt;/code&gt; breaks a manual publish.&lt;/strong&gt; Provenance can only be generated on a supported CI runner, so that flag makes &lt;code&gt;npm publish&lt;/code&gt; from your laptop fail outright. Leave it out of the manifest and pass &lt;code&gt;--provenance&lt;/code&gt; in the release workflow instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo&lt;/strong&gt; — &lt;a href="https://use-scroll-timeline-demo.vercel.app/" rel="noopener noreferrer"&gt;https://use-scroll-timeline-demo.vercel.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt; — &lt;a href="https://www.npmjs.com/package/use-scroll-timeline" rel="noopener noreferrer"&gt;use-scroll-timeline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt; — &lt;a href="https://github.com/saadahmad888/use-scroll-timeline" rel="noopener noreferrer"&gt;github.com/saadahmad888/use-scroll-timeline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Related&lt;/strong&gt; — &lt;a href="https://www.npmjs.com/package/stack-on-scroll" rel="noopener noreferrer"&gt;stack-on-scroll&lt;/a&gt;, the sticky stacking-card version of the same idea&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is &lt;code&gt;0.1.0&lt;/code&gt;, so the API can still move. If you use it and something is awkward, an issue is genuinely welcome — that is the most useful thing anyone can send me right now.&lt;/p&gt;




&lt;p&gt;Built by &lt;strong&gt;Saad Ahmad&lt;/strong&gt;. I build small, focused front-end libraries and the sites that show them off — TypeScript, React, Next.js, and a slightly unreasonable interest in what the platform can already do without a dependency.&lt;/p&gt;

&lt;p&gt;More of my work at &lt;strong&gt;&lt;a href="https://www.isaadahmad.com" rel="noopener noreferrer"&gt;isaadahmad.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your team is fighting a janky scroll page, carrying 70 kB of animation library for four effects, or needs a component library packaged properly — that is the kind of work I take on, and my inbox is open.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>I shipped a dark mode hook. Then I found four bugs my own demo never hit.</title>
      <dc:creator>Saad Ahmad</dc:creator>
      <pubDate>Mon, 31 Aug 2026 15:36:05 +0000</pubDate>
      <link>https://dev.to/saadahmad/i-shipped-a-dark-mode-hook-then-i-found-four-bugs-my-own-demo-never-hit-j69</link>
      <guid>https://dev.to/saadahmad/i-shipped-a-dark-mode-hook-then-i-found-four-bugs-my-own-demo-never-hit-j69</guid>
      <description>&lt;p&gt;Four months ago I published &lt;a href="https://www.npmjs.com/package/use-theme-mode" rel="noopener noreferrer"&gt;&lt;code&gt;use-theme-mode&lt;/code&gt;&lt;/a&gt;, a small React hook for light/dark theming. It did what I wanted: managed the state, persisted the choice, read the system preference, and stayed out of my CSS.&lt;/p&gt;

&lt;p&gt;It worked perfectly in my demo.&lt;/p&gt;

&lt;p&gt;Then people started using it in apps that weren't mine, and I found four bugs that my demo had never once triggered. Not edge cases — the kind of thing that breaks a real page on a real Tuesday.&lt;/p&gt;

&lt;p&gt;This is what was wrong, why my testing missed it, and what &lt;code&gt;2.0&lt;/code&gt; does about it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 1: every call site had its own state
&lt;/h2&gt;

&lt;p&gt;Here's the v1 hook, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useTheme&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;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTheme&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="nx"&gt;getInitialTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;THEME_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&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;theme&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="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDark&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;Spot it? &lt;code&gt;useState&lt;/code&gt; lives inside the hook. Call &lt;code&gt;useTheme()&lt;/code&gt; in two components and you get &lt;strong&gt;two independent pieces of state&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Header&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;toggleTheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// state copy A&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="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Toggle&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;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Sidebar&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;theme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;         &lt;span class="c1"&gt;// state copy B&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="nc"&gt;Icon&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&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;moon&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;sun&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;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click the button. The attribute on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; flips, so the page turns dark. But &lt;code&gt;Sidebar&lt;/code&gt; never re-renders, because &lt;em&gt;its&lt;/em&gt; copy of the state never changed. The icon keeps saying "light".&lt;/p&gt;

&lt;p&gt;My demo had one component. Of course it worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt; is to move the state out of React entirely. v2 keeps a single module-level store and every hook instance subscribes to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&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;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ThemeState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;applyToDom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolvedTheme&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;listener&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;listener&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 hook becomes a thin subscription. Twenty components, one source of truth.&lt;/p&gt;

&lt;p&gt;I hand-rolled the subscription rather than reaching for &lt;code&gt;useSyncExternalStore&lt;/code&gt;, because that's React 18+ and I wanted to keep the 16.8 peer range. It's about fifteen lines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 2: system mode that stopped listening
&lt;/h2&gt;

&lt;p&gt;v1 read &lt;code&gt;prefers-color-scheme&lt;/code&gt; exactly once, inside the &lt;code&gt;useState&lt;/code&gt; initialiser:&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;prefersDark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&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;prefersDark&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&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;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a snapshot, not a subscription. Change your OS appearance while the tab is open — which macOS does &lt;em&gt;automatically at sunset&lt;/em&gt; — and the page ignores you until you reload.&lt;/p&gt;

&lt;p&gt;Worse, v1 immediately wrote that snapshot to &lt;code&gt;localStorage&lt;/code&gt;. So the very first visit permanently pinned you to whatever your OS happened to prefer at that moment. There was no way to say "just follow the system" and mean it.&lt;/p&gt;

&lt;p&gt;v2 treats &lt;code&gt;"system"&lt;/code&gt; as a real, storable value that resolves at read time:&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;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&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;systemTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ColorScheme&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;theme&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;theme&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;systemTheme&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;and attaches a listener so the resolution stays live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mediaQueryList&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="nx"&gt;event&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;systemTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&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;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;systemTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resolvedTheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;activeTheme&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;systemTheme&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;This is why the hook now returns three values instead of one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;what the user chose — may be &lt;code&gt;"system"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resolvedTheme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;what's actually on screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemTheme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;what the OS prefers, regardless of the choice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you were branching on &lt;code&gt;theme === "dark"&lt;/code&gt;, switch to &lt;code&gt;resolvedTheme&lt;/code&gt; or the new &lt;code&gt;isDark&lt;/code&gt; flag.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 3: localStorage can throw
&lt;/h2&gt;

&lt;p&gt;This line looks harmless:&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;storedTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;THEME_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It isn't. Accessing &lt;code&gt;localStorage&lt;/code&gt; &lt;strong&gt;throws a SecurityError&lt;/strong&gt; in several ordinary situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safari in private browsing, historically&lt;/li&gt;
&lt;li&gt;inside a sandboxed &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; without &lt;code&gt;allow-same-origin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;when a user has blocked all cookies and site data&lt;/li&gt;
&lt;li&gt;some enterprise-managed browser policies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because v1 called it during the &lt;code&gt;useState&lt;/code&gt; initialiser, the throw happened &lt;em&gt;during render&lt;/em&gt;. Not a broken theme — a blank page.&lt;/p&gt;

&lt;p&gt;v2 wraps every storage access:&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;readStored&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getStore&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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 storage is unavailable the theme still works for the session; it just doesn't survive a reload. Degraded, not dead.&lt;/p&gt;

&lt;p&gt;There's a related fix: v1 applied whatever string it found in storage straight to the DOM. v2 validates it against the configured theme list first, so a stale or tampered value falls back to the default instead of putting &lt;code&gt;data-theme="&amp;lt;script&amp;gt;"&lt;/code&gt; on your &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 4: the flash
&lt;/h2&gt;

&lt;p&gt;The one everybody notices and nobody reports as a bug, because it feels like the cost of doing business.&lt;/p&gt;

&lt;p&gt;In v1 the DOM update lived in an effect:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&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;theme&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Effects run &lt;strong&gt;after&lt;/strong&gt; paint. So the sequence is: browser paints your default (light), React hydrates, effect fires, page snaps to dark. On a fast connection it's a blink. On a slow one it's a second of white.&lt;/p&gt;

&lt;p&gt;You cannot fix this from inside React. The correct theme has to be on the element before the browser's first frame, which means a synchronous script in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;v2 ships one. &lt;code&gt;&amp;lt;ThemeScript /&amp;gt;&lt;/code&gt; renders a ~0.8 kB blocking IIFE that reads storage, resolves system preference, and writes the attribute — before anything paints.&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;// app/layout.tsx&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;ThemeScript&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;use-theme-mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;suppressHydrationWarning&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;head&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;ThemeScript&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;head&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;body&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;children&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;body&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;html&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;For Vite there's &lt;code&gt;themeScript()&lt;/code&gt;, which returns the raw string so you can inline it at build time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&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;themeScript&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;use-theme-mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;themeOptions&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;./src/theme-options.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;noFlashTheme&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-flash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;transformIndexHtml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&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;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;themeScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;themeOptions&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/script&amp;gt;&amp;lt;/head&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo site uses exactly this. Both the plugin and &lt;code&gt;&amp;lt;ThemeProvider&amp;gt;&lt;/code&gt; read the same options object, so the pre-paint pass and the React runtime can't drift apart.&lt;/p&gt;

&lt;p&gt;Here's the test I like: &lt;strong&gt;block the JavaScript bundle entirely and reload.&lt;/strong&gt; React never mounts — and the page still comes up in the right theme, because the only thing that ran was the inline script.&lt;/p&gt;




&lt;h2&gt;
  
  
  What else 2.0 adds
&lt;/h2&gt;

&lt;p&gt;Fixing the four bugs was the reason for a rewrite. Once I was in there, three things were worth adding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unlimited themes
&lt;/h3&gt;

&lt;p&gt;Light and dark was always an artificial limit. The hook writes a string to an attribute; it has no opinion about which strings are legal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeProvider&lt;/span&gt;
  &lt;span class="na"&gt;themes&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&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;dark&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;sepia&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;nord&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;colorSchemes&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="na"&gt;sepia&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&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;ThemeProvider&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;code&gt;colorSchemes&lt;/code&gt; is the part that's easy to miss. It sets the CSS &lt;code&gt;color-scheme&lt;/code&gt; property so &lt;strong&gt;native&lt;/strong&gt; UI — scrollbars, checkboxes, the text caret, date pickers — matches your custom theme. Without it, a beautiful dark "nord" page gets a bright white scrollbar.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toggleTheme()&lt;/code&gt; now cycles the whole list and wraps around. With the default two themes, that's still a plain toggle.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;class&lt;/code&gt; or any attribute
&lt;/h3&gt;

&lt;p&gt;v1 hardcoded &lt;code&gt;data-theme&lt;/code&gt;. Tailwind's &lt;code&gt;dark:&lt;/code&gt; variant wants a class, so Tailwind users had to reconfigure their build to match my library — backwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeProvider&lt;/span&gt; &lt;span class="na"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"class"&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;Tailwind v4 then needs one line of CSS:&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="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@custom-variant&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="err"&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 can also pass an array — &lt;code&gt;attribute={["class", "data-theme"]}&lt;/code&gt; — if part of your stack wants one and part wants the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  A configurable storage key
&lt;/h3&gt;

&lt;p&gt;v1 hardcoded &lt;code&gt;"app-theme"&lt;/code&gt;. If you had another library using the same key, or wanted a per-tenant key, you were out of luck.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeProvider&lt;/span&gt; &lt;span class="na"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"my-app-theme"&lt;/span&gt; &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"session"&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;code&gt;storage&lt;/code&gt; accepts &lt;code&gt;"local"&lt;/code&gt;, &lt;code&gt;"session"&lt;/code&gt; or &lt;code&gt;"none"&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The size question
&lt;/h2&gt;

&lt;p&gt;The whole point of this package is being small, so:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;you import&lt;/th&gt;
&lt;th&gt;gzipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;useTheme&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.84 kB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;useTheme&lt;/code&gt; + &lt;code&gt;ThemeProvider&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2.64 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;themeScript&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;0.84 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;everything&lt;/td&gt;
&lt;td&gt;2.66 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero runtime dependencies. &lt;code&gt;sideEffects: false&lt;/code&gt; and dual ESM/CJS output, so bundlers can drop what you don't import. TypeScript declarations ship with the package.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migrating
&lt;/h2&gt;

&lt;p&gt;Nothing was removed. &lt;code&gt;useTheme&lt;/code&gt;, &lt;code&gt;theme&lt;/code&gt;, &lt;code&gt;toggleTheme&lt;/code&gt;, &lt;code&gt;setLight&lt;/code&gt; and &lt;code&gt;setDark&lt;/code&gt; all behave the same.&lt;/p&gt;

&lt;p&gt;Two things to check:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The default storage key changed&lt;/strong&gt; from &lt;code&gt;app-theme&lt;/code&gt; to &lt;code&gt;theme&lt;/code&gt;. If you have users with a saved preference, keep it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeProvider&lt;/span&gt; &lt;span class="na"&gt;storageKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"app-theme"&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;2. &lt;code&gt;theme&lt;/code&gt; can now be &lt;code&gt;"system"&lt;/code&gt;.&lt;/strong&gt; If you branch on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- const { theme } = useTheme();
- const isDark = theme === "dark";
&lt;/span&gt;&lt;span class="gi"&gt;+ const { isDark } = useTheme();
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;p&gt;The bugs weren't subtle. &lt;code&gt;useState&lt;/code&gt; inside a hook creating per-instance state is something I'd explain to someone else in an interview. Reading a media query once instead of subscribing is a well-known trap. I still shipped both.&lt;/p&gt;

&lt;p&gt;What let them through wasn't ignorance, it was the &lt;strong&gt;shape of my testing&lt;/strong&gt;. One component, one browser, one tab, storage enabled, OS theme never changing, always on localhost with a warm cache. Every one of the four bugs lives outside that box.&lt;/p&gt;

&lt;p&gt;The demo I've built for v2 is deliberately harder on itself: four themes, several components reading the hook at once, a live readout of what the hook returns, and a second-tab test. Every bug above would be visible on that page within about ten seconds.&lt;/p&gt;

&lt;p&gt;If you maintain a small package: the day someone opens an issue is the day it starts getting good.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://use-theme-mode-demo.vercel.app/" rel="noopener noreferrer"&gt;use-theme-mode-demo.vercel.app&lt;/a&gt; — four themes, a live view of the hook's return value, and the OS-change test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm:&lt;/strong&gt; &lt;code&gt;npm install use-theme-mode&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/saadahmad888/use-theme-mode" rel="noopener noreferrer"&gt;saadahmad888/use-theme-mode&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Issues and PRs welcome. If v2 breaks something for you, that's the useful kind of feedback — tell me.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My 4KB React package was installing 116 dependencies. Here's what I found when I audited it.</title>
      <dc:creator>Saad Ahmad</dc:creator>
      <pubDate>Mon, 24 Aug 2026 12:34:59 +0000</pubDate>
      <link>https://dev.to/saadahmad/my-4kb-react-package-was-installing-116-dependencies-heres-what-i-found-when-i-audited-it-47ig</link>
      <guid>https://dev.to/saadahmad/my-4kb-react-package-was-installing-116-dependencies-heres-what-i-found-when-i-audited-it-47ig</guid>
      <description>&lt;p&gt;I published a small React component to npm. Four kilobytes. One job: stack cards as you scroll.&lt;/p&gt;

&lt;p&gt;Then I sat down and actually audited it — the way you'd audit someone else's package before adding it to production.&lt;/p&gt;

&lt;p&gt;It was installing &lt;strong&gt;116 packages&lt;/strong&gt; onto every machine that ran &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's everything that was wrong, why each one mattered, and how to check your own packages tonight. 🔍&lt;/p&gt;




&lt;h2&gt;
  
  
  🚨 Bug 1: I was shipping a bundler to my users
&lt;/h2&gt;

&lt;p&gt;This was in my &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rollup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.40.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rollup-plugin-postcss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.0.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@types/react"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^19.1.2"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are &lt;strong&gt;build tools&lt;/strong&gt;. They belong in &lt;code&gt;devDependencies&lt;/code&gt;. Sitting in &lt;code&gt;dependencies&lt;/code&gt;, npm faithfully installed all three — plus their transitive deps — into every consuming project.&lt;/p&gt;

&lt;p&gt;I measured it. Installing that dependency set pulls &lt;strong&gt;116 packages&lt;/strong&gt;. My actual library is one file.&lt;/p&gt;

&lt;p&gt;The fix is a two-second edit. The lesson is that nobody ever looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm view your-package dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that on your own packages right now. If a build tool comes back, that's a bundler on someone else's laptop. 😬&lt;/p&gt;




&lt;h2&gt;
  
  
  💥 Bug 2: React's JSX runtime was baked into my bundle
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;rollup.config.js&lt;/code&gt; had 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="nx"&gt;external&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;react&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;react-dom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks right. It isn't.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;tsconfig.json&lt;/code&gt; used &lt;code&gt;"jsx": "react-jsx"&lt;/code&gt;, so every component compiled to an import from &lt;strong&gt;&lt;code&gt;react/jsx-runtime&lt;/code&gt;&lt;/strong&gt; — a different specifier than &lt;code&gt;react&lt;/code&gt;. It wasn't in &lt;code&gt;external&lt;/code&gt;, so Rollup happily inlined it.&lt;/p&gt;

&lt;p&gt;Worse: it inlined the &lt;em&gt;development&lt;/em&gt; build, which contains 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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;process&lt;/code&gt; doesn't exist in a browser. Anyone importing my package through plain ESM or certain Vite configs got a hard crash: &lt;code&gt;process is not defined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had &lt;code&gt;rollup-plugin-peer-deps-external&lt;/code&gt; installed to prevent exactly this. But I'd never declared any &lt;code&gt;peerDependencies&lt;/code&gt; — so it read an empty list and did nothing. A safety net bolted to the ceiling. 🪤&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;// what it should have been&lt;/span&gt;
&lt;span class="nx"&gt;external&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;react&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;react-dom&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;react/jsx-runtime&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;react/jsx-dev-runtime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚛️ Bug 3: No &lt;code&gt;"use client"&lt;/code&gt;, so Next.js rejected it
&lt;/h2&gt;

&lt;p&gt;My components use hooks. Under the Next.js App Router, that makes them client components — and they must say so.&lt;/p&gt;

&lt;p&gt;Mine didn't. Anyone on modern Next.js hit an error immediately.&lt;/p&gt;

&lt;p&gt;The fix is one line at the top of the built bundle. But here's the part that nearly bit me twice: &lt;strong&gt;esbuild strips module-level directives when it bundles&lt;/strong&gt;, and &lt;code&gt;tsup&lt;/code&gt;'s &lt;code&gt;banner&lt;/code&gt; option doesn't survive minification.&lt;/p&gt;

&lt;p&gt;I only caught it because I checked the built file instead of trusting the config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 20 dist/index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a post-build script adds the directive, and a test asserts it's there. Which brings me to the thing I'd most want you to take away. 👇&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 The real lesson: test the artifact, not the source
&lt;/h2&gt;

&lt;p&gt;Every bug above lived in &lt;code&gt;dist/&lt;/code&gt;, not &lt;code&gt;src/&lt;/code&gt;. My source code was fine. Unit tests passing means nothing if the thing you &lt;em&gt;ship&lt;/em&gt; is broken.&lt;/p&gt;

&lt;p&gt;So I wrote tests that read the built output:&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;never references process.env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;esm&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;process.env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;leaves the JSX runtime external&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;esm&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/from&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;react&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;jsx-runtime&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;opens with the use client directive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;esm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those exists because the previous release shipped that exact mistake. They're regression tests for bugs I'd already inflicted on people.&lt;/p&gt;

&lt;p&gt;Two tools do most of this for you, and both take under a minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx publint          &lt;span class="c"&gt;# catches malformed exports, wrong entry points&lt;/span&gt;
npx @arethetypeswrong/cli &lt;span class="nt"&gt;--pack&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;   &lt;span class="c"&gt;# catches broken TypeScript resolution&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you maintain a package and have never run these, go run them. 🎯&lt;/p&gt;




&lt;h2&gt;
  
  
  🔎 Bug 4: Zero keywords, zero downloads
&lt;/h2&gt;

&lt;p&gt;My npm page said &lt;strong&gt;Keywords: none&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;npm search is largely keyword-driven. No keywords means you are effectively unlisted. My weekly downloads were zero, and I'd assumed the idea was unpopular. It was just invisible.&lt;/p&gt;

&lt;p&gt;My description also read &lt;code&gt;"Reuseable Stack on scroll librabry"&lt;/code&gt; — two typos in the single line that shows up in search results. 🙈&lt;/p&gt;




&lt;h2&gt;
  
  
  🐛 Bug 5: The one my tests couldn't catch
&lt;/h2&gt;

&lt;p&gt;After shipping v2, I rebuilt my demo site on it — and the build failed with a TypeScript error.&lt;/p&gt;

&lt;p&gt;My hook returned &lt;code&gt;RefObject&amp;lt;T | null&amp;gt;&lt;/code&gt;. React 19's types accept that. React 18's &lt;strong&gt;don't&lt;/strong&gt;, because React 18 treats &lt;code&gt;RefObject&lt;/code&gt; as covariant.&lt;/p&gt;

&lt;p&gt;My package claimed &lt;code&gt;"react": "&amp;gt;=18"&lt;/code&gt;. For every TypeScript user on React 18, it didn't compile.&lt;/p&gt;

&lt;p&gt;My CI &lt;em&gt;had&lt;/em&gt; a React 18 job. It ran the test suite. But &lt;strong&gt;tests don't typecheck against React 18&lt;/strong&gt; — Vitest ran against whatever types were installed. The job was theater.&lt;/p&gt;

&lt;p&gt;The fix was a compile-only fixture:&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;// test/types/consumer.tsx — never runs, just has to compile&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PassesHookRefToCard&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useStackProgress&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;onProgress&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&amp;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;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="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="nc"&gt;StackCard&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;first&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;StackCard&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;Plus CI that installs the matching &lt;code&gt;@types/react&lt;/code&gt; for each React major and runs &lt;code&gt;tsc&lt;/code&gt;. I verified it works by reverting the fix — the error came right back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you support a version range, prove it. Don't just declare it.&lt;/strong&gt; ✅&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ The 10-minute audit for your own packages
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm view your-package dependencies   &lt;span class="c"&gt;# build tools in here? move them&lt;/span&gt;
npm view your-package keywords       &lt;span class="c"&gt;# empty? nobody can find you&lt;/span&gt;
npm publish &lt;span class="nt"&gt;--dry-run&lt;/span&gt;                &lt;span class="c"&gt;# see exactly what ships&lt;/span&gt;
npx publint                          &lt;span class="c"&gt;# manifest correctness&lt;/span&gt;
npx @arethetypeswrong/cli &lt;span class="nt"&gt;--pack&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;   &lt;span class="c"&gt;# TypeScript resolution&lt;/span&gt;
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 20 dist/index.js             &lt;span class="c"&gt;# is "use client" actually there?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Six commands. They'd have caught all five of my bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Where the package landed
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/stack-on-scroll" rel="noopener noreferrer"&gt;&lt;strong&gt;stack-on-scroll&lt;/strong&gt;&lt;/a&gt; is now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies.&lt;/strong&gt; 4 packages install total, versus 116 before&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero config&lt;/strong&gt; — no stylesheet to import, styles are inline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero JavaScript on the default path&lt;/strong&gt; — the stacking is pure &lt;code&gt;position: sticky&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Opt-in scale, fade, and tilt, all sharing one animation frame&lt;/li&gt;
&lt;li&gt;Full TypeScript types, verified against React 18 &lt;em&gt;and&lt;/em&gt; 19&lt;/li&gt;
&lt;li&gt;58 tests, including ones that read the built artifact
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;stack-on-scroll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StackContainer&lt;/span&gt; &lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;scaleStep&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;0.07&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fadeStep&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;0.35&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;chapters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StackCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&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="nx"&gt;c&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="nc"&gt;StackCard&lt;/span&gt;&lt;span class="p"&gt;&amp;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;lt;/&lt;/span&gt;&lt;span class="nc"&gt;StackContainer&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;No &lt;code&gt;index&lt;/code&gt; prop — the container counts its own children.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://stack-on-scroll.vercel.app/" rel="noopener noreferrer"&gt;Live demo with sliders for every prop&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
🐙 &lt;strong&gt;&lt;a href="https://github.com/saadahmad888/stack-on-scroll" rel="noopener noreferrer"&gt;Source on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  👋 About me
&lt;/h2&gt;

&lt;p&gt;I'm &lt;strong&gt;Saad Ahmad&lt;/strong&gt;, a freelance frontend developer. I build React and Next.js interfaces, and — as you can probably tell — I enjoy the unglamorous part: the packaging, the build config, the reason your bundle is 400KB when it should be 40.&lt;/p&gt;

&lt;p&gt;If your team is shipping a component library, fighting a slow build, or has an npm package nobody can find, that's the kind of work I like. &lt;strong&gt;I'm currently open to freelance projects.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;📮 &lt;a href="https://isaadahmad.com" rel="noopener noreferrer"&gt;isaadahmad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found a bug in your own package after reading this? I'd genuinely love to hear about it in the comments — misery loves company.&lt;/em&gt; 😄&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>npm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Lightweight Dark/Light Mode System in React with use-theme-mode</title>
      <dc:creator>Saad Ahmad</dc:creator>
      <pubDate>Tue, 14 Apr 2026 13:44:50 +0000</pubDate>
      <link>https://dev.to/saadahmad/building-a-lightweight-darklight-mode-system-in-react-with-use-theme-mode-42o0</link>
      <guid>https://dev.to/saadahmad/building-a-lightweight-darklight-mode-system-in-react-with-use-theme-mode-42o0</guid>
      <description>&lt;p&gt;Dark mode has become a standard expectation in modern web applications. But implementing it &lt;strong&gt;correctly&lt;/strong&gt;—with persistence, system preference detection, and clean separation of logic and styles—is often more complicated than it should be.&lt;/p&gt;

&lt;p&gt;That’s exactly why I built &lt;strong&gt;&lt;code&gt;use-theme-mode&lt;/code&gt;&lt;/strong&gt;, a lightweight React hook that handles theme state for you—without enforcing any styling system.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Why Another Theme Hook?
&lt;/h2&gt;

&lt;p&gt;Most theme solutions today fall into one of two categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Too opinionated&lt;/strong&gt; (UI framework–specific)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too heavy&lt;/strong&gt; for simple use cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Works with &lt;strong&gt;any CSS solution&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Persists user preference&lt;/li&gt;
&lt;li&gt;✅ Respects system theme&lt;/li&gt;
&lt;li&gt;✅ Is &lt;strong&gt;SSR‑safe&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Has &lt;strong&gt;zero runtime dependencies&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That became &lt;strong&gt;&lt;code&gt;use-theme-mode&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;use-theme-mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 Basic Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;useTheme&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;use-theme-mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toggleTheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTheme&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;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="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Current theme: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Behind the scenes, the hook:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Detects&lt;/strong&gt; system preference&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Saves&lt;/strong&gt; the theme to &lt;code&gt;localStorage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Updates&lt;/strong&gt; the &lt;code&gt;&amp;lt;html data-theme="..."&amp;gt;&lt;/code&gt; attribute&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Syncs&lt;/strong&gt; changes across tabs&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🎨 Styling Is 100% in Your Control
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;use-theme-mode&lt;/code&gt; does not inject styles. Instead, it gives you a single source of truth via &lt;code&gt;data-theme&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS Implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"light"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;:root&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#121212&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&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;background-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;--bg-color&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;--text-color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach works beautifully with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tailwind&lt;/li&gt;
&lt;li&gt;CSS variables&lt;/li&gt;
&lt;li&gt;Styled-components&lt;/li&gt;
&lt;li&gt;MUI / Chakra&lt;/li&gt;
&lt;li&gt;Vanilla CSS&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔄 Works Great with Tailwind Too
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JavaScript Configuration:&lt;/strong&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="c1"&gt;// tailwind.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;darkMode&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;attribute&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;data-theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JSX Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-white text-black dark:bg-black dark:text-white"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Hello world
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🌍 Live Demo
&lt;/h2&gt;

&lt;p&gt;I’ve deployed a live demo showcasing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real theme switching&lt;/li&gt;
&lt;li&gt;Persistent state&lt;/li&gt;
&lt;li&gt;Code examples synced with UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;a href="https://use-theme-mode-demo.vercel.app/" rel="noopener noreferrer"&gt;View Live Demo&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 API Reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDark&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;theme&lt;/code&gt;&lt;/strong&gt; → &lt;code&gt;"light" | "dark"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;toggleTheme()&lt;/code&gt;&lt;/strong&gt; → switch modes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;setLight()&lt;/code&gt;&lt;/strong&gt; → force light mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;setDark()&lt;/code&gt;&lt;/strong&gt; → force dark mode&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛡️ SSR Safe &amp;amp; Production Ready
&lt;/h2&gt;

&lt;p&gt;The hook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoids direct &lt;code&gt;window&lt;/code&gt; access during render.&lt;/li&gt;
&lt;li&gt;Works in &lt;strong&gt;Next.js&lt;/strong&gt; and other SSR frameworks.&lt;/li&gt;
&lt;li&gt;Synchronizes changes across tabs automatically.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔮 What’s Coming Next?
&lt;/h2&gt;

&lt;p&gt;Plans for future versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ TypeScript-first API&lt;/li&gt;
&lt;li&gt;✅ Optional context provider&lt;/li&gt;
&lt;li&gt;✅ ESM build support&lt;/li&gt;
&lt;li&gt;✅ Custom storage keys&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/use-theme-mode" rel="noopener noreferrer"&gt;use-theme-mode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/saadahmad888/use-theme-mode" rel="noopener noreferrer"&gt;saadahmad888/use-theme-mode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://use-theme-mode-demo.vercel.app/" rel="noopener noreferrer"&gt;Vercel Demo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find this useful, I’d love feedback or contributions!&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>react</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Digging Deep into the Core of Frontend Development</title>
      <dc:creator>Saad Ahmad</dc:creator>
      <pubDate>Sat, 08 Mar 2025 14:35:15 +0000</pubDate>
      <link>https://dev.to/saadahmad/digging-deep-into-the-core-of-frontend-development-oi3</link>
      <guid>https://dev.to/saadahmad/digging-deep-into-the-core-of-frontend-development-oi3</guid>
      <description>&lt;p&gt;Frontend development is more than just writing HTML, CSS, and JavaScript. It’s about creating seamless user experiences, ensuring performance, and writing maintainable code. If you truly want to master frontend development, you need to dig deep into its core concepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;strong&gt;HTML - The Backbone&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;HTML (HyperText Markup Language) structures the web. Understanding semantic HTML is crucial for accessibility and SEO. Don't just use &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; for everything; use &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt; where appropriate.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;strong&gt;CSS - The Art of Styling&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;CSS (Cascading Style Sheets) is what makes the web beautiful. Mastering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flexbox &amp;amp; Grid for layout&lt;/li&gt;
&lt;li&gt;Variables for maintainability&lt;/li&gt;
&lt;li&gt;Transitions &amp;amp; animations for better UX&lt;/li&gt;
&lt;li&gt;Responsive design techniques&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;will help you create pixel-perfect designs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;strong&gt;JavaScript - The Brain&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;JavaScript brings life to your UI. Core concepts to master:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ES6+ features (let, const, arrow functions, destructuring)&lt;/li&gt;
&lt;li&gt;DOM Manipulation&lt;/li&gt;
&lt;li&gt;Event Handling&lt;/li&gt;
&lt;li&gt;Fetch API &amp;amp; Async/Await&lt;/li&gt;
&lt;li&gt;Functional Programming principles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. &lt;strong&gt;Frameworks &amp;amp; Libraries&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;While JavaScript is powerful, frameworks like React, Vue, or Angular make development faster and more scalable. For React.js enthusiasts, understanding Hooks, Context API, and performance optimizations is key.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;strong&gt;State Management&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;State management is the heart of modern web apps. Whether you use React’s Context API, Redux, Zustand, or Recoil, understanding state flow and performance optimization will set you apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. &lt;strong&gt;Performance Optimization&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A fast website means a better user experience. Optimize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image loading (WebP, lazy loading)&lt;/li&gt;
&lt;li&gt;Code splitting &amp;amp; tree shaking&lt;/li&gt;
&lt;li&gt;Server-side rendering (SSR) &amp;amp; Static Site Generation (SSG)&lt;/li&gt;
&lt;li&gt;Caching strategies&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. &lt;strong&gt;Version Control (Git/GitHub)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A good frontend developer knows Git. You should be comfortable with branching, merging, rebasing, and handling pull requests. Speaking of GitHub, let’s connect! 😃&lt;/p&gt;

&lt;p&gt;📌 Follow me on GitHub: &lt;a href="https://github.com/saadahmad888" rel="noopener noreferrer"&gt;saadahmad888&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. &lt;strong&gt;Testing &amp;amp; Debugging&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Don’t just write code—test it! Get familiar with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Console debugging&lt;/li&gt;
&lt;li&gt;Unit testing (Jest, React Testing Library)&lt;/li&gt;
&lt;li&gt;End-to-end testing (Cypress, Playwright)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. &lt;strong&gt;Understanding APIs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Frontend isn't just UI. Learning how to consume RESTful and GraphQL APIs will help you build dynamic applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. &lt;strong&gt;Soft Skills &amp;amp; Community Contribution&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Being a great frontend developer is not just about coding. It’s about collaboration, problem-solving, and sharing knowledge.&lt;/p&gt;

&lt;p&gt;🖥️ Want to see my work? Visit my portfolio: &lt;a href="https://isaadahmad.com" rel="noopener noreferrer"&gt;isaadahmad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🔥 If you're passionate about frontend and want to stay updated, let’s connect on GitHub and build something awesome together! 🚀&lt;/p&gt;

&lt;p&gt;Drop a comment below—what's your favorite frontend concept? 😊&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>react</category>
      <category>css</category>
    </item>
    <item>
      <title>🚀 How AI is Revolutionizing Frontend Development 🤖</title>
      <dc:creator>Saad Ahmad</dc:creator>
      <pubDate>Thu, 06 Mar 2025 10:55:19 +0000</pubDate>
      <link>https://dev.to/saadahmad/how-ai-is-revolutionizing-frontend-development-1apm</link>
      <guid>https://dev.to/saadahmad/how-ai-is-revolutionizing-frontend-development-1apm</guid>
      <description>&lt;p&gt;Artificial Intelligence (AI) is transforming every industry, and frontend development is no exception. With AI-powered tools and frameworks, developers can now create faster, more efficient, and highly personalized user experiences. Let’s dive into how AI is reshaping frontend development and what it means for developers. 🛠️&lt;/p&gt;

&lt;h2&gt;
  
  
  1️⃣ AI-Powered Code Generation ✨
&lt;/h2&gt;

&lt;p&gt;Tools like &lt;strong&gt;GitHub Copilot&lt;/strong&gt;, &lt;strong&gt;Tabnine&lt;/strong&gt;, and &lt;strong&gt;Codeium&lt;/strong&gt; are helping developers write code faster and with fewer errors. These AI-powered assistants suggest code snippets, complete functions, and even generate entire components based on context. This not only speeds up development but also reduces cognitive load. ⚡&lt;/p&gt;

&lt;h2&gt;
  
  
  2️⃣ Automated UI/UX Design 🎨
&lt;/h2&gt;

&lt;p&gt;AI is making UI/UX design more intuitive with tools like &lt;strong&gt;Figma AI&lt;/strong&gt; and &lt;strong&gt;Adobe Sensei&lt;/strong&gt;. These tools analyze user behavior and suggest design improvements, color palettes, and layouts, making it easier for developers to create visually appealing interfaces. 🖌️&lt;/p&gt;

&lt;h2&gt;
  
  
  3️⃣ AI for Accessibility and Responsiveness 📱
&lt;/h2&gt;

&lt;p&gt;AI-driven tools can analyze websites and ensure accessibility compliance with standards like &lt;strong&gt;WCAG&lt;/strong&gt;. They also help in making layouts responsive by predicting how elements should adjust for different screen sizes and devices. 🔍&lt;/p&gt;

&lt;h2&gt;
  
  
  4️⃣ AI-Based Testing and Debugging 🛠️
&lt;/h2&gt;

&lt;p&gt;AI-powered testing frameworks like &lt;strong&gt;Selenium AI&lt;/strong&gt;, &lt;strong&gt;TestCraft&lt;/strong&gt;, and &lt;strong&gt;Applitools&lt;/strong&gt; can automate UI testing, detect anomalies, and provide insights on how to fix them. This saves time in debugging and ensures a smooth user experience. ✅&lt;/p&gt;

&lt;h2&gt;
  
  
  5️⃣ Personalized User Experiences 🎯
&lt;/h2&gt;

&lt;p&gt;AI enables real-time personalization based on user behavior, preferences, and interactions. Recommendation engines, dynamic content rendering, and chatbot integrations are all powered by AI to enhance user engagement. 🤝&lt;/p&gt;

&lt;h2&gt;
  
  
  🔮 What’s Next?
&lt;/h2&gt;

&lt;p&gt;As AI continues to evolve, we can expect more intelligent automation in frontend development. From AI-generated animations to real-time code optimizations, the possibilities are endless. While AI won't replace frontend developers, it will certainly enhance their capabilities and streamline workflows. 💡&lt;/p&gt;

&lt;p&gt;What are your thoughts on AI in frontend development? Are you using any AI-powered tools in your projects? Let’s discuss in the comments! 🚀&lt;/p&gt;

</description>
    </item>
    <item>
      <title>🚀 My Journey in Frontend Development &amp; Building with the Community</title>
      <dc:creator>Saad Ahmad</dc:creator>
      <pubDate>Thu, 13 Feb 2025 06:10:55 +0000</pubDate>
      <link>https://dev.to/saadahmad/my-journey-in-frontend-development-building-with-the-community-31h3</link>
      <guid>https://dev.to/saadahmad/my-journey-in-frontend-development-building-with-the-community-31h3</guid>
      <description>&lt;p&gt;Hey Dev.to! 👋 I'm excited to share my journey as a frontend developer and how I keep up with the ever-evolving world of web development. From building innovative tools to engaging with the dev community, here’s my story.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 Who Am I?
&lt;/h2&gt;

&lt;p&gt;I'm a &lt;strong&gt;frontend developer&lt;/strong&gt; passionate about crafting high-performance user interfaces with &lt;strong&gt;React, Next.js, and modern web technologies&lt;/strong&gt;. I love solving problems, optimizing performance, and exploring new trends in frontend development. My journey has been fueled by curiosity, continuous learning, and a deep love for coding.&lt;/p&gt;

&lt;h3&gt;
  
  
  💡 Things I’m Working On:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-Source Projects&lt;/strong&gt; → I maintain &lt;strong&gt;npm packages&lt;/strong&gt; like &lt;a href="https://www.npmjs.com/package/stack-on-scroll" rel="noopener noreferrer"&gt;stack-on-scroll&lt;/a&gt;, helping developers create better UI experiences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend Innovation&lt;/strong&gt; → I’m building a &lt;strong&gt;Skeleton Screen Generator&lt;/strong&gt;, making it easier for developers to create seamless loading states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speech-to-Text &amp;amp; Text-to-Speech in Urdu&lt;/strong&gt; → I’m working on implementing this feature to enhance accessibility in apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔥 Staying Updated with Frontend Trends
&lt;/h2&gt;

&lt;p&gt;Frontend development is always changing! Here’s how I stay ahead:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Exploring New Features&lt;/strong&gt; → React Server Components (RSC), Next.js advancements, and modern UI patterns.&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Experimenting with Tech&lt;/strong&gt; → Trying new tools and frameworks to build better web apps.&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Engaging in the Dev Community&lt;/strong&gt; → Writing blogs, sharing knowledge, and connecting with like-minded developers.  &lt;/p&gt;

&lt;h2&gt;
  
  
  🏗 How I Build &amp;amp; Share
&lt;/h2&gt;

&lt;p&gt;One of my goals is to &lt;strong&gt;share my knowledge&lt;/strong&gt; with others. Writing blogs, building open-source tools, and contributing to the dev community help me grow as a developer.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✍️ Topics I Write About:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modern Frontend Trends&lt;/strong&gt; → React, Next.js, and emerging frameworks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance Optimization&lt;/strong&gt; → Making apps faster and more efficient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Tools &amp;amp; Productivity&lt;/strong&gt; → Tips and tricks for better frontend development.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 Let’s Connect &amp;amp; Grow Together!
&lt;/h2&gt;

&lt;p&gt;I’d love to hear from you! Whether it's discussing &lt;strong&gt;frontend trends, performance hacks, or open-source projects&lt;/strong&gt;, let’s build together. Drop a comment, share your thoughts, or connect with me on &lt;strong&gt;&lt;a href="https://isaadahmad.com" rel="noopener noreferrer"&gt;isaadahmad.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s your favorite frontend trend right now? Let’s discuss!👇
&lt;/h3&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>frontend</category>
      <category>servercomponents</category>
    </item>
  </channel>
</rss>
