Tailwind CSS — Specificity Conflict Breaks Mobile Layout
Mobile checkout card lost padding from CSS specificity conflict.
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
- 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
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 Tailwind CSS Breaks When You Least Expect It
Tailwind CSS is a utility-first CSS framework that provides low-level, composable classes to build custom designs without writing custom CSS. Its core mechanic is generating thousands of single-purpose utility classes (like p-4, text-center, flex) from a configuration file, then purging unused styles in production. This approach eliminates the traditional cascade and specificity wrestling of hand-written CSS — until you mix it with other CSS sources.
In practice, Tailwind's utility classes all have the same specificity (0,1,0 or 0,1,1 for pseudo-classes). This means the last class in the HTML attribute wins, not the most specific selector. This is by design: it makes overrides predictable. But when a third-party component or legacy stylesheet injects a selector with higher specificity (e.g., .card p { margin: 0 }), that rule overrides Tailwind's m-4 regardless of order. The result: your mobile layout collapses because a global reset or library style silently wins.
Use Tailwind when you control the entire CSS stack and can enforce utility-first discipline. It shines in greenfield projects, design systems, and teams that value consistency over custom CSS artistry. Avoid it in projects with heavy legacy CSS, complex third-party widget libraries, or where specificity battles are inevitable — because Tailwind's flat specificity is both its superpower and its Achilles' heel.
.hero-section p { margin: 0 } to style a landing page. Tailwind's m-4 on a <p> inside .hero-section is ignored. Symptom: mobile layout collapses — text runs edge-to-edge, spacing disappears. Rule of thumb: never mix utility classes with selectors that have higher specificity than (0,1,0) unless you use !important or @layer to reorder.@layer or !important sparingly — the real fix is eliminating competing CSS sources.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.
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.
<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.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.
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.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.
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.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.
extend — never replace the default colour palette.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.
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.darkMode: 'media' to config — OS-based dark mode works automatically.dark: class with a lighter shade.dark class on <html> if using 'class' strategy.Tailwind Flexbox: The Layout Engine That Won't Lie To You
Every production layout failure I've debugged traces back to one thing: someone tried to write flexbox manually and got the axis wrong. Tailwind's flex utilities don't fix that — they make the mistake obvious in two characters.
flex gives you display: flex. flex-row is the default horizontal axis. flex-col flips it vertical. The justify- and items- classes map directly to justify-content and align-items. No shorthand gymnastics. No align-self surprises.
The real power? flex-1, flex-shrink-0, flex-grow. These are your spacing contracts in a responsive app. Need a sidebar that stays 250px while content fills the rest? w-[250px] flex-shrink-0 on the sidebar, flex-1 min-w-0 on the content. That min-w-0 is the secret sauce — it overrides flex's default min-width: auto behavior that causes overflow nightmares.
When you see broken layouts in production, 90% of the time it's because someone omitted min-w-0 or overflow-hidden on a flex child. Don't be that someone.
min-w-0 on a flex child that contains dynamic content. Without it, long strings or media queries will push the parent past its flex limits and break your layout.flex-1 with min-w-0 on children that hold variable-width content to prevent overflow collapse.Tailwind Grid: Stop Writing CSS Grid From Scratch, You're Wasting Time
CSS Grid is the most powerful layout tool in modern CSS. Tailwind's grid utilities are the least surprising abstraction of it you'll find — and that's a good thing.
grid gets you display: grid. grid-cols-3 creates three equal columns. gap-4 adds spacing. That's it. No media query hell, no calc() nonsense. For responsive grids, use responsive prefixes: grid-cols-1 md:grid-cols-2 lg:grid-cols-3. One line handles mobile, tablet, desktop.
Where most devs screw up: grid items that need specific placement. col-span-2 makes an item span two columns. row-span-1 is default. If you need a grid item to start at column 3, use col-start-3. These map 1:1 to grid-column-start and grid-column-end without the verbose syntax.
The only trap? Tailwind's grid works with explicit tracks. If you use auto-rows-min or auto-rows-fr, you're telling the browser to size rows implicitly. That's fine for dynamic content, but it means your layout can shift unpredictably. Explicit rows via grid-rows-3 are safer for dashboard layouts.
Grid and flexbox solve different problems. Grid is for two-dimensional layouts (rows AND columns). Flexbox is for one-dimensional flows. Use the right tool or I'll find you.
auto-rows-fr on the grid container when you want all implicit rows to equal height. Perfect for card grids where some cards are taller than others.grid-cols-{n} for columns, col-span-{n} for spanning, and responsive prefixes to break layouts without custom CSS.Getting Started with Tailwind CSS
Tailwind CSS is a utility-first framework that writes your CSS for you—no more context switching between HTML and stylesheets. Start by installing it via npm or yarn into any JavaScript project, then configure your tailwind.config.js to define design tokens like colors and spacing. The real power comes from scanning your source files for class names: Tailwind generates only the CSS you actually use. This eliminates dead code and keeps bundles small. For single-page apps, add Tailwind directly via CDN during prototyping, but always switch to a build step for production. The @tailwind directives in your CSS file inject base, components, and utilities layers in that exact order—don't reorder them. Before writing a single custom class, style an element using utilities like text-center, p-4, and bg-blue-500. This builds muscle memory for the vocabulary. The goal isn't memorizing every class; it's understanding that one class does one thing well. Start with a simple card component—grid layout, rounded corners, shadow—and you'll see why teams drop custom CSS within a day.
content paths in config means Tailwind compiles every utility class—ballooning your CSS to megabytes. Always point to your source files.Q1. Explain the concept of Utility-First in Tailwind CSS?
Utility-first means you build interfaces by composing single-purpose classes instead of writing custom CSS. Each class like flex, mt-4, or text-lg does exactly one job. Why does this exist? Because global CSS breaks teams. A single .card class in a stylesheet can accidentally override .button when specificity piles up. Utility-first kills that problem: no cascade, no inheritance surprises. You style directly in your markup—every property is explicit. If you need a red button with padding, you write <button class="bg-red-500 px-4 py-2">. No digging through style sheets to find where --primary-color was overridden. The trade-off is more class names in HTML, but the payoff is predictable styling that scales across large teams. Tailwind generates these utilities from a configurable design system, so you never repeat values. When the design changes, you update the token in one place—every component using that token updates instantly. This is not "writing CSS in HTML." It's removing the indirection layer that made CSS fragile for decades.
Basics: The Atomic Units of Tailwind CSS
Tailwind CSS is a utility-first framework built on atomic classes—each class applies a single CSS property. The core principle: you compose your UI directly in HTML using small, reusable utility classes instead of writing custom CSS. The two foundational concepts are the box model (margin, padding, border) and typography (font-size, color, alignment). Every utility follows a predictable naming pattern: {property}-{value}. For example, p-4 sets padding to 1rem, text-center aligns text, and bg-blue-500 applies a blue background. Understanding the spacing scale (multiples of 0.25rem) and color system (shades from 50 to 900) is essential. Tailwind uses a mobile-first breakpoint system: sm:, md:, lg:, xl:, 2xl:. Adding a breakpoint prefix applies the utility only at that screen width and above. State variants like hover:, focus:, and active: modify behavior on interaction. The magic is that each class is deterministic—no specificity wars, no cascade surprises.
@apply to extract repeated patterns—it reintroduces the cascade and bloats your bundle. Raw utilities are smaller and faster.Conclusion: Why Tailwind Wins in Production
After building with Tailwind CSS for years across multiple codebases, the payoff is undeniable: no more dead CSS, no more specificity wars, and no more context switching between files. Every utility class is a guaranteed one-to-one mapping to a CSS property—zero ambiguity. The framework forces design consistency through your config’s spacing, color, and typography tokens. Teams ship faster because designers and developers share the same visual vocabulary. The biggest lie about Tailwind is that it makes HTML ugly—in reality, it makes design intent explicit. You can read a component and immediately understand its layout, spacing, and state behavior. The @apply directive is the only trap: it tries to revert to the old paradigm. For production, commit to raw utilities and the purge process will keep your CSS under 10KB. Tailwind isn’t a fad; it’s the final answer to the global CSS problem. Start with a tiny project, stay in the editor, and let the utility-first mindset reshape how you think about styling the web.
@layer utilities unless your design truly repeats 5+ times. Raw utilities keep your bundle minimal and your mental model clean.The Specificity War That Cost a Sprint
- 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.
'bg-' + colour + '-500'). Tailwind's content scanner needs complete class name strings. Use a lookup object mapping status to full class names.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.hidden md:block hides on all sizes up to 768px, then shows. Reverse logic: block md:hidden for mobile-only.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.grep -r 'text-' + src/npx tailwindcss -i ./src/css/main.css -o ./dist/styles.css --content ./src/**/*.{html,jsx}Key takeaways
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.'bg-' + colour produces no CSShidden md:block hides on mobile and shows on desktop — get this backwards and your layout breaks silently.Common mistakes to avoid
6 patternsNot adding new file types to the `content` array after adding React/Vue/Svelte components
'./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'`)
'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
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')
@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
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)
block md:hidden. Desktop-only: hidden md:block.Interview Questions on This Topic
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?
'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.Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.
That's HTML & CSS. Mark it forged?
12 min read · try the examples if you haven't