Senior 6 min · March 28, 2026

Tailwind CSS — Specificity Conflict Breaks Mobile Layout

Mobile checkout card lost padding from CSS specificity conflict.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Tailwind CSS is a utility-first framework that replaces custom CSS with pre-built single-purpose classes
  • Core components: spacing scale (p-4 = 1rem padding), color system (bg-blue-500), layout (flex, grid)
  • State variants: hover:, focus:, active:, dark: — all compile to pseudo-class selectors
  • Responsive prefixes: sm:, md:, lg:, xl: — mobile-first, unprefixed applies everywhere
  • Production insight: CLI build purges unused classes, output is 5-15kb regardless of project size
  • Biggest mistake: dynamically building class names with concatenation — scanner never sees the full string
Plain-English First

Imagine building with LEGO instead of sculpting clay. With traditional CSS, you mix and shape raw clay every single time — you name it, mould it, store it somewhere, then hope you can find it again later. Tailwind gives you a pre-made bucket of bricks: tiny, single-purpose pieces that each do one thing (make text red, add padding, centre something). You just snap the right bricks together directly on your HTML. No naming. No storing. No hunting. The component IS the instructions.

I watched a five-person team spend three days hunting a production bug that turned out to be a CSS specificity war — one developer's .card class was silently overriding another's .card in a shared stylesheet nobody fully owned. Three days. For padding. Tailwind makes that entire category of problem structurally impossible. Traditional CSS isn't broken — it's just that the way most teams use it at scale is a slow-motion catastrophe waiting for a deadline to trigger it. Every team eventually hits the same wall: stylesheets that nobody dares delete, class names that mean different things in different files, and a 'quick style fix' that somehow breaks the checkout page on mobile. The root cause is always the same — CSS is global by default, and humans are terrible at managing global mutable state. Tailwind CSS flips the model. Instead of writing styles in a separate file and linking them to your HTML with class names you invented, you apply pre-built single-purpose utility classes directly in your markup. After working through this, you'll be able to build a fully styled, responsive, production-quality UI component from scratch using only Tailwind classes — no custom CSS file required. You'll know exactly how Tailwind generates its output, why your bundle stays small in production, and which patterns will save you from the mistakes that burn teams who adopt Tailwind halfway.

Why Utility-First Exists: The Global CSS Problem That Broke Teams

Before Tailwind, the dominant approach was semantic CSS — you'd write a class like .product-card__title--highlighted (yes, BEM, we see you) and stuff the actual styles in a separate .css file. The idea was clean separation of concerns. The reality was a different story. Stylesheets grew. Nobody deleted old rules because nobody knew if something somewhere still used them. Specificity battles broke out when two developers named things similarly. You'd add a rule for the new feature, and three months later realise it was silently overriding a rule in a completely unrelated component. I've personally seen a 'minor padding tweak' on a .container class wreck the layout on six different pages because that class name was reused in five different contexts across the codebase. The hack everyone reached for was CSS Modules or Styled Components — scoped styles that can't leak. Those work, but they come with a build pipeline, a mental context switch, and a separate file to manage per component. Tailwind's answer is more radical: what if the styles lived right in the HTML, were tiny and single-purpose, and you never had to name anything? Each utility class does exactly one thing — p-4 adds 1rem of padding on all sides, text-red-500 makes text a specific shade of red, flex sets display to flexbox. There's nothing to name, nothing to hunt down, nothing to accidentally override. The style IS the markup.

ProductCardComponent.htmlHTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!-- io.thecodeforge — JavaScript tutorial -->

<!--
  BEFORE: Traditional CSS approach
  You'd have a separate styles.css with:
    .product-card { ... }
    .product-card__image { ... }
    .product-card__title { ... }
  ...and pray nobody else named something .product-card
-->
<div class="product-card">
  <img class="product-card__image" src="/shoe.jpg" alt="Running shoe" />
  <h2 class="product-card__title product-card__title--highlighted">Air Max 2024</h2>
  <p class="product-card__price">$129.99</p>
</div>

<!--
  AFTER: Tailwind utility-first approach
  Every style lives right here. No external file. No naming conflicts.
  Anyone can read this and know exactly what it looks like — zero context switching.
-->
<div class="flex flex-col rounded-xl shadow-md overflow-hidden bg-white max-w-xs">
  <!-- rounded-xl: large border radius on all corners -->
  <!-- shadow-md: medium drop shadow — gives card depth -->
  <!-- overflow-hidden: clips the image to the card's rounded corners -->

  <img
    class="w-full h-48 object-cover"
    <!-- object-cover: scales image to fill the box without distorting it -->
    src="/shoe.jpg"
    alt="Running shoe"
  />

  <div class="p-4">
    <!-- p-4: padding of 1rem (16px) on all four sides -->

    <h2 class="text-xl font-bold text-gray-900 mb-1">
      <!-- text-xl: font-size 1.25rem -- larger than body text -->
      <!-- font-bold: font-weight 700 -->
      <!-- text-gray-900: very dark gray, nearly black — better for readability than pure #000 -->
      <!-- mb-1: margin-bottom 0.25rem — tight spacing before the price -->
      Air Max 2024
    </h2>

    <p class="text-lg font-semibold text-indigo-600">
      <!-- text-indigo-600: brand accent colour — draws the eye to the price -->
      $129.99
    </p>
  </div>
