With all that's going on around Tailwind at the moment, I thought that it would be interesting to analyze what IVP says about Tailwind CSS.
The Coupling Critique Misidentifies the Problem
"Tailwind couples appearance to structure." This critique surfaces in every Tailwind discussion. The argument seems intuitive: by placing bg-blue-500 p-4 flex items-center directly in HTML, we're violating the sacred separation of concerns—mixing what something is with how it looks.
But what if this intuition is wrong? What if Tailwind actually improves architectural quality by a measure more rigorous than "feels clean"?
The Independent Variation Principle (IVP) offers exactly such a measure. IVP states: separate elements with different change driver assignments into distinct units; unify elements with the same change driver assignment within a single unit. Quality isn't about file types—it's about change drivers.
Change Drivers as the Unit of Architectural Concern
A change driver represents an independently varying source of change to domain knowledge. Think about where change actually originates: business rules change because stakeholders request new features; UI layouts change because designers iterate; database schemas change because data requirements evolve. These are fundamentally different reasons for modification, and they often involve different people.
Here's where IVP becomes useful: group code by who decides how and why it changes, not by what it is. When a stakeholder says "make the Buy button more prominent," that's a single change driver. Now imagine an architecture where fulfilling that request requires modifying both an HTML file and a CSS file. You've just scattered knowledge about "the Buy button" across two locations. The architecture forces you to synchronize changes manually—a cognitive burden that could have been avoided.
Semantic CSS Architectures Create Transitive Dependencies
Consider BEM (Block Element Modifier) or other "semantic" CSS approaches:
<button class="btn btn--primary btn--large">Buy Now</button>
.btn { /* base styles */ }
.btn--primary { background: blue; color: white; }
.btn--large { padding: 1rem 2rem; }
This looks clean—HTML declares what, CSS declares how. But look at what actually changes:
The button's existence comes from HTML. The button's appearance comes from CSS. And then there's the contract between them: the class name .btn--primary. When the designer says "that button needs more padding," you edit the CSS. Straightforward. But what if you decide to rename the variant or restructure the component? Suddenly you're touching both files. The class name becomes a transitive dependency—HTML depends on it, CSS depends on it. Change one, and you must change the other.
The real problem emerges in deletion. Delete the HTML, and the CSS lingers as orphaned code. Delete the CSS, and the HTML silently breaks. The knowledge of "how this button looks" is split across an artificial boundary, making it impossible to reason about either piece independently.
Tailwind Unifies Change Drivers Within Components
<button class="bg-blue-500 text-white px-8 py-4 rounded-lg">
Buy Now
</button>
The "coupling accusation" sees this as mixing concerns. But ask a different question: when does this code actually change? The visual appearance of this specific button changes when a designer modifies this specific button. All the knowledge required to render this button sits in one place. Delete the component, and the styles vanish—no orphaned CSS. Modify the padding, and no other button in the application is affected.
That's not coupling. That's cohesion. Tailwind unifies elements that share a single change driver: the visual manifestation of this component.
The Cascade as Implicit Shared State
CSS's "C" is precisely what IVP warns against. The cascade creates a form of implicit dependency that violates independent variation. Consider a simple rule like .card { padding: 1rem; background: white; }. This declaration means every element with class card now varies together. Change the rule once, and every card in your application changes—the product card, the modal card you forgot about, the legacy card nobody maintains, the third-party component you styled to match your brand.
This is transitive coupling through shared state. A modification intended for one concern propagates unexpectedly to another. The problem compounds in large codebases where you can't hold the entire cascade in your head.
Tailwind bypasses this entirely. The utility classes p-4 bg-white on one component have zero effect on another. Each component varies independently. This is what IVP actually demands.
Design Tokens Reconcile Global Consistency with Local Variation
"If every component hard-codes its colors, how do you change the brand palette?"
This reveals the real architectural boundary. Tailwind's tailwind.config.js serves as the knowledge partition for design system constants. Rather than hard-coding hex codes, components reference semantic aliases like bg-primary. Change the brand color once in the config, and the change propagates everywhere automatically. Meanwhile, individual components still vary their application of those tokens independently based on feature requirements.
The separation works because different change drivers are at play. Design tokens change when brand or system requirements change. Component styling changes when feature requirements change. The config ensures global consistency without forcing local components to vary together. The utility classes ensure each component can vary independently. Both principles are satisfied—IVP operates correctly at both levels.
Rethinking What HTML Actually Does
Here's where the analysis gets interesting. The traditional separation of concerns doctrine taught us that HTML handles content, CSS handles appearance, and JavaScript handles behavior. This framing makes intuitive sense until you examine where content actually lives in modern web applications. In a typical system, content doesn't live in HTML. It lives in databases, APIs, content management systems. The HTML file isn't the authoritative source of content—it's a rendering template that transforms data for human consumption.
If HTML isn't organizing content, then what is it actually doing? It's presenting data and enabling interaction. HTML is fundamentally a presentation tool. Its job is to take structured data from elsewhere and manifest it in visual and interactive form for human consumption. Once we accept this reality, coupling HTML to styling stops being a violation. They're both expressions of the same concern: the human interface. A <button> with Tailwind classes isn't mixing "content" with "appearance"—it's describing a single unit of user experience.
Semantic HTML Enriches the Human Interface
"But what about <article> and <section>? Isn't semantic HTML for machines?"
No. Semantic HTML is for humans, delivered through browser features. Consider what semantic tags actually accomplish: Reader View functionality strips away navigation and ads to present clean content—for human reading. Screen readers convey document structure to users with visual impairments—for human accessibility. The <time> element allows browsers to parse dates and offer "Add to Calendar" functionality—for human convenience. Form inputs with proper semantic attributes enable browser autofill—for human efficiency.
Semantic HTML is metadata that helps browsers provide enhanced capabilities to human users. It's analogous to ARIA attributes—not a completely separate interface layer, but enrichment delivered through a different channel. Both serve the same consumer: the human using the browser.
A <div> styled with Tailwind and an <article> styled with Tailwind serve the same change driver: the human experience. The semantic tag adds metadata that unlocks browser capabilities; the utility classes define visual appearance. Both are aspects of the presentation layer serving the same user. There's no violation in placing them together—meaning and appearance are co-located knowledge for a unified concern.
Search Engines Audit the Human Interface, Not a Separate One
"Surely Google's crawler is a machine consumer that needs semantic HTML?"
This seems like a counterexample—a machine that consumes the presentation layer. But examine what the bot actually evaluates. Google's crawler measures Core Web Vitals to proxy for perceived load speed and human experience. Mobile-friendliness gets assessed to measure usability on small screens where humans actually browse. Content quality signals get analyzed to predict whether humans will find the content valuable. Accessibility checks ensure the site works for humans with disabilities.
Google's crawler is fundamentally an automated auditor of the human interface. It uses semantic HTML not because the bot needs semantic structure for its own evaluation logic, but because semantic structure correlates with good human experience. The bot isn't a different consumer—it's mimicking human usage patterns to evaluate human-centric quality metrics.
There's ultimately one change driver for the presentation layer: the human user. Google's approach recognizes this. The bot doesn't require a separate interface; it audits the same interface humans use, for the purpose of predicting human satisfaction. Any attempt to serve bots differently—through cloaking or dynamic rendering with alternate content—is penalized precisely because Google prioritizes a single source of truth: the interface as humans experience it.
The Actual Architectural Boundary: Data versus Presentation
If there is a meaningful architectural boundary worth defending, it's not between HTML and CSS. It's between data and presentation.
The Interface Segregation Principle (ISP) states that no client should depend on methods it doesn't use. When applied to web architecture, this becomes clear: system consumers like mobile apps, integrations, and third-party services need raw data and shouldn't be forced to parse HTML to extract information. Human consumers using browsers need a rendered interface and shouldn't have to consume JSON directly. These are genuinely different consumers with fundamentally different needs, and the proper architectural response is to provide different views over the same data:
┌─────────────────────────────────────────────────┐
│ Domain Data │
│ (database, business logic) │
└───────────────────┬─────────────────────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────────────┐
│ JSON API │ │ HTML + Tailwind │
│ (systems) │ │ (humans) │
└───────────────┘ └───────────────────────┘
The JSON API serves as the interface for system-to-system communication. It contains pure domain knowledge without presentational noise. Mobile apps consume it, analytics services consume it, partner integrations consume it. The HTML + Tailwind view, by contrast, serves as the interface for human consumption. It contains the same domain knowledge, but transformed into visual and interactive form. Semantic tags provide metadata that unlocks browser features; utility classes provide visual styling.
Within the presentation layer, further separation between "structure" and "style" only creates artificial boundaries that scatter knowledge across multiple files. The actual boundary worth defending is between what the data means and how humans interact with it.
How Independent Variation Plays Out in Practice
Consider a system where you need to display a product catalog:
The Data (JSON API):
{
"products": [
{ "id": 1, "name": "Widget", "price": 29.99, "inStock": true },
{ "id": 2, "name": "Gadget", "price": 49.99, "inStock": false }
]
}
The Human View (HTML + Tailwind):
<article class="grid gap-6">
<div class="p-4 bg-white rounded-lg shadow">
<h2 class="text-xl font-bold">Widget</h2>
<p class="text-gray-600">$29.99</p>
<span class="text-green-600">In Stock</span>
</div>
<!-- ... -->
</article>
Each layer operates under different change drivers. The data layer changes when business rules change—when new fields are needed or pricing logic evolves. The presentation layer changes when user experience requirements change—when designers want a different layout or new visual treatment. The React/Vue/Svelte component acts as the mapping layer that connects them, pulling data from one interface and projecting it onto the human interface. This mapping is where the two concerns meet, and keeping it small is the architectural goal.
When you want to change how prices display, you modify the presentation layer. When you want to add a new product attribute, you modify the data layer and then decide whether to surface it in the presentation. Each concern varies independently.
Architectural Patterns That Follow from This Analysis
Treating the component as the unit of change means recognizing that a compon
With all that's going on around Tailwind at the moment, I thought that it would be interesting to analyze what IVP says about Tailwind CSS.
ent's HTML, Tailwind classes, and JavaScript logic all vary together for the same fundamental reason: the component's requirements changed. This is the proper granularity for architectural reasoning.
Design system constants belong in tailwind.config.js. Colors, spacing scales, and typography change for brand or system reasons, not component reasons. Centralizing them ensures global consistency while components remain locally independent.
Semantic HTML should be chosen based on what browser capabilities it actually unlocks for users—Reader View, accessibility features, form autofill. The semantic tag is presentation metadata, part of the human interface layer, not a separate concern.
Data and presentation must be strictly segregated. Provide a JSON API for system consumers. Don't force mobile apps to parse HTML or embed business logic in templates. The architectural clarity of this separation is worth the effort.
Dead code elimination becomes trivial with Tailwind. Remove a component, and its styles vanish. In traditional CSS architectures, deletion requires archeological investigation to find and remove orphaned rules. This practical difference reflects deeper architectural alignment.
The visual verbosity of utility classes is actually the manifestation of explicit knowledge. Hidden complexity scattered across distant CSS files isn't cleaner—it's just hidden, deferring the cognitive burden rather than eliminating it.
Conclusion
The "separation of concerns" argument against Tailwind rests on a category error. It treats file types (HTML, CSS) as if they were concerns. IVP reveals what the actual concerns are: change drivers. HTML doesn't organize content in modern applications—it presents data that lives elsewhere. Semantic HTML doesn't serve machines—it's metadata that enables browser features for human users. The cascade isn't an abstraction mechanism—it's transitive coupling that forces independent variations together.
When appearance and structure share a change driver, unifying them isn't coupling. It's cohesion. For UI components, appearance and structure genuinely do share a single change driver: the component's requirements. Tailwind's utility-first approach recognizes this, localizing knowledge about each component's visual manifestation in one place and enabling independent variation across the codebase.
The real architectural boundary is between data (for systems) and presentation (for humans). Within the presentation layer, HTML structure and CSS styling are co-located knowledge serving a single consumer. Tailwind recognizes this reality; traditional CSS architectures pretend otherwise, creating artificial boundaries.
The practical consequences follow naturally: changes stay local rather than propagating unexpectedly, dead code disappears when components are removed, and the cognitive overhead of maintaining synchronized files across multiple locations vanishes. This isn't because Tailwind is convenient. It's because Tailwind aligns code structure with actual causal structure.
The Independent Variation Principle is formally developed in The Independent Variation Principle - A Unifying Meta-Principle for Software Architecture.
Top comments (0)