CSS in 2025: Let's write html inlined styles as if it was 2005 and separation of formatting/representation was never invented. I talk of tailwind, of course.
CSS in 2025: Let's write html inlined styles as if it was 2005 and separation of formatting/representation was never invented. I talk of tailwind, of course.
The deadest horse in web development is the myth of “separation of concerns”
I was recently doing some very specific web scraping of some very public very static documents. About 25% of them use a soup of divs with hashes for class names. Not a <main> or <article> or <section> in sight. I am fine with the idea of what tailwind does but like at least using semantic tags where appropriate could be a thing.
You can separate concerns without violating locality of behavior, and that’s exactly what tailwind does.
It admittedly does not do a good job at being very DRY but I think that’s poorly applied to HTML/CSS in general, and the most DRY css is often over abstracted to the point of becoming nigh uninterpretable.
When I write CSS, I most often do not want the locality of behavior. I instead want uniformity of behavior, hence "semantic" styles. Even the trivial light / dark mode switching is pain with Tailwind, when classes like "color-gray-200" are routinely applied.
I’d somewhat agree with you there, but I usually use variables for uniformity. I do see arguments against tailwind but find anytime I’ve tried to do anything else it just feels like bikeshedding on internals for the same end result.
Really what I want to see is beautiful TDD for CSS so that uniformity can be enforced, but I’m not sure that exists.
Variables are hugely helpful, I agreee. IDK about bikeshedding. I'm very used to writing React code that normally declares no styles for components at all, and having CSS that style components using 1-2 classes, specific to these components. Container components control margins, <body> controls general things like fonts.
It seems that what solves the problem is a good component library. "But I need red text here!" For what reason? It's a warning. OK, we've got <Text variant="warning">, it will be styled appropriately, and will look like every other warning in the application.
Tailwind is a direct response to how the "C" in "CSS" actually sucks, so there's no surprise that it's so popular.
The "C" (Cascade) in CSS doesn't suck, the education about it sucks.
People don't know how it works, then things go wrong so they learn to work around it.
That's what led to things like div + class soup that you get with the BEM naming convention or Tailwind.
The cascade is actually awesome, super powerful and if you know how to use it, it can greatly simplify your code.
Education is the problem and the solution.
---
To anyone outside the CSS space, this is the closest analogy I can find:
In the American education system, there was a recent-ish change where children are "taught" to read using a method of just learning the shape of every word (e.g. "thermally" has a th at the start and ly at the end, so it must be the word "thermally", despite other similar looking words like thematically).
The method was disproven but the American education system still uses it.
Now illiteracy rates are climbing where almost 1/4 Americans (USA) can't read.
It's basically the same thing with CSS, where developers don't know what the code they're reading/writing is actually going to do.
Wait until you see React & JSX...
At least html and CSS are both presentation. React/JSX now confuses presentation and business logic.
> React/JSX now confuses presentation and business logic
React was originally designed to be the "V in MVC". You can still use it that way. React becomes very simple when you only use it as the V in MVC.
What are the M and the C, and how do they talk to the V in this case?
react can be pure functions that take in props. Given a set of props, ideally data primitives, the outputted view is guaranteed. it's nice.
In practice, the entire JS ecosystem enjoys flying off the rails, every season, but it's not strictly react's fault.
To answer your question, however those props get into the component is up the the M & C. can be async server, or shoved in as json in the script tag.
If you move the data (the M and the C) entirely out of react, and only pass it in via props, there would be only one place — the root react node — where the props could get into react. Is this what you have in mind? Or are you envisioning multiple root nodes?
Well, i've always been a fan of the island architecture that effectively mounts root nodes as little islands of isolated state, yes.
Mainly this avoids the hell that global state SPA patterns produce: redux, reducer patterns in general, and 8 thousand context providers.
I do think there's use cases that warrant global in-memory state, but it's such a pain in the ass to maintain and evolve, i'd always plan against it. Every html node in your app does not need to know about literally everything going on and react instantly to it. it just doesn't.
Just make another page!
Also: so the islands pattern can be as fancy or rudimentary as desired. they can bootstrap themselves via async endpoints, they can be shipped as web components even, or they can be static, pre-hydrated in some manner.
The islands pattern is underrated for maintainability. I've found the biggest win isn't even the state isolation — it's that each island can have a completely independent upgrade path. You can rewrite one island from React to vanilla JS (or whatever comes next) without touching anything else.
The global state SPA pattern fails for a more fundamental reason than just being painful to maintain: it creates an implicit contract between every component in the app. Change one reducer and you're debugging side effects three layers away. Islands make the contract explicit — each one owns its data, full stop.
The one gotcha I've hit is cross-island communication. PostMessage works but gets messy. Custom events on a shared DOM ancestor end up being the cleanest pattern for the rare cases where islands genuinely need to coordinate.
With signals you can avoid the prop drilling. I think signals can help a lot with this approach
I think the parent wants to separate the V from the M/C. If you smuggle signals inside of components to avoid prop drilling, you would be coupling the M/C and the V. I suppose that's not what the parent has in mind.
M stands for Model layer. This layer handles business logic and knows nothing about UI. It does not have any html or CSS.
V stands for View. This layer handles HTML and CSS. You can use React here.
C stands for Controller. Controllers know about Views and Models and which model objects to instantiate for which view. It makes REST API calls and does caching, and handles errors. Controllers know about the application state and decide what page to display next.
For an application written in this style see: https://github.com/wisercoder/eureka/tree/master/webapp/Clie...
(This app doesn't use React, but does use TSX, and you could use React as well).
- M for Model: your data model. - V for View: views of your data. - C for Controller: does stuff with your data.
In the original MVC architecture, the fundamental idea was that the model was responsible for storing the application state, a view was responsible for rendering output to the user, and a controller was responsible for responding to user interactions.
The model can be completely unaware of any specific views or controllers. It only needs to provide an interface allowing views to observe the current state and controllers to update that state.
In practice, views and controllers usually aren’t independent and instead come as a pair. This is because most modern UIs use some kind of event-driven architecture where user interactions are indicated by events from some component rendered by the view that the controller then handles.
My go-to example to understand why this architecture is helpful is a UI that features a table showing some names and a count for each, alongside a chart visualising that data graphically. Here you would have a model that stores the names and counts as pure data, and you would have two view+controller pairs, one managing the table and one the chart. Each view observes the model and renders an updated table or chart when the model state changes. Each controller responds to user interactions that perhaps edit a name or change its count — whether by typing a new value as text in an editable table cell or by dragging somewhere relevant in the chart — by telling the model to update its state to match (which in turn causes all views observing the model to refresh, without any further action from whichever controller happened to be handling that user interaction).
In practical terms for a React application, we might implement this with a simple object/Map somewhere that holds the names and values (our “model”) and two top-level React components that each get rendered once into some appropriate container within the page. Each component would have props to pass in (a) the current state and (b) any functions to be called when the user makes a change. Then you just write some simple glue logic in plain old JavaScript/TypeScript that handles keeping track of observers of the model, registering an observer for each top-level component that causes it rerender when the state changes, and providing a handler for each type of change the user is allowed to make that updates the state and then notifies the observers.
There are lots of variations on this theme, for example once you start needing more complicated business logic to interpret a user interaction and decide what state change is required or you need to synchronise your front-end model state with some remote service. However, you can scale a very long way with the basic principle that you hold your application state as pure data in a model that doesn’t know anything about any specific user interface or remote service and instead provides an interface for any other modules in the system to observe and/or update that state.
I think you're confusing business logic with view logic.
React is great for MVVM indeed. Who is still using MVC in 2026?
MVVM was invented by Microsoft for 2-way syncing in WPF. Today we know 2-way syncing is a mistake.
Who uses MVC in 2026? Pretty much every framework out there, including Java frameworks and Python frameworks and .net
You have any more sources on MVVM being a mistake?
I found WPF rather nice to work with. Same with knockout.js and Angular I don’t see much downsides.
Everyone can write bad code of course in each of them but I think it was working quite well.
When React launched in 2013, its defining idea was strict one-way data flow: parents pass data down via props, and updates happen in a clear, explicit place. Children can't mutate parent state directly; they signal changes through callbacks. The result is predictable, traceable state changes.
This contrasted with MVVM frameworks like early AngularJS, Knockout, and WPF, which relied on two-way data binding. That automatic syncing felt convenient for small apps, but at scale it often led to hidden coupling and hard-to-trace update chains.
Over time, many developers came to view pervasive two-way binding as a design mistake in complex systems. React's unidirectional model gained traction because it favored clarity and control over "magic."
Isn't Vue also MVVM?
Ever heard of Django? ASP.NET? Most UI frameworks, including ASP.NET Core, Spring Boot (Java based framework), Ruby on Rails, and Django (Python) are all based on MVC.
Those are all stateless MVC over HTTP, which is a very different architecture from stateful MVC for long-lived UI. The latter was invented for Smalltalk by Trygve Reenskaug, and is far more relevant to front-end web.
Stateful MVC uses Publisher/Subscriber (or Observer) to keep Views and Controllers up-to-date with changing Models over time, which is irrelevant for stateless MVC over HTTP. Plus, in stateful MVC the View and Controller are often "pluggable," where a given Controller+Model may use a different View for displaying the same data differently (e.g. table vs. pie chart), or a given View+Model may use a different Controller for handling events differently (e.g. mouse+keyboard vs. game controller). Whereas, in stateless MVC over HTTP, the controller is the "owner" of the process, and won't generally be replaced.
And in the world of front-end web, stateful MVC really is mostly dead. MVVM and Component-based architectures (using the Composite pattern) have replaced it. A runtime is usually responsible for wiring up events, rather than individual controllers. Controllers don't need to be swappable because events can be given semantic meaning in components, and Views don't need to be swappable because you can instead render a sub-composite to change how the data is shown.
Whether application state is short-lived (e.g., request/response CRUD) or long-lived (e.g., an in-memory interactive UI) is orthogonal to MVC. MVC is a structural separation of responsibilities between model, view, and control logic. The duration of state affects implementation strategy, not the applicability of the pattern itself.
MVC is a structural separation of responsibilities between model, view, and control logic.
Yes, but the “MVC” pattern used by various back-end web frameworks that borrowed the term a while back actually has very little to do with the original MVC of the Reenskaug era.
The original concept of MVC is based on a triangle of three modules with quite specific responsibilities and relationships. The closest equivalent on the back-end of a web application might be having a data model persisted via a database or similar, and then a web server providing a set of HTTP GET endpoints allowing queries of that model state (perhaps including some sort of WebSocket or Server-Sent Event provision to observe any changes) and a separate set of HTTP POST/PUT/PATCH endpoints allowing updates of the model state. Then on the back end, your “view” code handles any query requests, including monitoring the model state for changes and notifying any observers via WS/SSE, while your “controller” code handles any mutation requests. And then on the front end, you render your page content based on the back-end view endpoints, subscribe for notifications of changes that cause you to update your rendering, and any user interactions get sent to the back-end controller endpoints.
In practice, I don’t recall ever seeing an “MVC” back-end framework used anything like that. Instead, they typically have a “controller” in front of the “model” and have it manage all incoming HTTP requests, with “view” referring to the front-end code. This is fundamentally a tiered, linear relationship and it allocates responsibilities quite differently to the original, triangular MVC.
Adding to sibling comments, Phoenix. And it’s a damn nice experience at that.
Yeah let's do that. You have everything related to your component on place instead of jumping between files.
Is jumping between files supposed to be difficult or something?
Without a lot of discipline it is very easy to end up with a css with lots of unclear and hard to guess effects. Eg consider the case of <A type=1><B><A type=2></A></B></A> where A and B are complex templates. Any selector with the " " operator on A risk expanding to the inner A even if it was intended only for the outer. Similarly a :has selector might catch a descendant of the wrong element.
@scope fixes a lot of this, but it is a complex problem. With tailwind you mostly have to worry about inheritance
This problem was solved a long time ago with CSS Modules.
I prefer almost anything to CSS modules, so this bike shedding topic is probably very subjective.
Colocation is a useful principle in component-based architecture.
In my lived experience, shared components just become another problem. Especially in a fledgling company, the iteration velocity is actually negatively affected by shared libs because there's always overhead to (not) break legacy. so shared components bloat to address every evolving need.
And now with AI generated code i see so many wrapper patterns that forward endless props down, it's crazy!
TLDR: i almost always end up branching out into evergreen "reusable" components anyway.
Very unlikely the component library the CTO asked claude to DRY up the code with, is the one to rule them all.
Also modern CSS is often written in a <style> tag either in a native web component or in a framework which supports single file component like vue or svelte.
Is staying in one file supposed to be difficult or something?
this is grey text from tailwindcss.com, I wouldn't call it easy and readable.
<div class="relative before:absolute before:top-0 before:h-px before:w-[200vw] before:bg-gray-950/5 dark:before:bg-white/10 before:-left-[100vw] after:absolute after:bottom-0 after:h-px after:w-[200vw] after:bg-gray-950/5 dark:after:bg-white/10 after:-left-[100vw]"><p class="max-w-(--breakpoint-md) px-2 text-base/7 text-gray-600 max-sm:px-4 dark:text-gray-400">Because Tailwind is so low-level, it never encourages you to design the same site twice. Some of your favorite sites are built with Tailwind, and you probably had no idea.</p></div>