CSS modules are great, they encapsulate component styles to themselves: so that we could start using .container
for everything in everywhere again đ
Not too long ago create-react-app
released version 2 with the feature of using CSS modules. I'm sure once people upgrade to react-scripts@2
, they would immediately opt-in with excitement and start using CSS modules without a doubt: it makes CSS modular like everything else!
Inevitable problem
But sooner or later, you would realize there is one thing we cannot easily do anymore in CSS modules â a rather important (if not fundamental) thing in CSS: selecting and overwriting the styles of the (deep nested) child component thatâs in a different module from the parent.
How Angular and Vue solves the problem
Before we come back to react, letâs take a look at how Angular approaches the problem. In Angular, style encapsulation is actually done in a different way with emulated shadow DOM. The actual implementation is through adding extra generated attributes to the DOM elements (rather than changing the class names, which will be mentioned below).
The way to target the child components is simple: you would just need to use ::ng-deep
(or /deep/
/>>>
) before the children selectors. And it would generate the styles for that element without any attribute attached, thus achieving the goal to target any nested child elements and get around the view encapsulation.
With Vue, I havenât work with it that much, but I saw this and assumed it is pretty similar to Angular in terms of implementation.
How we could do it in React with CRA
But in CSS modules, the actual implementation is to hash and rename the class names to make sure they are unique.
That made my first trial to do things in a way thatâs similar to Angular failed. My first intuitive way to do it is to use the : global
keyword to un-encapsulate (or de-encapsulate) the child selectors, but that didn't work as the children's selectors are hashed and renamed, which can't be easily targeted in this way.
Then I talked to @alemesa and found out @donghyukjacobjang and he are doing each component with an un-scoped normal string class name which has the sole purpose of being targeted from outside of the CSS module. This way has been working for them quite well, but in my opinion, this way is more like a convention that people have to follow; and it somewhat defeats the purpose and benefits of using CSS modules.
After doing some searches, I still couldnât find anything thatâs quite similar to how Angular and Vue do it. But I did find an interesting solution here that could satisfy me and my needs. It suggests that we could define a child and its styles in a parent module first, and then import the child class name and pass that down to the child as one of the props in JSX. This solution, in my opinion, is still kind of a way by convention as the children would need to know to expect and use the class names from the props. But itâs the best solution I could find/think of at the moment, and it also provides more predictability and stability compared to the Angular/Vue way.
Conclusion
Although at the moment if you ask me, I would still prefer the shadow DOM implementation and emulation with HTML element attributes like the way in Angular, CSS modules are great too! Itâs very easy to opt-in (thanks to CRA too!), and also you could migrate to it gradually and starting enjoying its benefit today.
All I have to say is that with all the benefits, it also comes with some minor issues that you need to consider before jumping into it, and the problem we discussed here is one of them. Moreover, Iâd like to also point out a few other things I noticed for your consideration:
- It is recommended that you use
camelCase
for the class names. (You could usekabab-case
, but you would not want to.) - The generated class names (with the CRA setup) are not uglified and usually very long (long enough to increase the bundle size).
Please leave a comment to share your opinions and solutions to this problem, cheers!
Top comments (4)
This was super helpful - thanks for taking the time to write it up.
I wasn't immediately able to understand the technique you mention from @alemesa and @donghyukjacobjang of using "an un-scoped normal string class."
I was trying something like this and it was failing me. I've since learned that to keep CSS Modules from also adding the unique modifier to the normal string class you're trying to reference, you need to explicitly tell CSS Modules to leave that class alone with the
:global()
modifier.For example, if you have an element with a CSS Modules class
.foo
with a child element with a normal static string CSS class.bar
, then you'd need to do the following:Thanks again for the helpful post.
Thank you, Steven. That really helped with an issue I was just having.
Thank you for helping me solve my issue.
Or use styled-components. :)