</div>
Output
A white card with rounded corners and a medium shadow. The image fills the top half, clipped cleanly to the card's rounded edges. Below it: bold dark-gray product name, indigo-coloured price. No CSS file created. No class naming decisions made. Styles are completely self-documenting in the markup.
Production Trap: The 'I'll Refactor the CSS Later' Lie
Every team says they'll clean up the CSS later. Later never comes. I've audited codebases with 8,000-line CSS files where removing any rule required a full regression test across the whole app. Tailwind's styles are co-located with the markup that uses them — when you delete the component, the styles die with it. Zero orphaned CSS.
Production Insight
Global CSS naming collisions are a ticking time bomb in any codebase beyond 5 developers.
The most common fix — adding !important — is a short-term bandage that makes the next override even harder.
Tailwind eliminates this at the architectural level: no two elements can accidentally share a class.
Key Takeaway
Utility-first is not about writing inline styles — it's about eliminating the global state problem that makes CSS unmaintainable at scale.
When you delete a Tailwind component, its styles die with it.
No orphaned CSS. Ever.

Setting Up Tailwind: From Zero to First Styled Component in 10 Minutes

Tailwind isn't a CDN drop-in you slap in a <script> tag (well, there is a Play CDN for experimenting, but don't ship that to production — it sends every possible class to the browser, all 3MB of it). The real setup takes about five minutes and gives you a build step that strips every unused class. Your production CSS bundle ends up being 5–15kb. Not a typo. That's the whole stylesheet. The core tool is the Tailwind CLI or its PostCSS plugin. You point it at your HTML/JS files, it scans for class names, and it outputs only the CSS those classes need. Nothing more. Install it, configure which files to scan (the content array in tailwind.config.js — this is the most common setup mistake, more on that later), run the build, and you're done. If you're on a Vite/React/Next.js project, there are official integration guides that wire this up automatically. For a plain HTML project or a beginner learning, the CLI approach below is the clearest path — no framework magic hiding what's happening.

TailwindProjectSetup.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# io.thecodeforge — JavaScript tutorial

# Step 1: Initialise a new project (skip if you already have one)
mkdir storefront-ui && cd storefront-ui
npm init -y

# Step 2: Install Tailwind CSS and its peer dependency
npm install -D tailwindcss

# Step 3: Generate the Tailwind config file
# This creates tailwind.config.js in your project root
npx tailwindcss init

# Step 4: Create the input CSS file that Tailwind reads
# This file is the entry point — Tailwind injects its generated styles here
mkdir -p src/css
cat > src/css/main.css << 'EOF'
/* These three directives are NOT normal CSS comments.
   They are Tailwind's injection points — the CLI replaces them
   with the actual utility classes your project uses. */
@tailwind base;       /* Resets browser inconsistencies (Preflight) */
@tailwind components; /* Slot for any @apply component abstractions */
@tailwind utilities;  /* The actual utility classes — p-4, text-red-500, etc. */
EOF

# Step 5: Open tailwind.config.js and set the content paths.
# THIS IS CRITICAL. If Tailwind can't find your files, it generates
# an empty CSS output and you'll wonder why nothing is styled.
# The content array tells Tailwind which files to scan for class names.
cat > tailwind.config.js << 'EOF'
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    // Scan all HTML files at any depth under src/
    "./src/**/*.html",
    // If you add React/Vue components later, add the path here:
    // "./src/**/*.{js,jsx,ts,tsx,vue}"
  ],
  theme: {
    extend: {}, // Your custom design tokens go here — colours, spacing, fonts
  },
  plugins: [],
};
EOF

# Step 6: Add a build script to package.json
# --input: your CSS entry point
# --output: where the generated CSS lands
# --watch: re-builds whenever you save a file (dev mode)
npm pkg set scripts.build="tailwindcss -i ./src/css/main.css -o ./dist/styles.css"
npm pkg set scripts.dev="tailwindcss -i ./src/css/main.css -o ./dist/styles.css --watch"

# Step 7: Run the dev watcher — it rebuilds on every file save
npm run dev
Output
Rebuilt in 123ms
dist/styles.css created — 3.2kb (only the CSS your project actually uses)
Watching for changes...
Never Do This: Shipping the Play CDN to Production
The Tailwind Play CDN (<script src="https://cdn.tailwindcss.com">) downloads every possible utility class at runtime — roughly 3MB of CSS, parsed in the browser on every page load. It exists for CodePen demos and learning. The moment you push it to a real server, your Lighthouse performance score craters and your users on mobile data connections feel it. Use the CLI build. Always.
Production Insight
The content array is the single most common production gotcha in Tailwind projects.
If you add .jsx or .vue files and forget to update the config, Tailwind silently generates an empty stylesheet.
Always verify: run the build with --verbose and check that your file paths are being scanned.
Key Takeaway
Tailwind's production build purges every unused class — your CSS bundle is typically 5–15kb regardless of project size.
But this only works if the content scanner can find your files.
If a class works in dev but not production, check the content array first.

Core Utility Classes: The Vocabulary You'll Use Every Single Day

