Structural sibling to Why Every Large Stylesheet Eventually Becomes Unpredictable — same distinction, different layer.
Also available in Español
The Problem
A dashboard section is titled "Active Subscriptions." Visually, it reads as a section heading: bold, sized larger than the surrounding text, sitting exactly where a reader expects a section title to sit. Ask a screen reader what it actually is, and the answer is different. It's a heading six levels below the page title, an element sitting at the bottom of the heading hierarchy, describing a section a reader would expect to find near the top of it.
Nobody wrote that heading level on purpose. A developer dropped a card component into a dashboard layout and moved on. The component looked correct in the page it landed on. It looked correct in isolation, in the design system's own component library, in every place anyone actually reviewed it. The heading level was never a decision. It was a default, set six components away, inherited by a section that never got a vote.
Why the Problem Exists
Component abstraction exists to solve a real problem. Without it, every team rebuilds the same card, the same button, the same typography scale, and small inconsistencies compound across the codebase. A shared component library trades that inconsistency for one reviewed implementation. Reuse goes up. Visual drift goes down.
The trade happens at a specific seam. A component author writes code once, for use in places they can't fully predict. To make that possible, the component has to make decisions on its own: how it looks, how it behaves, what it renders, all without knowing exactly where it will land on the page. That isn't a flaw in the design — it's the entire premise of reuse.
Most of those local decisions are genuinely safe to make. A button's padding doesn't depend on where the button sits on the page. A card's border radius doesn't either. But a small number of decisions aren't local at all. They depend on document context the component was never given. A heading level is one of them. Whether a section title should render as an <h2> or an <h6> isn't a property of the section. It's a property of where the section sits inside the page's outline — a fact the component rendering the heading usually never sees.
The First Principle
The browser receives the DOM the components produced: a hierarchy of elements with no memory of which file authored which node, no boundary marking where one component's output ends and the next one's begins. Six components can collaborate to render one heading. The browser reads one <h6>.
Heading levels in HTML are not inferred from component or section depth. An <h6> nested three sections deep is still exactly what its tag name says, read in the order headings appear on the page. A screen reader's headings list is built from those tag names, in document order — not from which component authored them. It has no way to ask which component meant to say something different.
Which makes the browser the point where every componentized decision collapses into a single, flat fact. Six layers of intention resolve down to one tag. Whichever layer ultimately sets that tag becomes responsible for the heading level, whether or not that layer has the information needed to set it correctly.
Demonstrating the Principle
Reduce the pattern to its smallest form. A layout component knows where a section sits on the page. A typography component knows how a heading should look.
function Section({ title, children }) {
return (
<section>
<Heading variant="subheading">{title}</Heading>
{children}
</section>
);
}
function Heading({ variant, children }) {
const Tag = variant === 'subheading' ? 'h6' : 'h2';
return <Tag>{children}</Tag>;
}
Rendered on a page after an <h1>, this produces:
<main>
<h1>Dashboard</h1>
<section>
<h6>Active Subscriptions</h6>
</section>
</main>
Section knows it sits inside a dashboard, after an <h1>. It never sees which tag its heading renders as. Heading knows exactly which tag it renders, decided entirely by a variant prop. It has no idea where on the page it's being used. Between the two, neither component holds both pieces of information the decision actually needed: the page position, and the tag name.
The fix isn't removing a layer. It's moving the decision to whichever layer already has the missing half:
function Section({ title, level, children }) {
const HeadingTag = `h${level}`;
return (
<section>
<HeadingTag>{title}</HeadingTag>
{children}
</section>
);
}
Now the component that knows the page position also sets the tag. Visual treatment can still live somewhere else entirely — a class, a size variant, a weight override. What moves is the heading level itself, and it moves to the component that had the context to set it, not the component that only knew how a heading should look.
The Pain Point
This surfaced during an audit of a client's design-system migration, on an enterprise dashboard already in production. A section titled "Active Subscriptions" was visually correct: sized and weighted like every other section header on the page, and six levels below the page title in the heading hierarchy. Getting there took six components — a dashboard layout, a card group, a card, a card header, a typography utility, and a heading primitive that mapped a variant prop to a tag name.
Tracing the actual heading level meant reading through all six. The dashboard layout owned the page's outline and had no visibility into what any individual card rendered. The heading primitive owned the tag name and had no visibility into where on the page it had landed. Every layer in between passed the decision along untouched. No single component was broken. Each one did exactly what it was built to do. The heading level simply wasn't a decision any of them was positioned to make correctly, because none of them held the page context and the tag name at the same time.
Saturday's Notes from the Pass walks that same six-layer trace, marks the point where the structural decision and the document context it needed came apart, and compares two ways of putting them back together — without collapsing the six components into one.
The Broader Lesson
This isn't an argument against component abstraction. Every reason components exist is a real reason, and none of it disappears because one heading landed on the wrong tag. The failure isn't in dividing a page into components. It's in dividing a decision across components without checking whether any single one of them was left holding enough information to make it.
Every layer of abstraction, in any system, draws a boundary around what that layer needs to know. Most of the time the boundary is exactly right: the layer doesn't need the rest of the picture, and hiding it from that layer is the whole point. Occasionally a decision doesn't respect the boundary the way the architecture assumed it would. It needs information from both sides at once, and when neither side owns the whole decision, the decision gets made anyway, by whichever layer happens to touch it last.
Component architecture can organize the source code. It cannot replace the structure of the document. The two answer to different consumers: one to the developer opening the file, the other to the browser, and everything the browser hands that decision to next.
Top comments (0)