In Prod

← Back to Posts

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
}

index.tsx

_3
import "./app.css";
_3
_3
<Component className={"myClass"} />

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
_2
yarn add node-sass

index.tsx

_5
// change the file type from .css to .scss
_5
// then import and use as usual
_5
import "./app.scss";
_5
_5
<Component className={"myClass"} />


_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:

app.tsx

_4
// app.scss becomes app.module.scss
_4
import styles from "./app.module.scss";
_4
_4
<Component className={styles.myClass} />

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.


Andrew McMahon
These are a few of my insignificant productions
by Andrew McMahon.