css modules, sanitize.css and publishing a standalone react component

At $WORK we started with a simple web app, I think its boilerplate was copied from grafana or something, so it came with custom webpack scripts.

At some point we decided that one of its components was self contained enough and worth it to publish on its own. We moved it to lerna and published on npm. I will refer it as standalone from now on.

We noticed that the styling for the standalone component looked off. A button looked slightly different, a table looked weird etc.

And to be clear, it looks just right in the webapp version. This is an important point.

So after some quick investigation, obviously we found out that the set of styles we were shipping to the webapp and to the standalone component were different.

Turns out the difference is that the we were not shipping sanitized styles (from sanitize.css) to the standalone component.

However, we can’t just ship those styles, since they are global and they may affect end user’s styles (imagine I embed the standalone component into my app, and then somehow it overrides my app’s button!)

The solution, of course, is to scope those rules.

However, if we scope them, then they start to override the ones from css modules. For illustration, consider a sanitize.css rule for table:

1
2
3
table {
  border-collapse: collapse;
}

If we scope under .pyroscope it will become:

1
2
3
.myapp table {
  border-collapse: collapse;
}

Then let’s say we have a css modules rule, which normally would override the one from sanitize css (since class has higher precedence over a simple tag)

1
2
3
.oiJX5Zp1+W8aJOcWSU-baQ== {
  border-collapse: separate;
}

In our case, .myapp table is higher than .oiJX5Zp1+W8aJOcWSU-baQ==, therefore our css module style would never be applied.

Option 1: Increase specificity of classes generated by css modules

This would be the best solution IMO. Somehow increase the specificity by making a class have the same name, like:

1
2
3
.oiJX5Zp1+W8aJOcWSU-baQ==.oiJX5Zp1+W8aJOcWSU-baQ== {
  border-collapse: separate;
}

use webpack’s localIdentName/getLocalIdent

I tried using localIdentName and a custom function getLocalIdent

1
  localIdentName: '[hash:base64].[hash:base64]'

But the dot is escaped (https://github.com/webpack-contrib/css-loader/blob/5e6cf91fd3f0c8b5fb4b91197b98dc56abdef4bf/src/utils.js#L811).

use postcss

There are plugins that allow prefixing classes automatically, like postcss-prefix-selector The problem is that they would also prefix the rules from sanitize.css, defeating the purpose.

We could however, only process css modules in a different rule from “barebones” css.

That would work, except that it would generate 2 different css files. Which we would need to combine somehow when shipping to the user, since people expect importing a single css file, not two.

Although this would be my preferred method, unfortunately I could not make it work.

Option 2: Get rid of “global” css and use only css modules

From what I’ve seen, global css and css modules don’t really play well with each other. After all, the advantage of using css modules is to give up on the cascading part of CSS.

I am not opposed to this to be honest. However it’s not easy since code is kinda messy, it would require refactoring eg all <inputs> into an <Input> element that has all the sanitized css + custom css modules and so on. IE. feasible but complicated.

Option 3: Increase specificity of certain classes

Did you know you can create a class .mybutton.mybutton which will have higher specificity than just .mybutton? So the idea is to use that to our advantage, and increase the specificity of the styles that are being overwritten by sanitize.

Option 4: Prefix certain classes

A slight improvement of option #3

This is a lazy and ugly solution that works.

Basically the idea is to prefix certain classes with :global

1
2
3
4
5
:global(.myapp) {
  .table {
    border-collapse: separate;
  }
}

Which then generates a class such as .myapp .oiJX5Zp1+W8aJOcWSU-baQ==, which then override (since 2 classes > class + tag). That however breaks css-modules/no-undef-class lint rule, so it needs to be turned off, which really sucks.

The downside, of course, it’s that it’s super arbitrary when to add the prefix.

Option 5: Prefix with a tag instead of an element

Since a single class should have higher specificity than couple elements.

Trick is to create a custom html tag such as

1
2
<mytag>
</mytag>

Then scope sanitize.css rules under that tag

1
2
3
mytag table {
  border-collapse: collapse;
}

Which then should be overwritten by our css modules class:

1
2
3
.oiJX5Zp1+W8aJOcWSU-baQ== {
  border-collapse: separate;
}

Since classes have higher specificity.

Option 6: #5 but without an element

Suggested by my SO, instead of creating a custom element, we can use a “data-something” attribute

However, that is considered a pseudo class and therefore has the same specificity of a class.

Option 7: iframe/shadow DOM

Iframes are kinda cumbersome for this case, since we expect people to integrate their react code with our component.

Same for for shadow DOM.

Decided to not go with this solution.

Conclusion

Ended up going with #5, which is a clean enough solution.

There’s still a downside though: nothing is stopping the page’s css to overwrite my component’s css.

For illustration, if the page has a css such as

1
2
3
table {
  border-collapse: collapse !important;
}

That would of course overwrite my own.

But I guess that’s fine, because it allows people to use my component, but still customize it as they want.