<?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>Forem: Divyam Gupta</title>
    <description>The latest articles on Forem by Divyam Gupta (@divyamdotfoo).</description>
    <link>https://forem.com/divyamdotfoo</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1196561%2F2cb71a89-22d5-422c-9c91-37d27551ab1d.jpg</url>
      <title>Forem: Divyam Gupta</title>
      <link>https://forem.com/divyamdotfoo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/divyamdotfoo"/>
    <language>en</language>
    <item>
      <title>A Practical Guide to Secure 2FA with TOTP</title>
      <dc:creator>Divyam Gupta</dc:creator>
      <pubDate>Mon, 23 Feb 2026 15:31:29 +0000</pubDate>
      <link>https://forem.com/divyamdotfoo/a-practical-guide-to-secure-2fa-with-totp-560m</link>
      <guid>https://forem.com/divyamdotfoo/a-practical-guide-to-secure-2fa-with-totp-560m</guid>
      <description>&lt;h3&gt;
  
  
  Why Your Password Isn't Enough Anymore?
&lt;/h3&gt;

&lt;p&gt;Passwords get reused, phished, and leaked. It's not a matter of if, it's when. And when it happens, your app pays the price even if you did everything right on your end.&lt;/p&gt;

&lt;p&gt;The fix? Add a second layer. Something that lives on the user's phone, changes every 30 seconds, and works even without internet. That's TOTP-based 2FA, and by the end of this guide you'll have it fully wired up in your TypeScript app.&lt;/p&gt;

&lt;p&gt;Let's build it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: This article consists of pieces and excerpts from an educational conversation with &lt;code&gt;Claude Sonnet 4.6&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  What Even Is TOTP?
&lt;/h3&gt;

&lt;p&gt;TOTP stands for &lt;strong&gt;Time-based One-Time Password&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a user enables 2FA on your app, your server generates a &lt;strong&gt;shared secret&lt;/strong&gt;. It's basically a random string that both your server and the user's authenticator app know about. Using that secret and the current time, both sides independently calculate the same 6-digit code. The user types it in, you verify it, done.&lt;/p&gt;

&lt;p&gt;The code refreshes every 30 seconds and is only valid for that window. So even if someone intercepts it, it's already useless by the time they try to use it.&lt;/p&gt;

&lt;p&gt;You've probably seen this in action. That little 6-digit code ticking down in &lt;strong&gt;Google Authenticator&lt;/strong&gt; or &lt;strong&gt;Authy&lt;/strong&gt;? That's TOTP.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why TOTP over SMS OTP?
&lt;/h4&gt;

&lt;p&gt;SMS OTP is convenient but has real problems. SIM swap attacks are a thing, carriers can be socially engineered, and SMS simply doesn't work when you have no signal. TOTP has none of these issues. The code is generated entirely on the device, no network required.&lt;/p&gt;

&lt;p&gt;It's also an open standard (&lt;a href="https://datatracker.ietf.org/doc/html/rfc6238" rel="noopener noreferrer"&gt;RFC 6238&lt;/a&gt;), which means it works with any authenticator app including Google Authenticator, Authy, 1Password, and Bitwarden. You're not locking anyone into anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwyyngxwcculok2p161v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwyyngxwcculok2p161v.png" alt="TOTP shared secret diagram" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  How It Actually Works Under the Hood
&lt;/h3&gt;

&lt;p&gt;You don't need to understand cryptography to implement TOTP, but knowing the flow will save you a lot of confusion later.&lt;/p&gt;

