The web's canonical separation — HTML, CSS, JavaScript — separates the framework's concerns, not yours. Real separation of concerns is domain-specific and cannot be prescribed by a platform.
Be aware - this article shamelessly plugs SX a Framework free Reactive Hypermedia which replaces html, css and js with a single isomorphic host independent evaluated language.
The Orthodoxy
Web development has an article of faith: separate your concerns. Put structure in HTML. Put presentation in CSS. Put behaviour in JavaScript. Three languages, three files, three concerns. This is presented as a universal engineering principle — the web platform's gift to good architecture.
It is nothing of the sort. It is the framework's separation of concerns, not the application's. The web platform needs an HTML parser, a CSS engine, and a JavaScript runtime. These are implementation boundaries internal to the browser. Elevating them to an architectural principle for application developers is like telling a novelist to keep their nouns in one file, verbs in another, and adjectives in a third — because that's how the compiler organises its grammar.
What is a concern?
A concern is a cohesive unit of functionality that can change independently. In a shopping application, concerns might be: the product card, the cart, the checkout flow, the search bar. Each of these has structure, style, and behavior that change together. When you redesign the product card, you change its markup, its CSS, and its click handlers — simultaneously, for the same reason, in response to the same requirement.
The traditional web separation scatters this single concern across three files. The product card's markup is in products.html, tangled with every other page element. Its styles are in styles.css, mixed with hundreds of unrelated rules. Its behavior is in app.js, coupled to every other handler by shared scope. To change the product card, you edit three files, grep for the right selectors, hope nothing else depends on the same class names, and pray.
This is not separation of concerns. It is commingling of concerns, organized by language rather than by meaning.
The framework's concerns are not yours
The browser has good reasons to separate HTML, CSS, and JavaScript. The HTML parser builds a DOM tree. The CSS engine resolves styles and computes layout. The JS runtime manages execution contexts, event loops, and garbage collection. These are distinct subsystems with distinct performance characteristics, security models, and parsing strategies.
But you are not building a browser. You are building an application. Your concerns are: what does a product card look like? What happens when a user clicks 'add to cart'? How does the search filter update the results? These questions cut across markup, style, and behavior. They are not aligned with the browser's internal module boundaries.
When a framework tells you to separate by technology — HTML here, CSS there, JS over there — it is asking you to organize your application around its architecture, not around your problem domain. You are serving the framework's interests. The framework is not serving yours.
React understood the problem
React's most radical insight was not the virtual DOM or one-way data flow. It was the assertion that a component — markup, style, behavior, all co-located — is the right unit of abstraction for UI. JSX was controversial precisely because it violated the orthodoxy. You are putting HTML in your JavaScript! The concerns are not separated!
But the concerns were separated — by component, not by language. A contains everything about product cards. A contains everything about search bars. Changing one component does not require changes to another. That is separation of concerns — real separation, based on what changes together.
CSS-in-JS libraries followed the same logic. If styles belong to a component, they should live with that component. Not in a global stylesheet where any selector can collide with any other. The backlash — "you're mixing concerns!" — betrayed a fundamental confusion between technologies and concerns.
Separation of concerns is domain-specific
Here is the key point: no framework can tell you what your concerns are. Concerns are determined by your domain, your requirements, and your rate of change. A medical records system has different concerns from a social media feed. An e-commerce checkout has different concerns from a real-time dashboard. The boundaries between concerns are discovered through building the application, not prescribed in advance by a platform specification.
A framework that imposes a fixed separation — this file for structure, that file for style — is claiming universal knowledge of every possible application domain. That claim is obviously false. Yet it has shaped twenty-five years of web development tooling, project structures, and hiring practices.
The right question is never "are your HTML, CSS, and JS in separate files?" The right question is: "when a requirement changes, how many files do you touch, and how many of those changes are unrelated to each other?" If you touch three files and all three changes serve the same requirement, your concerns are not separated — they are scattered.
What SX does differently
An SX component is a single expression that contains its structure, its style (as keyword-resolved CSS classes), and its behaviour (event bindings, conditionals, data flow). Nothing is in a separate file unless it genuinely represents a separate concern.
(defcomp ~product-card (&key product on-add)
(div :class "rounded-lg border border-stone-200 p-4 hover:shadow-md transition-shadow"
(img :src (get product "image") :alt (get product "name")
:class "w-full h-48 object-cover rounded")
(h3 :class "mt-2 font-semibold text-stone-800"
(get product "name"))
(p :class "text-stone-500 text-sm"
(get product "description"))
(div :class "mt-3 flex items-center justify-between"
(span :class "text-lg font-bold"
(format-price (get product "price")))
(button :class "px-3 py-1 bg-violet-500 text-white rounded hover:bg-violet-600"
:sx-post (str "/cart/add/" (get product "id"))
:sx-target "#cart-count"
"Add to cart"))))
Structure, style, and behavior are co-located because they represent one concern: the product card. The component can be moved, renamed, reused, or deleted as a unit. Changing its appearance does not require editing a global stylesheet. Changing its click behavior does not require searching through a shared script file.
This is not a rejection of separation of concerns. It is separation of concerns taken seriously — by the domain, not by the framework.
When real separation matters
Genuine separation of concerns still applies, but at the right boundaries:
- Components from each other — a product card should not know about the checkout flow. They interact through props and events, not shared mutable state.
- Data from presentation — the product data comes from a service or API, not from hardcoded markup. The component receives data; it does not fetch or own it.
- Platform from application — SX's boundary spec separates host primitives from application logic. The evaluator does not know about HTTP. Page helpers do not know about the AST.
- Content from chrome — layout components (nav, footer, sidebar) are separate from content components (articles, product listings, forms). They compose, they do not intermingle. These boundaries emerge from the application's actual structure. They happen to cut across HTML, CSS, and JavaScript freely — because those categories were never meaningful to begin with. The cost of the wrong separation
The HTML/CSS/JS separation has real costs that have been absorbed so thoroughly they are invisible:
- Selector coupling — CSS selectors create implicit dependencies between stylesheets and markup. Rename a class in HTML, forget to update the CSS, and styles silently break. No compiler error. No runtime error. Just a broken layout discovered in production.
- Global namespace collision — every CSS rule lives in a global namespace. BEM, SMACSS, CSS Modules, scoped styles — these are all workarounds for a problem that only exists because styles were separated from the things they style.
- Shotgun surgery — a single feature change requires coordinated edits across HTML, CSS, and JS files. Miss one, and the feature is half-implemented. The change has a blast radius proportional to the number of technology layers, not the number of domain concerns.
- Dead code accumulation — CSS rules outlive the markup they were written for. Nobody deletes old styles because nobody can be sure what else depends on them. Stylesheets grow monotonically. Refactoring is archaeology.
Every one of these problems vanishes when style, structure, and behaviour are co-located in a component. Delete the component, and its styles, markup, and handlers are gone. No orphans. No archaeology.
The principle, stated plainly
Separation of concerns is a domain-specific design decision. It cannot be imposed by a framework. The web platform's HTML/CSS/JS split is an implementation detail of the browser, not an architectural principle for applications. Treating it as one has cost the industry decades of unnecessary complexity, tooling, and convention.
Separate the things that change for different reasons. Co-locate the things that change together. That is the entire principle. It says nothing about file extensions.
Top comments (0)