Tailwind's class names follow a pattern so consistent you can usually guess the class before looking it up. Once you know the naming convention, the docs become a fast reference instead of a tutorial you have to re-read. The pattern is: [property-abbreviation]-[value]. Spacing uses a scale where 1 = 0.25rem (4px), 2 = 0.5rem (8px), 4 = 1rem (16px), 8 = 2rem (32px). So p-4 is padding: 1rem, mt-8 is margin-top: 2rem, gap-2 is gap: 0.5rem. Colours follow [property]-[colour]-[shade] where shades run from 50 (lightest) to 950 (darkest). bg-blue-500 is a mid-range blue background. text-slate-700 is dark grey text. Typography, flexbox, grid, borders, shadows, opacity, transitions — every CSS property you reach for has a corresponding utility. The example below builds a real checkout summary card you'd find in an e-commerce app, using the most commonly reached-for utilities. Read the inline comments — they're the cheat sheet you'll reference in your first week.

CheckoutSummaryCard.htmlHTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<!-- io.thecodeforge — JavaScript tutorial -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Order Summary</title>
  <!-- Link to the CSS Tailwind CLI generated -->
  <link href="/dist/styles.css" rel="stylesheet" />
</head>
<body class="bg-slate-100 min-h-screen flex items-center justify-center p-6">
  <!--
    bg-slate-100 : very light grey background for the page
    min-h-screen : minimum height = full viewport height
    flex          : display: flex
    items-center  : vertically centre the card in the page
    justify-center: horizontally centre the card
    p-6           : 1.5rem padding so card doesn't touch screen edges on mobile
  -->

  <div class="bg-white rounded-2xl shadow-lg w-full max-w-md">
    <!--
      rounded-2xl : border-radius: 1rem — noticeably rounded corners
      shadow-lg   : larger, softer drop shadow — card floats off the page
      max-w-md    : maximum width 28rem — prevents the card stretching too wide on desktop
    -->

    <!-- Card Header -->
    <div class="border-b border-slate-200 px-6 py-4">
      <!--
        border-b          : bottom border only
        border-slate-200  : light grey border colour
        px-6              : horizontal padding (left + right) 1.5rem
        py-4              : vertical padding (top + bottom) 1rem
      -->
      <h1 class="text-lg font-semibold text-slate-800">Order Summary</h1>
      <p class="text-sm text-slate-500 mt-0.5">3 items · Estimated delivery: 35 days</p>
      <!--
        text-sm   : font-size: 0.875rem — one step smaller than default
        mt-0.5    : margin-top: 0.125rem — barely any gap, keeps lines visually tight
      -->
    </div>

    <!-- Line Items -->
    <ul class="divide-y divide-slate-100 px-6">
      <!--
        divide-y          : adds a top border to every child EXCEPT the first one
        divide-slate-100  : colour of those divider lines
        This replaces manually adding border-t to each item — very common Tailwind pattern
      -->

      <!-- Single line item — the structure repeats for each product -->
      <li class="flex items-center gap-4 py-4">
        <!--
          flex        : lay out image + text side by side
          items-center: vertically align image and text to the middle
          gap-4       : 1rem space between the image and the text block
        -->
        <img
          class="w-14 h-14 rounded-lg object-cover flex-shrink-0"
          <!--
            w-14 h-14    : 3.5rem × 3.5rem fixed size
            flex-shrink-0: prevents the image from shrinking when text is long
          -->
          src="/images/shoe.jpg"
          alt="Air Max 2024"
        />
        <div class="flex-1 min-w-0">
          <!--
            flex-1  : this div grows to fill remaining horizontal space
            min-w-0 : critical — without this, long text can overflow flex containers
          -->
          <p class="text-sm font-medium text-slate-800 truncate">Air Max 2024Wolf Grey</p>
          <!-- truncate: adds text-overflow: ellipsis so long names don't break layout -->
          <p class="text-xs text-slate-400 mt-0.5">Size 10 · Qty 1</p>
        </div>
        <span class="text-sm font-semibold text-slate-700 whitespace-nowrap">$129.99</span>
        <!-- whitespace-nowrap: prevents the price from wrapping to two lines -->
      </li>

      <li class="flex items-center gap-4 py-4">
        <img class="w-14 h-14 rounded-lg object-cover flex-shrink-0" src="/images/tshirt.jpg" alt="Tech Tee" />
        <div class="flex-1 min-w-0">
          <p class="text-sm font-medium text-slate-800 truncate">Dri-Fit Training Tee</p>
          <p class="text-xs text-slate-400 mt-0.5">Medium · Qty 2</p>
        </div>
        <span class="text-sm font-semibold text-slate-700 whitespace-nowrap">$49.98</span>
      </li>
    </ul>

    <!-- Totals -->
    <div class="px-6 py-4 bg-slate-50 rounded-b-2xl space-y-2">
      <!--
        bg-slate-50    : slightly off-white background differentiates the totals section
        rounded-b-2xl  : only round the BOTTOM corners — matches the card's outer radius
        space-y-2      : adds 0.5rem vertical gap between every direct child — replaces manual margins
      -->
      <div class="flex justify-between text-sm text-slate-600">
        <span>Subtotal</span>
        <span>$179.97</span>
      </div>
      <div class="flex justify-between text-sm text-slate-600">
        <span>Shipping</span>
        <span class="text-green-600 font-medium">Free</span>
        <!-- text-green-600: positive/success colour — draws eye to the free shipping benefit -->
      </div>
      <div class="flex justify-between text-base font-bold text-slate-900 pt-2 border-t border-slate-200">
        <!--
          pt-2          : padding-top 0.5rem — visual breathing room above the total line
          border-t      : top border separates the grand total from the line items
        -->
        <span>Total</span>
        <span>$179.97</span>
      </div>
    </div>

    <!-- CTA Button -->
    <div class="px-6 pb-6 pt-4">
      <button
        class="w-full bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800
               text-white font-semibold text-sm
               py-3 rounded-xl
               transition-colors duration-150
               focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
        <!--
          w-full                : button spans full card width
          hover:bg-indigo-700   : darker shade on mouse hover — the hover: prefix is a state variant
          active:bg-indigo-800  : even darker on click — tactile feedback
          transition-colors     : animate only colour changes (not all properties)
          duration-150          : 150ms transition — snappy but not instant
          focus:ring-2          : visible keyboard focus ring — accessibility non-negotiable
          focus:ring-offset-2   : 2px gap between button edge and the focus ring
        -->
      >
        Proceed to Payment
      </button>
    </div>
  </div>