&lt;p&gt;Here's what happens, step by step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Secret Generation&lt;/strong&gt;&lt;br&gt;
When a user enables 2FA, your server generates a &lt;strong&gt;random secret key&lt;/strong&gt;. It's a short unique string tied to that user. Think of it as a shared password between your server and their authenticator app. This secret is stored on your server and never shown to the user directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. QR Code&lt;/strong&gt;&lt;br&gt;
Instead of asking the user to type the secret manually, you encode it into a &lt;strong&gt;QR code&lt;/strong&gt; using a standard format called &lt;a href="https://docs.yubico.com/yesdk/users-manual/application-totp/uri-format.html" rel="noopener noreferrer"&gt;otpauth URL&lt;/a&gt;. The user scans it with their authenticator app, and now the app has the secret stored locally on their phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Code Generation&lt;/strong&gt;&lt;br&gt;
Every 30 seconds, the authenticator app combines the &lt;strong&gt;secret + current timestamp&lt;/strong&gt; and runs it through a hashing algorithm (&lt;a href="https://en.wikipedia.org/wiki/HMAC" rel="noopener noreferrer"&gt;HMAC-SHA1&lt;/a&gt;) to produce a 6-digit code. Your server does the exact same calculation independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Verification&lt;/strong&gt;&lt;br&gt;
When the user submits a code, your server runs the same calculation and checks if both results match. If yes, access granted. No network calls, no third-party service, just math on both ends arriving at the same answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The key insight:&lt;/strong&gt; Neither side is &lt;em&gt;sending&lt;/em&gt; the code to the other. Both sides are &lt;em&gt;independently calculating&lt;/em&gt; the same value because they share the same secret and trust the same clock.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F471lspielqak26z6aoea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F471lspielqak26z6aoea.png" alt="TOTP flow diagram" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Setting Up TOTP in a TypeScript Project
&lt;/h3&gt;

&lt;p&gt;We'll be using two packages: &lt;a href="https://www.npmjs.com/package/otplib" rel="noopener noreferrer"&gt;otplib&lt;/a&gt; for everything TOTP-related and &lt;a href="https://www.npmjs.com/package/qrcode" rel="noopener noreferrer"&gt;qrcode&lt;/a&gt; to generate the QR code image.&lt;br&gt;
&lt;/p&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;otplib qrcode
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @types/qrcode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;otplib&lt;/code&gt; ships with its own TypeScript types so no extra install needed there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating a Secret&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user wants to enable 2FA, the first thing you do is generate a secret for them on the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authenticator&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;otplib&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;const&lt;/span&gt; &lt;span class="nx"&gt;generateSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userEmail&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="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;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authenticator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateSecret&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;otpauthUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authenticator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keyuri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YourAppName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&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;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;otpauthUrl&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;generateSecret()&lt;/code&gt; gives you a random Base32 string. &lt;code&gt;keyuri()&lt;/code&gt; packages it into a standard &lt;code&gt;otpauth://&lt;/code&gt; URL that any authenticator app understands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turning That URL Into a QR Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;otpauthUrl&lt;/code&gt; alone isn't useful to the user. We need to render it as a scannable QR code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;QRCode&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;qrcode&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;const&lt;/span&gt; &lt;span class="nx"&gt;generateQRCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;otpauthUrl&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;QRCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;otpauthUrl&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 returns a &lt;strong&gt;base64 image string&lt;/strong&gt; that you can drop directly into an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag on the frontend. No file storage needed.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;otpauthUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&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;qrCodeImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateQRCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;otpauthUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Send qrCodeImage to the frontend&lt;/span&gt;
&lt;span class="c1"&gt;// Store secret temporarily until the user verifies setup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Don't save the secret to your database yet.&lt;/strong&gt; Wait until the user successfully scans the QR and verifies with a valid code. Otherwise you might store secrets for setups that were never completed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fahago8kcdm2xzqkb4ey2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fahago8kcdm2xzqkb4ey2.png" alt="QR code setup screenshot" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Registration Flow
&lt;/h3&gt;

&lt;p&gt;This is where everything comes together for the first time. The goal is simple: let the user enable 2FA and confirm they've set it up correctly before you commit anything to the database.&lt;/p&gt;

&lt;p&gt;Here's the full picture of what we're building.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;User clicks "Enable 2FA"&lt;/code&gt; → &lt;code&gt;Backend generates secret + QR&lt;/code&gt; → &lt;code&gt;Frontend shows QR&lt;/code&gt; → &lt;code&gt;User scans with authenticator app&lt;/code&gt; → &lt;code&gt;User submits a verification code&lt;/code&gt; → &lt;code&gt;Backend verifies it&lt;/code&gt; → &lt;code&gt;Secret saved to DB&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Generate and Return the QR Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The user hits "Enable 2FA" in your UI. Your backend generates a secret, creates a QR code, and sends it back. The secret goes into a &lt;strong&gt;temporary store&lt;/strong&gt; like a user session or a short-lived cache, not the database yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Verify the Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After scanning the QR, the user submits the 6-digit code shown in their authenticator app. You grab the pending secret from the session, verify the code against it, and only then save the secret to the database.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why verify before saving?&lt;/strong&gt; If you save the secret immediately and the user never completes the scan, their account now has a 2FA secret attached that they can't use. They'll be locked out on next login. Always confirm first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7x70yhwsis65pd956hn7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7x70yhwsis65pd956hn7.png" alt="Registration flow UI mockup" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Login Flow
&lt;/h3&gt;

