DEV Community

Wenchen (Neo) Li
Wenchen (Neo) Li

Posted on

Selecting elements in children components within CSS modules

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 use kabab-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)

Collapse
 
sgarrity profile image
Steven Garrity

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:

.foo :global(.bar)  { 🎨 }
Enter fullscreen mode Exit fullscreen mode

Thanks again for the helpful post.

Collapse
 
darryl profile image
Darryl Young

Thank you, Steven. That really helped with an issue I was just having.

Collapse
 
vicradon profile image
Osinachi Chukwujama

Thank you for helping me solve my issue.

Collapse
 
jadedevin13 profile image
Jade Devin Cabatlao

Or use styled-components. :)