How to setup and use CSS/modules in React with Webpack
This post will guide you through setting up webpack for importing both globally scoped CSS modules and locally scoped CSS modules and how to correctly import these in to your react classes using typescript.
Probably in most occasions your react project will be using globally scope CSS files. That is the same CSS definitions will be shared throughout your project. This is obviously useful, however, there is common need to ensure only local react components have access to CSS definitions, i.e no chance of CSS class names coinciding with other CSS definitions. This is where CSS Modules come in to play.
Globally scoped CSS class files are imported for example as follows
import '../semantic/dist/semantic.min.css';
Where as locally scoped CSS Modules are used like the following:
import styles from './niceButton.module.css';
Lets say the css file looks as follow:
// niceButton.module.css.mainBit{
font-family: 'Exo', sans-serif !important;
box-sizing: border-box;
}.otherStuff{
font-family: 'Exo', sans-serif !important;
box-sizing: border-box;
}.more-stuff{
font-family: 'Exo', sans-serif !important;
box-sizing: border-box;
}
To use such a CSS module we might do the following:
const buttonClasses = styles.mainBit + " " + styles.moreStuff;return (<button className={buttonClasses} onClick={this.handleClick}></button>);
For those using typescript you must add a .d.ts file, in this case we would create ‘niceButton.module.css.d.ts’
// niceButton.module.css.d.tsexport const mainBit: string;
export const otherStuff: string;
export const moreStuff: string;
The styles object will contain string properties which match the classes in the imported CSS file. If you were to look at the value of each of the properties based on how we will set it up in this guide then the following boolean comparions below give a good idea of what is going on:
styles.mainBit === "niceButton-module_mainBit_GIFda"
styles.otherStuff === "niceButton-module_otherStuff_FknN"
styles.moreStuff === "niceButton-module_moreStuff_QJxiu"
styles["moreStuff"] === "niceButton-module_moreStuff_QJxiu"
styles["more-stuff"] === "niceButton-module_moreStuff_QJxiu"
You will notice the class ‘more-stuff’ contains a dash character. Javascript does not support this character for variable names, so these will be converted to camel case propertie names and similarly the prefix for the propery value will also be camel cased.
As you can see you can access the classes via a normal property or by using either the original name for the class or the camel cased name which is also accessible by the property on the object.
i.e the following two boolean comparions are all true:
styles.moreStuff === styles["more-stuff"]styles["moreStuff"] === styles["more-stuff"]
As you can see under the hood the CSS class names are all unique and will never clash with the rest of the application making CSS modules and their locally scope nature very useful.
So how to set this up using Webpack?
Firstly it should be reminded that we want our project to allow for both CSS shared classes as well as locally scoped CSS modules.
To do this we will follow a very simple naming convention
- CSS Modules: [name].module.css
- Shared global CSS files: [name].css
And for typescript users for the case of CSS modules you must create the css.d.ts with the following standard naming convention:
- Typescript declaration: [name].module.css.d.ts
Our webpack setup will need to work for all css, sass and scss files.
Your webpack.config.js file will need to contain the following at the top:
// For our css modules these will be locally scopedconst CSSModuleLoader = {
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]_[local]_[hash:base64:5]',
importLoaders: 2,
camelCase:true,
sourceMap: false, // turned off as causes delay
}
}// For our normal CSS files we would like them globally scopedconst CSSLoader = {
loader: 'css-loader',
options: {
modules: "global",
importLoaders: 2,
camelCase:true,
sourceMap: false, // turned off as causes delay
}
}// Our PostCSSLoader
const autoprefixer = require('autoprefixer')
const PostCSSLoader = {
loader: 'postcss-loader',
options: {
ident: 'postcss',
sourceMap: false, // turned off as causes delay
plugins: () => [
autoprefixer({
browsers: ['>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9']
})
]
}
}// Standard style loader (prod and dev covered here)
const MiniCssExtractPlugin = require("mini-css-extract-plugin");const devMode = process.env.NODE_ENV !== 'production';
const styleLoader = devMode ? 'style-loader' : MiniCssExtractPlugin.loader;
The under your module/rules array:
module: {
rules:
[
{
test: /\.(sa|sc|c)ss$/,
exclude: /\.module\.(sa|sc|c)ss$/,
use: [styleLoader, CSSLoader, PostCSSLoader, "sass-loader"]
},
{
test: /\.module\.(sa|sc|c)ss$/,
use: [styleLoader, CSSModuleLoader, PostCSSLoader, "sass-loader"]
}
]
}
The first block will match on all the shared global css files, i.e those which do not contain the .module and apply our defined ‘CSSLoader’
The second block will match the local css modules and use our defined ‘CSSModuleLoader’
The main difference as you can see is the option ‘modules’ is set to ‘true’ for our CSSModuleLoader which will apply our unique naming convention and as well facilitate the import of the module in our javascript or typescript at runtime using the syntax described earlier:
import styles from './niceButton.module.css';
That is it!