&lt;p&gt;The login flow with TOTP is just your existing login with one extra step added at the end.&lt;/p&gt;

&lt;p&gt;User submits email and password. You verify credentials as usual. If the user has 2FA enabled, you &lt;strong&gt;don't issue a token yet&lt;/strong&gt;. Instead you return a signal to the frontend saying a second factor is needed.&lt;/p&gt;

&lt;p&gt;The frontend then shows a code input screen. The user opens their authenticator app, types in the 6-digit code, and submits it. Your backend grabs their stored secret, runs &lt;code&gt;authenticator.verify({ token: code, secret })&lt;/code&gt; and if it passes, now you issue the token and let them in.&lt;/p&gt;

&lt;p&gt;The key thing to get right here is the &lt;strong&gt;in-between state&lt;/strong&gt;. After password verification but before TOTP verification, the user is neither fully authenticated nor a stranger. A clean way to handle this is a short-lived &lt;strong&gt;partial auth token&lt;/strong&gt; or a server-side session flag that says "password passed, waiting for TOTP." This prevents anyone from skipping the second step by hitting your token endpoint directly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ If the TOTP code is wrong, don't reveal whether the password was correct or not in your error message. A generic &lt;code&gt;"Invalid credentials"&lt;/code&gt; keeps things safer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faexx0ogs3whjnsy3thza.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faexx0ogs3whjnsy3thza.png" alt="Login flow diagram" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Edge Cases and Things That'll Bite You
&lt;/h3&gt;

&lt;p&gt;Getting the happy path working is the easy part. Here's what trips people up in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Clock Drift&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TOTP codes are time-based, which means if your server's clock and the user's phone clock are even slightly out of sync, valid codes will get rejected.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;otplib&lt;/code&gt; handles this with a tolerance window. By default it accepts the current code plus one code on either side, giving you a 90 second window. You can configure this via &lt;code&gt;authenticator.options = { window: 1 }&lt;/code&gt;. Don't set it too high though. The wider the window, the longer a stolen code stays valid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Encrypt Your Secrets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The TOTP secret stored in your database is the keys to the kingdom. If your database leaks and secrets are in plain text, attackers can generate valid codes for every user indefinitely.&lt;/p&gt;

&lt;p&gt;Always encrypt the secret before storing it. &lt;a href="https://en.wikipedia.org/wiki/Galois/Counter_Mode" rel="noopener noreferrer"&gt;AES-256-GCM&lt;/a&gt; via Node's built-in &lt;code&gt;crypto&lt;/code&gt; module is a solid choice. Decrypt it only at the moment of verification, never log it, never expose it in API responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Replay Attacks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A valid TOTP code stays valid for 30 seconds. If someone intercepts it and uses it within that window, they're in. The fix is to &lt;strong&gt;mark each code as used&lt;/strong&gt; the moment it's verified and reject any reuse within the same window. A simple cache entry with a 30 second TTL does the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Don't Skip Recovery Codes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What happens when a user loses their phone? Without a fallback, they're permanently locked out. Recovery codes are the safety net and we'll cover them properly in the next section.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;None of these are optional in a production app.&lt;/strong&gt; Clock drift and encryption especially. They're quick to implement and the cost of skipping them is high.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Backup and Recovery Codes
&lt;/h3&gt;

&lt;p&gt;Every 2FA implementation needs an answer to "what if I lose my phone?" Without one, your support inbox will fill up fast.&lt;/p&gt;

&lt;p&gt;The idea is simple. When a user enables 2FA, generate a set of &lt;strong&gt;one-time recovery codes&lt;/strong&gt; (typically 8-10 codes). The user saves them somewhere safe. If they ever lose access to their authenticator app, they can use one of these codes to get in instead of the TOTP code. Each code works exactly once and is gone after use.&lt;/p&gt;

