I recently opened a frontend with almost 20,000 lines of global CSS.
My first instinct was predictable:
We should probably move this into CSS Modules.
That turned out to be only partly right.
Some of the CSS could move into locally scoped component styles almost immediately. Other parts were expressing relationships between components that the codebase had never made explicit.
If I had scoped those selectors first, I would have broken the architecture before understanding it.
That led to a more useful conclusion:
CSS Modules enforce existing ownership boundaries. Defining those boundaries is separate work.
And that distinction matters.
Global CSS can be legitimate
Large global stylesheets tend to trigger an immediate architectural reaction.
They look risky.
They can be hard to navigate.
They can create specificity problems.
They can make it difficult to know which styles belong to which component.
All of that can be true without meaning the architecture is broken.
A global stylesheet can still contain:
design tokens
reset rules
typography foundations
document-level layout
accessibility helpers
theme variables
third-party overrides
deliberate application-wide contracts
Those things are genuinely global. They belong there even if the stylesheet stays large.
The more useful question is:
Which styles are actually global, and which styles belong to a component but currently live globally?
That is a very different problem.
BEM naming already does useful work
The frontend I was reviewing used a lot of BEM-style naming.
The naming stays structured even as the ownership boundary erodes.
The parent feature knows too much about how ProductCard is implemented internally. That cross-boundary knowledge is the architectural smell, regardless of whether the class is global.
Hidden contracts survive scoping
This is where CSS Modules can give you a false sense of progress.
Great. Now the internal selectors are locally scoped. Here's what happens to this:
css
.hero.cp-product-card__title {
color: white;
}
That selector was expressing a real requirement. The hero wanted a different version of the product card. Once the class becomes locally scoped, the parent can no longer reach into it directly.
Scoping the class removes the mechanism that expressed the requirement. The requirement itself stays.
That is why migrating a highly coupled component into CSS Modules too early can actually make the situation worse.
You need to understand the dependency first.
Ask what the override is trying to say
Before moving a component into a CSS Module, I would classify its external styling dependencies.
Most overrides I encounter fall into a handful of categories.
1. It is actually a component variant
For example:
css
.hero.product-card__title {
color: white;
}
may really mean:
tsx
<ProductCard tone="inverse" />
The component has a legitimate alternate presentation. The API simply never exposed it. That's a gap in the component's contract.
2. It is contextual theming
Sometimes the difference applies to everything inside a section.
Now components respond to context, and parents no longer need to manually restyle each one. This is a theme context, distinct from a component variant.
3. It is legitimate parent layout
Consider:
css
.carousel.product-card {
width: 280px;
}
That may be completely fine. The parent owns layout. It is reasonable for a grid, carousel or section to decide:
width
placement
gap
alignment
grid span
Turning every parent rule into a prop, like:
tsx
<ProductCard width="280px" />
moves layout responsibility into the wrong place.
4. It is a legitimate one-off composition
Not every unusual design needs to become a reusable variant. If something appears once and is genuinely specific to one feature, a local wrapper or composition may be cleaner than expanding a shared component API.
The scoping is real, and the parent is still manipulating the child's internals beneath it. Global selectors have simply become prop-based selector injection, carrying the same coupling forward.
The real question is still:
Who should own this decision?
If the child owns it, expose a semantic API. If the parent owns it, let the parent control layout. If it is context, use context. If it is truly one-off, compose locally.
Design-system thinking has to happen regardless of scoping.
What should remain global?
Even in a codebase that uses CSS Modules heavily, I would expect some global CSS to remain.
Their visual behaviour is specific to the product. A local stylesheet next to the component gives that component a clear ownership boundary. Meanwhile, the interaction primitives underneath may come from another layer.
For example:
text
Product feature
↓
Brand Drawer
↓
Base UI behaviour
while:
text
Drawer.tsx
Drawer.module.css
owns the brand-specific visuals.
That produces a useful separation:
text
Interaction behaviour → primitive layer
Visual styling → local component
Global design language → tokens/theme
Layout → parent
Each concern has somewhere obvious to live.
Skip the big-bang migration
This is probably the most practical lesson.
A project titled:
Convert globals.css to CSS Modules
sets a mechanical goal. It says nothing about whether the architecture improves.
Migrating incrementally, inside the path of real feature work, keeps the goal tied to the architecture. For example:
text
Component enters production work
↓
inspect external dependencies
↓
classify them
↓
formalize real component contracts
↓
remove unnecessary coupling
↓
move owned styles into CSS Module
↓
validate visual parity
That's slower per component, and safer for the project as a whole. Because you're doing it in the path of real implementation, you're spending time on components that actually matter.
Start with easy components
When introducing CSS Modules into an existing application, start with something isolated.
For example, a marketing band that:
owns its entire visual structure
has no external consumers styling its internals
has a small prop surface
does not depend on complicated responsive overrides
Move that first. Establish:
file convention
import pattern
naming style
testing expectations
Storybook pattern
token usage
Then move to slightly more complicated components. Only later tackle the shared components with significant external dependencies.
This gives the team a working pattern before the difficult cases appear.
Highly coupled components need a different first step
Suppose ProductCard is used everywhere.
Homepage.
Search.
Saved products.
Recommendations.
Recipes.
Product listings.
And each one modifies it slightly differently. That's a poor candidate for a first CSS Module migration.
First build a map of those differences. Maybe you discover that the dozens of overrides actually reduce to:
text
tone
density
metadata visibility
action presentation
layout context
Now you can design a stable API. Then move the styles. This is much better than translating every existing selector directly into another styling system.
The migration should reduce knowledge
One useful way to judge whether the refactor is working is to ask:
How much does the parent need to know about the child?