A few days ago, we had an interesting discussion in our codebase.
Some developers wanted to introduce a new Heading component.
Something like:
<Heading level={3}>Profile Settings</Heading>
At first glance, it looks harmless.
But there was one problem.
We already could do this:
<h3>Profile Settings</h3>
And we also could do this using our existing Text component we had built for creating different variants of a text:
<Text as="h3">Profile Settings</Text>
That immediately raised a bigger question:
Why are we introducing a new abstraction when the problem is already solved?
The answer I got was:
“I like that we have a separate heading component.”
That answer stuck with me.
Not because it was wrong.
But because it exposed something many teams struggle with:
How do we make architectural decisions in code without letting personal preference drive them?
The Problem With Preference-Driven Components
Frontend teams love abstractions. Sometimes too much!
We build wrappers around wrappers around wrappers until the original HTML becomes unrecognizable.
A lot of the time, the justification is vague:
- “It looks cleaner”
- “Feels more consistent”
- “I like it”
Those reasons are understandable. But they’re weak engineering reasons.
Because every new component adds cost:
- Documentation
- Maintenance
- Testing
- Migration complexity
- API overlap
- Team confusion
A shared codebase isn’t your personal sandbox.
Every abstraction becomes part of the team’s long-term maintenance burden. That burden needs justification.
HTML Is Already a Great API
This is something we forget.
- HTML is not primitive. *
It’s already a carefully designed abstraction.
Take headings:
<h1>Dashboard</h1>
<h2>Analytics</h2>
<h3>Revenue</h3>
This already gives us:
- semantic hierarchy
- accessibility structure
- screen reader support
- SEO context
- document outline
What does this improve by doing?
<Heading level={1}>Dashboard</Heading>
<Heading level={2}>Analytics</Heading>
<Heading level={3}>Revenue</Heading>
In many cases: Nothing.
It’s just different syntax.
And changing syntax without changing behavior is rarely a useful abstraction.
A Rule That Changed How I Think About Components
Here’s the simplest rule I follow now:
Don’t abstract syntax. Abstract behavior.
This one rule kills a lot of unnecessary debates.
Bad abstraction
<Heading level={3}>Title</Heading>
Why?
Because it mostly mirrors native HTML.
Useful abstraction
<Text variant="muted" size="small" truncate>
Description
</Text>
Why?
Because it adds real behavior:
- design token mapping
- truncation
- theme consistency
That’s value.
That’s worth abstracting.
Not All Components Are Equal
Let’s compare two examples.
Heading
<Heading level={2}>Billing</Heading>
Usually just renders:
<h2>Billing</h2>
No added capability.
Mostly syntactic sugar.
Text
<Text variant="caption">Last updated 2 mins ago</Text>
This can centralize:
- font sizes
- line heights
- weights
- spacing
- responsive behavior
That's a legitimate design-system concern.
Because HTML doesn't solve typography tokens.
CSS/design systems do.
That's where Text makes sense.
One Concept, One API
One of the easiest ways to create confusion in a codebase is giving developers multiple ways to do the same thing.
Bad:
<Heading level={3}>Profile</Heading>
<Text as="h3">Profile</Text>
<h3>Profile</h3>
Three options.
Same output.
Now every PR becomes a style debate.
Good systems reduce choices.
A useful principle:
One concept, one API.
If HTML already owns semantics, let it own semantics.
If Text owns typography, let it own typography.
Clear separation.
Clear ownership.
Better Composition Wins
Instead of this:
<Heading level={2}>Billing Settings</Heading>
Prefer:
<h2>
<Text variant="secondary">Billing Settings</Text>
</h2>
This gives:
- native semantics
- reusable typography tokens
- clear ownership of responsibility
No overlap.
No ambiguity.
Composition over abstraction.
Questions I Ask Before Adding a Component
Before adding any new component, I ask:
1. What problem does this solve?
Not:
“Looks cleaner.” or
“I like it this way.”
But:
“Enforces accessible hierarchy.”
That’s measurable.
2. Does HTML already solve it?
If yes:
Why are we replacing it?
3. Does it reduce complexity?
Or does it just move it?
4. Does it introduce overlap?
Overlapping APIs create inconsistent code.
5. Does it add behavior?
If not, it probably shouldn’t exist.
Final Thought
Not every repeated pattern deserves a component.
Not every native element needs a React wrapper.
And not every preference deserves to become a shared abstraction.
The next time someone says:
“Let's make a component for this. And I like it this way”
Ask:
Is this abstraction creating new capability, or just new syntax?
That one question can save your codebase from years of unnecessary complexity.
Because in frontend systems:
every abstraction is a liability until it proves otherwise.
Top comments (0)