&lt;p&gt;Here's a clean way to generate them in TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&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;crypto&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;const&lt;/span&gt; &lt;span class="nx"&gt;generateRecoveryCodes&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "A1B2C3D4E5"&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;Before saving them to the database, &lt;strong&gt;hash each code&lt;/strong&gt; using &lt;code&gt;bcrypt&lt;/code&gt; or Node's &lt;code&gt;crypto&lt;/code&gt; module, just like you'd hash a password. Store only the hashes, never the raw codes. When the user submits a recovery code at login, hash the input and compare it against your stored hashes.&lt;/p&gt;

&lt;p&gt;Once a code is used, delete it from the database immediately. It's a one-time key, treat it that way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Show the codes only once&lt;/strong&gt; — right after the user completes 2FA setup. Make it very clear that these should be saved somewhere safe. After that screen, they're gone even from your end.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What if the user loses their phone and their recovery codes both?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It happens. Here are two approaches you can take depending on your app's security requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Recovery codes as the only self-serve escape hatch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the stricter approach. Recovery codes are the only way in without a phone. Once all codes are used up or the user is fully locked out, they go through a manual account recovery process with identity verification on your end. No self-serve reset. This makes 2FA truly unbypassable with just a password.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Email-based re-enrollment link&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The friendlier approach. Add a "Lost access to your authenticator?" link on the 2FA prompt. This sends a &lt;strong&gt;secure, time-limited link&lt;/strong&gt; to the user's verified email address. Clicking it clears the existing TOTP secret and takes them straight into the 2FA setup flow to re-enroll with their new device.&lt;/p&gt;

&lt;p&gt;This works safely because the email becomes the second factor in the recovery scenario. An attacker with only the password still can't do anything without inbox access. Just make sure to &lt;strong&gt;rate limit these recovery emails&lt;/strong&gt; and flag repeated requests on the same account as suspicious activity.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Most consumer apps go with Option 2 for the better user experience. If you're building something that handles sensitive data, Option 1 or a combination of both gives you stronger guarantees.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;Here's what you've built. Your server generates a secret, shares it via QR code, the user's authenticator app takes it from there, and every login now requires a time-based code that only the user's device can produce. Clock drift is handled, secrets are encrypted, replay attacks are blocked, and recovery codes ensure nobody gets permanently locked out.&lt;/p&gt;

&lt;p&gt;That's a production-grade TOTP setup.&lt;/p&gt;

&lt;p&gt;What we haven't covered: &lt;a href="https://webauthn.guide/" rel="noopener noreferrer"&gt;WebAuthn&lt;/a&gt; and hardware security keys like &lt;a href="https://www.yubico.com/" rel="noopener noreferrer"&gt;YubiKey&lt;/a&gt; take this even further by eliminating shared secrets entirely. If you're building something that needs the highest level of security, those are worth exploring next.&lt;/p&gt;

&lt;p&gt;But for the vast majority of apps, what you've built here is more than enough. 2FA isn't a feature only big companies ship. It's a few hours of work and it meaningfully raises the bar for anyone trying to get into your users' accounts.&lt;/p&gt;

&lt;p&gt;Now go ship it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>authjs</category>
    </item>
    <item>
      <title>CSS Is Smarter Than You Think. Stop Babysitting It with JS</title>
      <dc:creator>Divyam Gupta</dc:creator>
      <pubDate>Thu, 05 Feb 2026 18:41:35 +0000</pubDate>
      <link>https://forem.com/divyamdotfoo/css-is-smarter-than-you-think-stop-babysitting-it-with-js-53j2</link>
      <guid>https://forem.com/divyamdotfoo/css-is-smarter-than-you-think-stop-babysitting-it-with-js-53j2</guid>
      <description>&lt;p&gt;You know those tiny &lt;code&gt;data-&lt;/code&gt; things in HTML that everyone ignores? They're actually way more powerful than you think. Keep reading to see what they can really do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: This article consists of pieces and excerpts from an educational conversation with &lt;code&gt;Claude Haiku 4.5&lt;/code&gt;&lt;/p&gt;



&lt;p&gt;CSS has evolved. You now have &lt;code&gt;:has()&lt;/code&gt; selectors that react to your HTML, &lt;code&gt;if()&lt;/code&gt; functions that make decisions, and data attributes that bridge everything together. Instead of JavaScript managing styles, your state lives in HTML and CSS reacts to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  THE OLD WAY: JavaScript Managing Everything
&lt;/h2&gt;