</body>
</html>
Output
A centred white card on a light slate background. Card header: 'Order Summary' in dark semibold text, subtitle in muted grey. Two product line items with thumbnail images, product names (truncated with ellipsis if too long), and prices. Totals section with a subtle off-white background: Subtotal, Free shipping in green, bold Total. Full-width indigo button at the bottom. Button darkens on hover, darkens further on click, shows a visible focus ring on keyboard navigation. No custom CSS file was written.
Senior Shortcut: space-y-* and divide-y-* Replace 90% of Margin Hacks
Stop adding mt-4 to every child except the first one manually. Use space-y-4 on the parent — it uses the CSS lobotomised owl selector ( + ) to add top margin only between siblings. Same idea: divide-y adds borders between list items without any JavaScript or :last-child gymnastics. These two utilities alone will clean up a surprising amount of your spacing code.
Production Insight
Missing min-w-0 on flex children is the most common layout bug in Tailwind — long text overflows and breaks the horizontal alignment.
The CSS default min-width: auto for flex items prevents them from shrinking below their content size.
Always add min-w-0 to any flex child that contains text that might be longer than expected.
Key Takeaway
Learn the spacing scale: 1=4px, 2=8px, 4=16px, 8=32px — it never changes.
Learn the colour shades: 50=lightest, 500=mid, 950=darkest — consistent across all colours.
Learn the state variants: hover:, focus:, active:, and combine them with breakpoints: md:hover:bg-blue-600.

Responsive Design and State Variants: One Class, Every Screen Size

Responsive CSS used to mean a separate @media query block at the bottom of your stylesheet, referencing class names you hoped you still remembered. Tailwind collapses that entire pattern into a prefix. Every utility class can be prefixed with a breakpoint: sm:, md:, lg:, xl:, 2xl:. Tailwind is mobile-first — unprefixed classes apply at all screen sizes, and prefixed classes kick in at that breakpoint and above. So text-sm md:text-base lg:text-lg means small text on mobile, base size from 768px up, larger from 1024px up. The same prefix pattern handles interactive states. hover: applies on mouse hover. focus: applies on keyboard focus. active: on click. disabled: when the element has the disabled attribute. dark: when the user's OS is in dark mode. These aren't JavaScript — they compile to CSS pseudo-class selectors. hover:bg-indigo-700 compiles to .hover\:bg-indigo-700:hover { background-color: ... }. You can combine them: md:hover:bg-indigo-700 applies the hover style only on medium screens and above. This is where Tailwind's model pays off the most — you see the full responsive + state behaviour of a component in one place, in one file, without context-switching.

ResponsiveNavigationBar.htmlHTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<!-- io.thecodeforge — JavaScript tutorial -->
<!--
  Real-world scenario: e-commerce site nav.
  Mobile: stacked, full-width with large tap targets.
  Desktop: horizontal layout with hover effects.
  Dark mode: inverted colours with no extra CSS files.
