Encapsulation Violation with CSS
Encapsulation is one of those principles most developers apply instinctively, even if they’ve never said the word out loud. A class or module exposes a public interface, and keeps everything else private. The private stuff is free to change however you like, whenever you like — because nothing outside the module is allowed to depend on it.
That freedom is what makes it scale. If you change something private, you know — with certainty — that no consuming code needs to change. The blast radius of your change is exactly one file.
A simple example
class Counter {
#count = 0; // private
increment() {
this.#logChange(); // private helper
this.#count++;
}
#logChange() {
console.log("count is about to change");
}
getCount() {
// public
return this.#count;
}
}
Consumers only ever touch increment() and getCount():
const counter = new Counter();
counter.increment();
counter.getCount(); // 1
Now say we rename #logChange to #logCountChange, and change #count to start at 10 instead of 0:
class Counter {
#count = 10; // changed
#logCountChange() {
// renamed
console.log("count is about to change");
}
increment() {
this.#logCountChange();
this.#count++;
}
getCount() {
return this.#count;
}
}
Nothing else in the codebase needs to change. The consumer above still calls increment() and getCount() and still works, untouched, no matter what happens inside the class. That’s the whole point of private — the # guarantees it.
Most developers know this instinctively. When you rename a private field or method, you don’t go searching the repo for consumers — you already know there aren’t any, because nothing outside the class was ever allowed to reach in. That confidence is encapsulation doing its job.
Now bring in CSS
Take a React component, A, made up of a div styled with a class:
// A.jsx
import "./A.css";
const A = ({ text }) => <div className="a">{text}</div>;
/* A.css */
.a {
color: green;
}
Now take another component, B, that renders A:
// B.jsx
import "./B.css";
import A from "./A";
const B = () => (
<div className="b">
<A text="Hello" />
</div>
);
/* B.css */
.b .a {
color: red;
}
I’ve seen many developers do this — reach from an outer component’s stylesheet directly into an inner component’s class name to override its styling.
Two questions
If you were working on A, and you renamed .a to .a-outer, would you think to grep the repo for .a and update .b .a in B.css to .b .a-outer?
If you deleted A entirely, would you think to search for .a-outer and delete the now-dead .b .a-outer rule in B.css?
I don’t think most developers would do either. Not because they’re careless, but because they don’t think of .a as something B was ever allowed to depend on in the first place. It doesn’t occur to them to check.
Compare that to what happens when you rename a prop, or rename the component itself. If you renamed A’s text prop to label, you’d absolutely go find every place that passes text={...} and fix it. If you renamed the A component to C, you’d absolutely go find every import of A and update it. Most developers don’t skip that step, because props and component names are the public interface. Most people know it, and most people treat it that way.
What that tells us
If most developers wouldn’t think to update .b .a when .a changes or disappears, then most developers don’t actually believe .a is part of A’s public interface — whether they’d phrase it that way or not. .a is being treated, correctly, as private and internal to A. Which means reaching into it from B.css is an encapsulation violation. B is depending on a piece of A that was never meant to be depended on.
The fix is the same fix as always: respect the public interface. If B needs to affect how A looks, it should do that through props — the same channel it already uses for everything else.
// A.jsx
const A = ({ text, style }) => (
<div className="a" style={style}>
{text}
</div>
);
// B.jsx
const B = () => (
<div className="b">
<A text="Hello" style={{ color: "red" }} />
</div>
);
(A className prop works the same way, if you’d rather pass a class than inline styles.) Either way, A decides what it accepts and how it applies it. B no longer knows or cares what A’s internal class names are, or whether it even has any — A is free to rename .a, restructure its markup, or rewrite its styling entirely, and B never has to change.
Takeaway
Encapsulation isn’t just an OO concept that applies to classes and private fields — it applies to React components and their CSS too. A component’s internal class names are implementation details, not part of the public interface, and styling a component from the outside by reaching into those class names breaks encapsulation just as surely as reaching into a private field would. However props are a part of the public interface — why not just use them to style components from the outside instead?