&lt;p&gt;For years, this is how we've done it. You have some state in your app—maybe a form field that's valid or invalid, a button that's loading, a card that's active or inactive. JavaScript holds that state, and whenever it changes, JavaScript updates the DOM by adding or removing classes, or changing inline styles. The browser then reads those changes and updates how things look. It works, but there's a lot of back-and-forth happening between your JavaScript and the browser's rendering engine.&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;// Button loading state&lt;/span&gt;
&lt;span class="nx"&gt;button&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="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Browser reflows and repaints&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Form validation&lt;/span&gt;
&lt;span class="nx"&gt;input&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="s1"&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;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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;// Browser reflows and repaints&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Toggle dark mode&lt;/span&gt;
&lt;span class="nx"&gt;toggleBtn&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="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark-mode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Browser reflows and repaints everything&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Show/hide a modal&lt;/span&gt;
&lt;span class="nx"&gt;openBtn&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="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Browser reflows and repaints&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  Why Should You Care?
&lt;/h2&gt;

&lt;p&gt;Here's the thing: every time your JavaScript toggles a class or changes a style, your browser has to do work. It recalculates which styles apply, figures out new positions and sizes (reflow), and redraws pixels (repaint). This happens thousands of times per second in interactive apps. On a fast laptop, you might not notice. But on a phone? Your app feels sluggish.&lt;/p&gt;