-->
<nav class="bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700">
  <!--
    dark:bg-slate-900    : when OS/browser is in dark mode, swap background to near-black
    dark:border-slate-700: darker border in dark mode — slate-200 would be invisible on dark bg
  -->

  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
    <!--
      max-w-7xl       : caps content width at 80rem — prevents stretching on ultrawide monitors
      mx-auto         : centres the content block horizontally
      px-4            : 1rem horizontal padding on mobile
      sm:px-6         : 1.5rem from 640px up — more breathing room on larger phones/tablets
      lg:px-8         : 2rem from 1024px up — desktop gets generous side padding
    -->

    <div class="flex items-center justify-between h-16">
      <!-- h-16: fixed nav height of 4rem (64px) — standard nav height -->

      <!-- Logo -->
      <a
        href="/"
        class="text-xl font-bold text-indigo-600 dark:text-indigo-400 flex-shrink-0"
        <!--
          dark:text-indigo-400 : lighter indigo in dark mode — indigo-600 is too dark against dark bg
          flex-shrink-0        : logo never compresses when nav items crowd it
        -->
      >
        StoreFront
      </a>

      <!-- Desktop navigation links — hidden on mobile, shown from md breakpoint up -->
      <div class="hidden md:flex md:items-center md:gap-8">
        <!--
          hidden   : display:none on mobile — these links move into a hamburger menu on small screens
          md:flex  : from 768px, switch to flexbox — links appear in a row
          md:gap-8 : 2rem gap between nav links on desktop
        -->

        <a
          href="/products"
          class="text-sm font-medium text-slate-600 hover:text-indigo-600
                 dark:text-slate-300 dark:hover:text-indigo-400
                 transition-colors duration-150
                 relative after:absolute after:bottom-0 after:left-0
                 after:w-0 after:h-0.5 after:bg-indigo-600
                 hover:after:w-full after:transition-all after:duration-200"
          <!--
            The 'after:' prefix targets the CSS ::after pseudo-element.
            This creates an animated underline on hover:
            - after:w-0          : underline starts at zero width
            - hover:after:w-full : expands to full width on hover
            - after:transition-all: the width change animates
            No JavaScript. No extra HTML elements. Pure CSS via Tailwind prefixes.
          -->
        >
          Products
        </a>

        <a
          href="/deals"
          class="text-sm font-medium text-slate-600 hover:text-indigo-600
                 dark:text-slate-300 dark:hover:text-indigo-400
                 transition-colors duration-150"
        >
          Deals
        </a>

        <a
          href="/about"
          class="text-sm font-medium text-slate-600 hover:text-indigo-600
                 dark:text-slate-300 dark:hover:text-indigo-400
                 transition-colors duration-150"
        >
          About
        </a>
      </div>

      <!-- CTA + Cart — always visible but changes size on mobile vs desktop -->
      <div class="flex items-center gap-3">
        <a
          href="/cart"
          class="relative p-2 text-slate-600 hover:text-indigo-600
                 dark:text-slate-300 dark:hover:text-indigo-400"
          aria-label="Shopping cart"
        >
          <!-- Cart icon SVG -->
          <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                  d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-1.5 6M17 13l1.5 6M9 19a1 1 0 100 2 1 1 0 000-2zm8 0a1 1 0 100 2 1 1 0 000-2z" />
          </svg>
          <!-- Cart item count badge -->
          <span
            class="absolute -top-1 -right-1
                   w-4 h-4
                   bg-indigo-600 text-white text-xs font-bold
                   rounded-full
                   flex items-center justify-center"
            <!--
              absolute -top-1 -right-1 : positions the badge in the top-right corner of the icon
              -top-1 is equivalent to top: -0.25rem — pulls it slightly outside the icon boundary
            -->
          >
            3
          </span>
        </a>

        <a
          href="/account"
          class="hidden sm:inline-flex items-center gap-2
                 bg-indigo-600 hover:bg-indigo-700
                 text-white text-sm font-medium
                 px-4 py-2 rounded-lg
                 transition-colors duration-150"
          <!--
            hidden sm:inline-flex : invisible on very small phones, appears from 640px up
            This prevents the nav from becoming too cramped on tiny screens
          -->
        >
          My Account
        </a>
      </div>
    </div>
  </div>
</nav>
Output
Mobile (< 640px): Nav bar with logo left, cart icon with badge right. 'My Account' button hidden. No nav links visible (would be behind hamburger in a real implementation).
Small (640px+): 'My Account' button appears next to cart icon.
Medium (768px+): Products, Deals, About nav links appear in a horizontal row. Links have animated underline on hover.
Dark mode active: Background switches to near-black slate, text lightens, borders darken. All without a single line of JavaScript or a media query written by hand.
Interview Gold: Tailwind is Mobile-First — Unprefixed Means 'All Sizes'
Interviewers catch people on this constantly. hidden md:block does NOT mean 'hidden on desktop, visible on mobile.' It means 'hidden at all sizes, then display:block from 768px upward.' To show something only on mobile and hide it on desktop, you write block md:hidden. Get this backwards and your responsive layout is invisibly broken — I've seen this ship to production more than once.
Production Insight
The 'hidden md:block' inversion is the #1 responsive bug in Tailwind codebases.
It happens because engineers think of CSS as 'do this on mobile, do that on desktop' instead of the mobile-first cascade.
Rule: write the mobile layout first with unprefixed classes, then add prefixes to override for larger screens.
Key Takeaway
Unprefixed utility applies at ALL screen sizes.
Prefix (sm:, md:, lg:) applies AT THAT BREAKPOINT AND ABOVE.
If you want a class only on mobile, write it unprefixed and override it with a prefix: 'text-center md:text-left'.

Customizing Tailwind: Design Tokens, Themes, and the @apply Trap

