Style the Document, Not a Component Tree
First published at Wednesday, 10 June 2026
Style the Document, Not a Component Tree
I work as a software architect at FernUniversität in Hagen on LEAD:FUH, a learning-analytics data platform handling highly sensitive student data. This post is adapted from our internal engineering documentation. It is not about the data platform itself, but about how we write the frontends on top of it.
The scoping trade nobody prices in
BEM, CSS Modules, and styled-components all solve the same problem: styles leaking between components. They solve it well. But the price is paid somewhere else, and it is rarely written on the label: When every button has its own generated class or block__element--modifier name, there is no shared design language left. There is just a pile of isolated components which happen to look similar. Until they don't.
Three things become effectively impossible at that point:
Re-theming. Changing the brand color or switching to dark mode means hunting through hundreds of scattered rules. With every color and every distance defined as a CSS variable, a theme is a single
:rootblock.Layout consistency. Without a constrained system every new page introduces a slightly different spacing value, a near-duplicate gray, and one more button variant. Nothing lines up, and no single commit is to blame.
A design system which grows by composition. When
<button>,<table>, and<section>are styled directly and varied through data attributes, every new page inherits the design language by default. Component-class systems grow by accumulation instead: Every feature adds styles, and the pile only ever gets taller.
So our rule is the title of this post: Write semantic HTML first. Style HTML elements directly. Introduce a class, a data attribute, or a custom element only when the element alone is not specific enough. The browser's defaults plus your element styles should carry most of the weight.
Concretely, this is the entire button story, lightly condensed from our shared stylesheet:
button,
a.button {
padding: var(--space-xs) var(--space-md);
background: var(--brand);
color: var(--surface);
border: none;
border-radius: var(--radius-lg);
box-shadow: 0 2px 4px var(--shadow);
}
button[data-status="danger"] {
background: var(--danger);
}Every <button> in every application is now the designed button. There is no class to remember and no component to import. A destructive action is <button data-status="danger">Delete</button>: The variant is data on the element, not a new visual species. And because every color and distance is a variable, the brand refresh or the dark mode is a change to :root instead of a hunt through component files. There is no Button.module.css, no btn btn--primary btn--lg, and no decision to make at the call site. The hundredth button looks like the first.
There is a team dimension to this, and it was one of the deciding reasons for the approach: When the elements themselves carry the design, anyone who writes plain, semantic HTML immediately gets a page which follows the corporate identity, without knowing that there is a design system, let alone how to use it. On a platform team most of the people touching a template are backend engineers with no feel for spacing, color, or typography, and no ambition to develop one. Element-first styling means their honest <form><label><button> markup comes out looking right by default. They never have to learn the design system to benefit from it.
We did not have to invent the methodology for this either. Our stylesheets follow CUBE CSS (Composition, Utility, Block, Exception): Global element styles and layout compositions do the bulk of the work, utilities and blocks refine where needed, and exceptions, our data-attribute variants, are explicitly the last resort. CUBE's founding idea is the same as this post's: Work with the browser and the cascade instead of against them. Style the document and let specificity grow only as the need does.
This is, I am fully aware, how we wrote CSS twenty years ago. It was a good idea then. It is a better idea now, for a reason which did not exist then.
The LLM angle
Visual entropy used to accumulate at human speed: A new developer here, a rushed deadline there, and after two years the application has eleven grays and four button heights. A lot of frontend code is now generated by LLMs, and there the problem is much worse. LLMs are visual-entropy machines. Ask one for a settings page and it will invent a new spacing value, a new shade of gray, and three new class names, all plausible and all slightly different from the last generation. Each result is locally fine and globally inconsistent, and the inconsistency now arrives at generation speed instead of hiring speed. A component-scoped methodology even invites this: Every component is supposed to be a one-off.
A strict variable system and element-first styling invert the situation. When the only available colors are var(--color-*) tokens, the only distances are the spacing scale, and a plain <button> is already styled, the path of least resistance is the consistent one, for a developer under deadline exactly as for a code generator. The constraint does not care who or what writes the code, it steers both. Consistency stops being a matter of discipline and becomes the default output.
I have written before that LLMs force engineering discipline we should have had all along. This is the CSS instance of that rule: The guardrails which keep generated frontend code consistent are the same ones which kept human frontend code consistent.
Components without a framework
None of this means "no components". It means components are the exception which earns its place, not the default unit of everything. When a pattern has real behaviour or real reuse, we build a vanilla web component: a standard HTMLElement subclass on a shared base class of twenty lines which provides attribute/property syncing and a define() convenience. No framework, no build-step lock-in, the browser API is the only runtime.
The discipline lives in the bar for creating one. A pattern becomes a component when at least two of these hold:
It is used in more than one application.
It has its own interactive behaviour (open/close, fetch, toggle).
It encapsulates non-trivial rendering logic.
It would otherwise be copy-pasted between files.
Purely structural or layout concerns are explicitly no reason for a component. A <section> with a class is not a component. Wrapping it in one adds a naming decision, a file, and a registry entry, removes the thing from the reach of plain CSS, and provides no behaviour at all.
The one supporting tool worth its weight is a live pattern library: a single HTML page showing every shared style and component. Its job is to make "check whether this already exists" cheaper than "invent it again", and it works on anything which reads it, human or otherwise.
Summary
Frameworks, CSS methodologies, and build toolchains have a shelf life of a few years. <button>, <table>, and CSS variables are web standards with backwards compatibility measured in decades. A university platform is built to be handed over and operated long after the project which built it ends, and betting its frontend on the web platform itself (semantic HTML, element styles, standard custom elements) is the lowest-risk bet available. The result is boring, but it is boring with a design language, a one-block dark mode, and new pages which line up with the pages next to them, no matter who, or what, wrote them.
Readers of my book will recognize this as Peter's CSS. In Nothing Shared, Everything Gained Peter is the product-developer archetype: He builds products for his own company, controls all the code, and refuses abstractions for hypothetical future needs. He adds them when actual needs appear. The same stance, applied to styling, is everything this post describes. BEM's naming discipline and Tailwind's utility layer hedge against problems Peter does not have: stylesheets shared across teams which cannot coordinate, or components shipped to codebases one does not control. When you control all the code, semantic elements plus a constrained variable set are enough. Most of the time your CSS should be Peter's CSS.
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: