Giles BradshawThe web's canonical separation — HTML, CSS, JavaScript — separates the framework's concerns, not...
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.
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.
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 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'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.
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:
The HTML/CSS/JS separation has real costs that have been absorbed so thoroughly they are invisible:
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.
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.