Tailwind's default theme is a solid starting point — it's been carefully tuned with a balanced colour palette and spacing scale. But every real project needs custom colours, fonts, or spacing. That's where the theme.extend object in tailwind.config.js comes in. You add your brand's primary colour under colors, your custom font family under fontFamily, and your own spacing values. Tailwind merges them with the defaults. Never override theme.colors entirely unless you want to lose all default colours — always use extend. There's also the @apply directive that lets you bundle multiple utilities into a single class. It sounds great: 'I'll just create a .btn-primary class so I don't have to repeat 8 utilities on every button.' And that is exactly how you recreate the global CSS problem you were trying to escape. Now you have a global class name that other developers might override or misuse, and you've lost the readability that comes from seeing all styles in the markup. Use @apply sparingly — only for genuinely repeated patterns like base typography resets or when you need to style third-party content that you can't control. For your own components, keep the utilities in the markup.

tailwind.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// io.thecodeforge — JavaScript tutorial

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{html,jsx,tsx}'],
  theme: {
    extend: {
      // Add your brand's primary colour palette
      colors: {
        brand: {
          50: '#eff6ff',
          100: '#dbeafe',
          200: '#bfdbfe',
          300: '#93c5fd',
          400: '#60a5fa',
          500: '#3b82f6',  // Primary blue
          600: '#2563eb',
          700: '#1d4ed8',
          800: '#1e40af',
          900: '#1e3a8a',
        },
      },
      // Custom font
      fontFamily: {
        heading: ['Inter', 'sans-serif'],
      },
      // Extra spacing value
      spacing: {
        '18': '4.5rem',
      },
    },
  },
  plugins: [],
};

// Then in your HTML: <div class="bg-brand-500 text-white p-18">
// Brand-500 is your primary blue, p-18 gives 4.5rem padding.
Output
A config file that adds a custom 'brand' colour palette with 10 shades, a 'heading' font family, and an extra spacing value of 4.5rem. All default Tailwind utilities remain available. No classes are duplicated.
The @apply Trap: Don't Recreate the CSS Problem You Left
I've seen teams create 300-line components.css files full of @apply directives. They ended up with the exact same problems: global class names, specificity battles, orphaned styles. @apply is useful for base resets and third-party content, but for your own UI components, keep the utilities in the markup. Extract to a component (React/Vue) instead.
Production Insight
Overusing @apply is the fastest way to undo all of Tailwind's benefits.
Once you abstract utilities into semantic class names, you've lost co-location and gained a new global namespace.
The rule: if you wouldn't write it as a custom CSS class, don't @apply it.
Key Takeaway
Customize your theme using extend — never replace the default colour palette.
Prefer component extraction (React/Vue) over @apply for reusable patterns.
@apply is for base resets and third-party content only.

Dark Mode and Theming: One Prefix, Zero Effort

Dark mode used to mean duplicating your entire stylesheet under @media (prefers-color-scheme: dark). With Tailwind, it's a single character prefix: dark:. Add darkMode: 'media' to your config (for automatic OS-based dark mode) or darkMode: 'class' (for manual toggle), then prefix any class with dark: to apply it in dark mode. The example below shows a full page layout that adapts to dark mode with only utility classes — no JavaScript, no media queries, no separate CSS file. The approach scales to any component. The key is to think in pairs: for every class that sets a light-mode colour, background, or border, add a corresponding dark: class with the appropriate dark-mode value.

DarkLayoutExample.htmlHTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- io.thecodeforge — JavaScript tutorial -->
<div class="min-h-screen bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100">
  <!--
    bg-white → dark:bg-slate-900    : white background switches to near-black
    text-slate-900 → dark:text-slate-100 : dark text switches to light
  -->

  <header class="border-b border-slate-200 dark:border-slate-700 p-4">
    <h1 class="text-2xl font-bold">Dashboard</h1>
  </header>

  <main class="p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
    <div class="bg-white dark:bg-slate-800 rounded-xl shadow p-4">
      <h2 class="font-semibold">Revenue</h2>
      <p class="text-3xl font-bold text-green-600 dark:text-green-400">$12,340</p>
      <!-- green-600 too dark on dark bg? use green-400 which is lighter -->
      <p class="text-sm text-slate-500 dark:text-slate-400">↑ 8% from last month</p>
    </div>

    <div class="bg-white dark:bg-slate-800 rounded-xl shadow p-4">
      <h2 class="font-semibold">Users</h2>
      <p class="text-3xl font-bold text-indigo-600 dark:text-indigo-400">1,234</p>
      <p class="text-sm text-slate-500 dark:text-slate-400">Active today</p>
    </div>

    <div class="bg-white dark:bg-slate-800 rounded-xl shadow p-4">
      <h2 class="font-semibold">Orders</h2>
      <p class="text-3xl font-bold text-amber-600 dark:text-amber-400">56</p>
      <p class="text-sm text-slate-500 dark:text-slate-400">Pending</p>
    </div>
  </main>
