Previous article: Why I Chose C# for Business Systems and Still Use It
In the previous note, I wrote about project boundaries in C#. This time, I want to focus on style boundaries.
This is not a tutorial. It is a reflection on how my CSS architecture changed after repeated failures in real business screens.
The global style.css era
At the beginning, I put almost all styles into one global style.css. The entire application depended on that file. That was not a deliberate architecture decision. It was mainly a result of my limited frontend experience at that time.
I do not consider that design good. It worked while the system was small, but it broke down as soon as screen count and variation increased.
As features accumulated, style.css only expanded. Selectors became harder to trace because many rules were generic and far from the screens they affected. The cascade itself was not the problem. The problem was that ownership of the cascade was unclear. When ownership is unclear, a change made for one page can silently alter another page.
When this happened repeatedly, teams started to rely more on inline styles as a local escape hatch. That reduced short-term risk for one ticket, but it increased long-term inconsistency. During layout regressions, root-cause investigation slowed down because rule precedence had to be reconstructed from too many locations. The style system became operationally fragile.
The categorization phase
The first structural improvement was simple categorization by purpose. I split styles into frame.css, parts.css, list.css, and editor.css.
This pattern still exists in my systems today. It was not perfect, but it was a real improvement. Collision frequency dropped because broad layout rules and reusable part rules were no longer mixed without intent. Debugging also became easier because investigation started from a narrower file set.
More importantly, this phase changed how I thought about CSS. I stopped treating styles as one shared text asset and started treating them as responsibility domains. Once that perspective appeared, further separation became easier to justify architecturally.
Transition to Razor Pages and scoped CSS
When I moved back to C# and Razor Pages, scoped CSS had a strong impact on my architecture decisions.
To be clear, scoped CSS is not unique to C#. I do not claim that C# created the concept. I am not comparing ecosystems here. I first encountered scoped CSS in a C# project by chance, through Razor Pages. That first encounter changed how I structured screen styles.
My return to C# itself was based on familiarity, syntax preference, and strict typing. Azure strategy also influenced that decision. Azure fit my operational and organizational context best at the time.
What the Razor Pages mechanism changed
In Razor Pages, Main.cshtml pairs naturally with Main.cshtml.css. The same pattern applies to layout files, such as _Layout.cshtml and _Layout.cshtml.css.
That pairing shifted style ownership. Layout-level concerns that used to live in frame.css moved into _Layout.cshtml.css. Page-specific parts moved into each page-local stylesheet. The path from markup to style became physically short, and that reduced accidental cross-screen coupling.
The operational impact was immediate. Unintended style bleed that used to trigger late-night debugging sessions almost disappeared, and day-to-day screen maintenance became much calmer.
Why shared list.css and editor.css still remain
Even after adopting scoped CSS, I kept some shared files such as list.css and editor.css. This is intentional.
Scoped CSS works through attribute-based isolation, which is excellent for local ownership. Under the hood, the build process rewrites selectors by attaching a generated attribute to the component root and prefixing matching selectors accordingly. But some formatting responsibilities are cross-screen by design, especially table conventions and form-field structures that must stay visually consistent in multiple pages. For those domains, centralized shared CSS still provides a better control point than repeating equivalent rules across many scoped files.
So the model is not total replacement. It is selective isolation with explicit shared domains.
ElementWrap and the dynamic CSS boundary problem
Before CotomyElement, I used an earlier abstraction called ElementWrap. As dynamic DOM generation increased, a new boundary problem appeared. CSS definitions were often too far from the point where elements were created and used. That distance created architectural discomfort because behavior and ownership were no longer visible in one place.
The response was to allow HTML and CSS together at construction time. I intentionally did not use inline styles for this. Responsive behavior required media queries, and inline style attributes cannot represent media-query rules.
Trying to hard-code viewport-based class switching in TypeScript felt fundamentally wrong.
Style tag lifecycle management
Once HTML and CSS were allowed together, lifecycle discipline became mandatory. In CotomyElement, a style tag is generated automatically for scoped CSS, and cleanup is tied to element lifecycle. When the last element of the same scope disappears, the corresponding style tag is removed.
This behavior matters in dynamic grids and frequently recreated UI blocks. Without cleanup, style definitions accumulate over time and obscure the live state of the page. With cleanup, style resources follow the same lifecycle model as UI resources. That keeps runtime state understandable and reduces long-session drift.
From [scope] to [root] in CotomyElement
In early versions, the placeholder was [scope], largely because I was influenced by the term scoped CSS itself. But semantically, it was always pointing to the root element of each CotomyElement. That mismatch kept bothering me.
At that time, parts of the API were already publicly available even though version 1 had not been released yet, so changing the keyword was not a trivial decision. I prioritized semantic clarity and introduced a transition period where [scope] and [root] worked in parallel. When preparing version 1, I unified the syntax to [root].
Current behavior injects the actual root scope prefix automatically for child selectors. In implementation terms, [root] is rewritten to a data-cotomy-scopeid selector, and if [root] is omitted, it is prefixed automatically. Even so, I still recommend writing [root] explicitly for readability. It makes ownership visible in the selector itself and reduces interpretation cost during reviews.
The current separation model
At this point, style domains are clearer than before. I can separate three layers with less ambiguity. System-wide styles remain in shared files when global consistency is required. Server-rendered component styles are isolated by Razor Pages scoped files. Client-generated component styles are isolated at runtime through CotomyElement scoped CSS.
Since enabling CSS handling in ElementWrap and later formalizing it in CotomyElement, bugs caused by style-scope confusion have almost disappeared. Not because CSS became simple, but because style ownership became structurally explicit. What changed was not the syntax of CSS, but the structural visibility of style ownership.
Closing reflection
I do not know who first invented scoped CSS as an idea. But as an architectural tool, it deserves appreciation.
C# Architecture Notes
This article is part of the Cotomy C# Architecture Notes, which reflect on backend and project-structure decisions around business systems.
Series articles: Why I Chose C# for Business Systems and Still Use It , From Global CSS Chaos to Scoped Isolation, and Unifying Data Design and Code with Entity Framework .
Next article: Unifying Data Design and Code with Entity Framework