Overriding global classes in CSS modules
April 07, 2020
This is a fairly long-winded explanation of how we can do things like add a required asterisk to a column header on a Fabric React Details List using CSS modules.
TL;DR
_4.required span:global(.ms-DetailsHeader-cellName)::after {_4 content: " *";_4 color: darkred;_4}
Long Version
There are a few options when styling components in reactjs, but the one I keep coming back to is using sass in conjunction with CSS modules. Together we get the power of a CSS preprocessor, combined with locally scoped, modular imports that provide an object with mappings to the classes.
With vanilla CSS, we create a .css file and import it anywhere in the application and use it by referencing a globally scoped class name as a string literal:
_3.myClass {_3 display: block;_3}
This is fine for small applications, but can quickly lead to naming conflicts in larger applications. Not only that, but we need to remember our class names and reference them as string literals whenever we want to use them. We also don't have access to the more advanced features of a CSS preprocessor, such as importing variables and nesting.
Sass
Adding sass to our application is quite quick and straight forward:
_2# install the dependency_2yarn add node-sass
_8// we can now update our sass file to use advanced features_8$bgColor: #0008;_8.myClass {_8 display: block;_8 &:hover {_8 background-color: blue;_8 }_8}
CSS Module Files
With the above methods our CSS classes are still in the global scope, which means we could run into naming conflicts. We also have to refer to our class names as string literals.
To help with this we can use CSS modules to restrict our classes to a local scope and output a module that contains a mapping object that allows us to reference our classes as object properties.
To do this, we need to rename our file once more to include .module in the name and then import it as a js/ts module. The imported object properties can now be accessed:
This makes it a lot cleaner to keep track of and organise.
The issue
When using webpack, once the CSS module has been compiled and is in use on the web, the class names get replaced with a global unique name made up of the class name and a hash. This is what keeps the classes locally scoped.
This means any class name that is compiled from within a module will be replaced with a hashed version.
The issue here is that if we want to override a class that already exists on the page (such as in a SharePoint page in SPFx, or styling an imported library), the hashed version will not select the target.
_7// our css module with a globally scoped css class referenced_7.myClass {_7 display: block;_7}_7.ms-Button {_7 display: none;_7}
_16<style>_16 .myClass_a6b95e {_16 display: block;_16 }_16 .ms-Button_a6b95e {_16 display: none;_16 }_16</style>_16_16<div class="myClass_a6b95e"></div>_16_16..._16_16<button class="ms-Button">_16 Still Visible_16</button>
Previously, I had been adding these classes that I wanted to override into a normal SCSS file, such as globals.scss
and importing this at the root of the application. However, this doesn't work if we want to combine it with classes or variables that exist in our modules.
Luckily, CSS modules provide us with a way to access these globally scoped classes from within our modules.
:global()
The :global()
operator switches to the global scope within our module file. This means we can build selectors that reference globally scoped classes from our module. Likewise, the :local()
operator switches back to local scope if needed.
_7// our styles then become_7.myClass {_7 display: block;_7}_7:global(.ms-Button) {_7 display: none;_7}
_17<style>_17 .myClass_a6b95e {_17 display: block;_17 }_17 .ms-Button {_17 display: none;_17 }_17</style>_17_17<div class="myClass_a6b95e"></div>_17_17..._17<!--REMOVED FROM DOM_17 <button class="ms-Button">_17 Still Visible_17 </button>_17-->
Example
The below example shows how to add a required asterisk to a Fabric React Details List column header:
_4.required span:global(.ms-DetailsHeader-cellName)::after {_4 content: " *";_4 color: darkred;_4}
We can then apply our .required
className to a column and the selector will traverse to the globally scoped span with Fabric css class, and create an ::after
element on it.