</div>
Output
Light mode: white background, dark text, green/indigo/amber accents on cards.
Dark mode: near-black background, light text, lighter versions of accent colours. All card backgrounds switch to dark slate. Borders become darker. No flicker, no JavaScript, no extra stylesheet.
Dark Mode Colour Rule: Never Use the Same Shade in Light and Dark
A colour that looks great on white (green-600) becomes nearly invisible on dark backgrounds. Always pick a lighter shade for dark mode (green-400). The same logic applies to backgrounds: bg-white becomes dark:bg-slate-800, not dark:bg-black. Pure black (#000) causes eye strain — use slate-900 instead.
Production Insight
Dark mode classes add no runtime overhead — they compile to CSS media queries at build time.
But if you use 'class' strategy and forget to toggle the dark class on <html>, all dark styles are invisible.
Production check: manually toggle the class in DevTools to verify every component's dark mode is implemented.
Key Takeaway
Add darkMode: 'media' to config — OS-based dark mode works automatically.
For every light-mode colour class, add a corresponding dark: class with a lighter shade.
Test dark mode in DevTools by toggling the dark class on <html> if using 'class' strategy.
● Production incidentPOST-MORTEMseverity: high

The Specificity War That Cost a Sprint

Symptom
The checkout summary card displayed with no padding on mobile devices. Desktop looked fine. The padding was defined in a stylesheet but appeared to be overridden.
Assumption
The team assumed a media query was not applying, or that a CSS reset was stripping padding. They searched for missing @media blocks and !important declarations for two days.
Root cause
Two different stylesheets both defined a .card class. One had padding: 1rem, the other had padding: 0 on a specific media query due to a third-party widget integration. CSS specificity rules made the zero-padding rule win because it was more specific (had a parent selector) — even though it was intended only for the widget's small card.
Fix
Renamed one of the classes to .checkout-card. The real fix was to never rely on globally-scoped class names that multiple teams can accidentally override. The team migrated to Tailwind, where each element's styles are explicit in the markup and no naming collision is possible.
Key lesson
  • Global CSS class names are a shared mutable state that will eventually conflict.
  • Tailwind eliminates this by making styles co-located with markup — no two elements can accidentally share a class unless you explicitly use the same utility classes.
  • If you can't migrate fully, use CSS Modules or a naming convention like BEM with unique component-scoped names.
Production debug guideSymptom → Action guide for the most common Tailwind issues that make it past local development.4 entries
Symptom · 01
A class works in development but not in the production build
Fix
Check if the class is dynamically constructed (e.g., 'bg-' + colour + '-500'). Tailwind's content scanner needs complete class name strings. Use a lookup object mapping status to full class names.
Symptom · 02
All Tailwind styles are missing in production — everything is unstyled
Fix
Verify the content array in tailwind.config.js includes the correct file paths for your templates/components. If the array misses new file types (e.g., .jsx or .vue), Tailwind generates no CSS.
Symptom · 03
Responsive classes not applying on certain breakpoints
Fix
Confirm you're using mobile-first syntax: unprefixed classes apply at all sizes, and breakpoint prefixes (sm:, md:) apply FROM that breakpoint upward. hidden md:block hides on all sizes up to 768px, then shows. Reverse logic: block md:hidden for mobile-only.
Symptom · 04
Dark mode styles not activating
Fix
Check darkMode config in tailwind.config.js. If set to 'media', dark classes activate based on OS preference. If set to 'class', you need to toggle a dark class on <html>. Verify the dark prefix is applied to the correct elements.
★ Tailwind CSS Quick Debug Cheat SheetThree production scenarios and the exact commands and config changes to diagnose and fix them.
Class not in production CSS bundle
Immediate action
Check if the class name appears as a complete string in your source files. Search for partial concatenation.
Commands
grep -r 'text-' + src/
npx tailwindcss -i ./src/css/main.css -o ./dist/styles.css --content ./src/**/*.{html,jsx}
Fix now
Replace dynamic concatenation with a static object map: const colourMap = { error: 'text-red-500' }; className={colourMap[status]}
No CSS generated at all — blank stylesheet+
Immediate action
Verify that the output file exists and is non-empty. Run the build command with verbose output.
Commands
ls -la dist/styles.css && wc -c dist/styles.css
npx tailwindcss -i ./src/css/main.css -o ./dist/styles.css --verbose
Fix now
Add missing file extensions to the content array in tailwind.config.js: content: ['./src/*/.{html,jsx,tsx,vue}']
Responsive class not working at expected breakpoint+
Immediate action
Inspect the element in browser DevTools. Check if the responsive prefix class is present in the styles panel and if it's being overridden by an unprefixed class.
Commands
In Chrome DevTools, search for 'lg:' in the styles panel to see if that rule is defined.
Add the `important: true` config option to tailwind.config.js only as a last resort (adds !important to all utilities).
Fix now
Ensure unprefixed utility does not conflict: the unprefixed version always applies at all sizes. To override on larger screens, use the same property with a breakpoint prefix: text-sm md:text-base.
Tailwind vs Traditional CSS (BEM)
AspectTraditional CSS / BEMTailwind Utility-First
Style locationSeparate .css file(s) — context switching requiredCo-located in HTML markup — everything in one place
Naming collisionsHigh risk — global scope, BEM mitigates but doesn't eliminateZero — no class names invented by the developer
Dead CSS accumulationConstant — nobody deletes old rules safelyImpossible — unused classes are purged at build time
Responsive stylesSeparate @media blocks in CSS filesInline breakpoint prefixes: sm:, md:, lg: in markup
Dark modeCustom class toggling or separate stylesheetdark: prefix — one class, automatic OS detection
Production bundle sizeGrows with every feature added, rarely shrinksTypically 5–15kb regardless of project size
Learning curveLow to start, exponentially complex at scaleSteeper initial week, then dramatically faster at scale
Component reuseCopy CSS class names + keep stylesheet in syncExtract to component (React/Vue) or use @apply
Team consistencyDependent on discipline and code reviewEnforced by the design token system (spacing scale, colours)
IDE supportBasic autocomplete in most editorsTailwind IntelliSense extension gives full autocomplete + docs

Key takeaways

1
Utility-first is not about writing inline styles
it's about eliminating the global state problem that makes CSS unmaintainable at scale. When you delete a Tailwind component, its styles die with it. No orphaned CSS. Ever.
2
The content array in tailwind.config.js is the most common production gotcha. If Tailwind can't find your files, it generates empty CSS and everything looks broken. When a class mysteriously doesn't work in production but works locally, check this first.
3
Reach for Tailwind when your team is building a design-system-level UI with a consistent spacing and colour scale. Reach for CSS Modules when you're styling a third-party widget embed or a highly bespoke animation that genuinely needs to live outside the utility system.
4
Never build Tailwind class names with string concatenation at runtime. 'bg-' + colour produces no CSS
the class fragment never existed as a complete string during the build scan. Always store and reference full class names.
5
Responsive classes are mobile-first
unprefixed = all sizes, prefixed = from that breakpoint up. hidden md:block hides on mobile and shows on desktop — get this backwards and your layout breaks silently.

Common mistakes to avoid

6 patterns
×

Not adding new file types to the `content` array after adding React/Vue/Svelte components

Symptom
Classes used in .jsx or .vue files produce no CSS in the output — elements appear completely unstyled in production.
Fix
Add './src/*/.{js,jsx,ts,tsx,vue,svelte}' to the content array in tailwind.config.js and restart the build watcher.
×

Dynamically constructing class names with string concatenation (e.g., `'text-' + colour + '-500'`)

Symptom
Those classes are never generated in the CSS bundle because Tailwind's content scanner looks for complete class name strings, not fragments.
Fix
Always reference full class names in code: map colour to 'text-red-500' or 'text-green-500' — never build them at runtime with concatenation.
×

Using the Tailwind Play CDN script tag in a production deployment

Symptom
Lighthouse performance score drops dramatically, CSS payload is 2–3MB, users on slow connections see a flash of unstyled content.
Fix
Remove the CDN script, install tailwindcss via npm, run the CLI build which outputs only the CSS classes your project actually uses.
×

Overusing `@apply` to recreate semantic CSS classes ('I'll just wrap all these utilities into .btn-primary')

Symptom
You've recreated the exact global CSS problem Tailwind was designed to escape — now you have both a utility layer and a component layer fighting each other.
Fix
Use @apply only for genuinely repeated patterns (e.g., a base typography reset), and prefer extracting to a UI component in your framework instead.
×

Forgetting `min-w-0` on flex children that contain text

Symptom
Long product names, email addresses, or URLs overflow their flex container and break the layout horizontally.
Fix
Add min-w-0 to the flex child containing the text; flexbox children have min-width: auto by default which prevents shrinking below content size, min-w-0 overrides this.
×

Assuming `hidden md:block` shows on mobile and hides on desktop (the responsive inverted logic)

Symptom
An element that should be visible only on mobile is hidden everywhere, or visible everywhere.
Fix
Remember: unprefixed = all sizes, prefixed = from that breakpoint up. Mobile-only: block md:hidden. Desktop-only: hidden md:block.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Tailwind's content scanner purges unused CSS at build time. How does it ...
Q02SENIOR
When would you choose to keep traditional CSS Modules or Styled Componen...
Q03SENIOR
A colleague complains that every Tailwind component in the codebase has ...
Q04SENIOR
Explain exactly what Tailwind's Preflight does and why removing it can c...
Q05SENIOR
How do you handle multiple design systems or brand theming in a single T...
Q01 of 05SENIOR

Tailwind's content scanner purges unused CSS at build time. How does it handle classes that are generated dynamically at runtime — for example, a status badge whose colour class is determined by an API response value? What breaks and what's the correct pattern to ensure those classes always exist in the bundle?

ANSWER
The scanner reads static source files and looks for complete class name strings. If you write 'bg-' + status + '-500', the scanner never sees a full string like bg-red-500 — it sees fragments 'bg-', status, '-500'. The result: those classes are missing from the production CSS bundle. The fix is to use a lookup object that maps status values to complete class names: const badgeColour = { error: 'bg-red-500', success: 'bg-green-500', warning: 'bg-yellow-500' } and then reference badgeColour[status]. The scanner finds the full strings 'bg-red-500' etc. in the source file, and they get included in the output. You can also use the safelist option in tailwind.config.js to force-include specific classes, but that's a brute-force approach — the lookup pattern is cleaner.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Does Tailwind CSS produce bloated HTML with too many classes on every element?
02
What's the difference between Tailwind CSS and Bootstrap?
03
How do I make Tailwind work with dark mode?
04
Why do some Tailwind classes work in development but disappear in the production build?
05
Should I use Tailwind's JIT mode or the classic build?
🔥

That's HTML & CSS. Mark it forged?

6 min read · try the examples if you haven't

Previous
Web Accessibility — WCAG Basics
12 / 16 · HTML & CSS
Next
HTML iframe: Embedding External Content Explained