&lt;p&gt;The old approach triggers this expensive cycle repeatedly. The new approach? Your browser's CSS engine handles styling natively, which means your JavaScript thread stays free to do actual work. Less browser overhead, less lag, faster interactions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;JavaScript Managing Styles&lt;/th&gt;
&lt;th&gt;CSS Handling Natively&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reflow (recalculate layout)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Triggers on every class change&lt;/td&gt;
&lt;td&gt;Only when necessary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repaint (redraw pixels)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Follows each reflow&lt;/td&gt;
&lt;td&gt;Batched with CSS engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JavaScript thread&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Blocked by DOM manipulation&lt;/td&gt;
&lt;td&gt;Free for other logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per interaction cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8-15ms (class toggling overhead)&lt;/td&gt;
&lt;td&gt;2-4ms (attribute update only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mobile performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drops to 25-30fps&lt;/td&gt;
&lt;td&gt;Stays at 60fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10 rapid state changes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10 reflow cycles triggered&lt;/td&gt;
&lt;td&gt;1 reflow cycle batched&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you toggle classes, you're asking JavaScript to talk to the DOM, which talks to the rendering engine, which reflows and repaints. When you update a data attribute, the browser's CSS engine takes over—it's optimized for this exact job.&lt;/p&gt;



&lt;h2&gt;
  
  
  THE NEW WAY: Let Data &amp;amp; CSS Do The Work
&lt;/h2&gt;

&lt;p&gt;Here's where it changes. Instead of JavaScript juggling classes, you set a data attribute on your HTML element. That's it. CSS already knows what to do with it—it watches that attribute and applies the right styles automatically. Your JavaScript becomes simpler, your CSS becomes smarter, and your browser does way less work.&lt;/p&gt;



&lt;h3&gt;
  
  
  Example-1: Form Field Validation
&lt;/h3&gt;

&lt;p&gt;Let's say you have a signup form. Users enter their email, and you need to show them if it's valid or invalid. The old way? JavaScript checks the input, adds an error class, changes the border color, shows an error message. All managed by JavaScript. The new way is simpler: you just set a data attribute, and CSS does everything else.&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="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;EmailInput&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;isInvalid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsInvalid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleBlur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&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;[^\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;\.[^\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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setIsInvalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isValid&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;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"input data-[invalid=true]:border-red-500 data-[invalid=false]:border-green-500"&lt;/span&gt;
        &lt;span class="na"&gt;data-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isInvalid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onBlur&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleBlur&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="nt"&gt;span&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;"error-msg data-[invalid=false]:hidden"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Please enter a valid email
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&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;h3&gt;
  
  
  Example-2: Interactive states for buttons
&lt;/h3&gt;

&lt;p&gt;Buttons have multiple states—they can be disabled, pressed, or loading. Managing all of these with conditional classes gets messy fast. With data attributes, each state is explicit and CSS handles everything. Let's look at a real button that can be disabled or pressed.&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="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;Button&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;isPressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsPressed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isDisabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsDisabled&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&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;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn data-[pressed=true]:bg-blue-700 data-[pressed=false]:bg-blue-500 data-[disabled=true]:opacity-50 data-[disabled=true]:cursor-not-allowed"&lt;/span&gt;
      &lt;span class="na"&gt;data-pressed&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPressed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;data-disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isDisabled&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isDisabled&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onMouseDown&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsPressed&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="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onMouseUp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsPressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsDisabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isDisabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isDisabled&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Disabled&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;Click me&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two separate data attributes, two separate concerns. JavaScript only updates state. CSS handles all the visual feedback. No class juggling, no style conflicts.&lt;/p&gt;



&lt;h3&gt;
  
  
  Example-3: Conditional Styling with :has()
&lt;/h3&gt;

&lt;p&gt;So far we've seen data attributes controlling a single element's styles. But what if you want a parent element to change based on what's inside it? That's where &lt;code&gt;:has()&lt;/code&gt; comes in. It lets CSS check if a child with a specific data attribute exists, and then style the parent accordingly. For example, imagine a card that should remove bottom padding only when it has a footer slot inside 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="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;Card&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hasFooter&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;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;"card has-data-[slot=card-footer]:pb-0"&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;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;"card-header"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Header Content&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;&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;"card-body"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Body Content&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="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hasFooter&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;data-slot&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card-footer"&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;"card-footer"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Footer Content&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="si"&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="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parent card checks if a child with &lt;code&gt;data-slot="card-footer"&lt;/code&gt; exists—if it does, bottom padding is removed. You can also hide elements conditionally: use &lt;code&gt;has-data-[slot=card-footer]:block&lt;/code&gt; to show content only when that slot exists. CSS reacts to what's inside without any JavaScript logic.&lt;/p&gt;



&lt;h3&gt;
  
  
  Example-4: Conditional Logic with CSS if()
&lt;/h3&gt;

&lt;p&gt;Up until now, we've been using data attributes with Tailwind's conditional syntax. But CSS just got a new superpower: the &lt;code&gt;if()&lt;/code&gt; function. It lets you write actual conditional logic directly in CSS. Instead of relying on pre-built Tailwind classes, you can now evaluate conditions and apply styles on the fly. For example, imagine a status badge that changes color, text, and icon based on an order status—all decided by a single &lt;code&gt;if()&lt;/code&gt; statement in 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="nc"&gt;.badge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data-status&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;custom-ident&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shipped&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="m"&gt;#10b981&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="err"&gt;style(&lt;/span&gt;&lt;span class="py"&gt;--status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="m"&gt;#f59e0b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ef4444&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;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shipped&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="m"&gt;#d1fae5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="err"&gt;style(&lt;/span&gt;&lt;span class="py"&gt;--status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="m"&gt;#fef3c7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fee2e2&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;status&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;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;"badge"&lt;/span&gt;
      &lt;span class="na"&gt;data-status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&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;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shipped&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✓ Shipped&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;⏳ Pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✗ Failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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;CSS &lt;code&gt;if()&lt;/code&gt; evaluates the status value and applies colors conditionally—no JavaScript switch statements or ternary operators needed. The browser's CSS engine handles all the logic.&lt;/p&gt;



&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;These examples are just the start. Once you think in data attributes and CSS conditionals, you'll spot opportunities everywhere—product filters, cart states, navigation menus, themes, wizards, notifications. The pattern stays the same: store state in HTML, let CSS react, keep JavaScript simple.&lt;/p&gt;

&lt;p&gt;Pick one interactive component and try moving its styling from JavaScript to CSS. You'll feel the difference.&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>browser</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Simplest markdown component for your AI apps</title>
      <dc:creator>Divyam Gupta</dc:creator>
      <pubDate>Wed, 01 Jan 2025 17:07:42 +0000</pubDate>
      <link>https://forem.com/divyamdotfoo/simplest-markdown-component-for-your-ai-apps-28f2</link>
      <guid>https://forem.com/divyamdotfoo/simplest-markdown-component-for-your-ai-apps-28f2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3khrn34o385mc9yqavz3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3khrn34o385mc9yqavz3.png" alt=" " width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>openai</category>
      <category>vectordatabase</category>
    </item>
  </channel>
</rss>
