Setup CSS Module With React, Webpack, and SCSS
react
scss
css
webpack
As we often compare Angular with React, one of the thing that I like about Angular is the style encapsulation that we assign to components. Here is how we can achieve that with React.
Style Encapsulation makes the styles only applied to the local elements of a component and does not become global, even though the final bundle is still a single compiled styles.css
file. (Well, in reality, all the styles in styles.css
are still global. We simply append a base64 hash specific for each component to each of the selectors for that component to create the scope.)
Setup With CRA
If you are using CRA 2.0+, all you need to do is to name the local style file to [ComponentName].module.css
. This convention is recognized by create-react-app as a modular css.
Manual Setup
On the over hand, here is the setup if not using CRA. Assuming that you have set up your React app using Babel and Webpack, we just need to cusomtize css-loader
for this setup. CSS Module is already supported by Babel’s css-loader
. We just need to hook it up with sass-loader
in webpack.config.js
.
const CSSExtract = new MiniCssExtractPlugin() // Default output bundle filename: main.css
const scssLoader = {
test: /\.s?css$/,
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
sourceMap: true // Mapping to original scss source
}
}, {
loader: 'css-loader',
options: {
sourceMap: true, // Mapping to original scss source
modules: {
mode: 'local', // Enable local component styles scoping
localIdentName: '[local]--[hash:base64:7]' // [local] is placeholder for the CSS selector: e.g. container--b4ku7nb
}
}
}, {
loader: 'sass-loader',
options: {
sourceMap: true // Mapping to original scss source
}
}]
}
module.exports = {
mode: 'production',
//... Other setup lines here
module: {
rules: [
babelLoader,
scssLoader,
fileLoader
]
},
plugins: [
CSSExtract
]
}
There are multiple options that can be used to configure CSS Module with css-loader
. Check the documentation for more info.
How it works
Now, here is the folder structure of components. Let’s look at a custom TextInput
component as an example.
TextInput/
|- index.js
|- styles.scss
|- TextInput.test.js
styles.scss
can contain the styles specific for this component. Also, keep in mind that all css rules are valid scss rules. So using a SCSS setup is much flexible than simpy using CSS.
/* --- styles.scss --- */
// SCSS Variables
$fontSize: 16;
$fontWeight: 100;
$fontFamily: Arial, Helvetica, sans-serif;
// Element Styles
.container {
flex: 1;
flex-direction: 'row';
align-items: 'center';
}
.label {
font-family: $fontFamily;
font-size: $fontSize;
font-weight: $fontWeight;
padding-left: 20;
flex: 1;
}
.input {
color: '#000';
padding-right: 5;
padding-left: 5;
font-size: $fontSize;
flex: 3;
}
Now, styles.scss
can be imported directly into index.js
as any other ES6 modules and styles can be applied as if they were props off that object. It is also possible to apply destructuring here but be careful not to have variable collision. (e.g. label
vs styles.label
)
/* --- index.js --- */
import { container }, styles from './styles.scss'
class TextInput extends Component {
render() {
const { type, label, placeholder } = this.props
return (
<div className={container}>
{
label &&
<label htmlFor='text-input' className={styles.label}>
{label}
</label>
}
<input
type={type}
placeholder={placeholder}
ref={this.textInputRef}
name='text-input'
className={styles.input}
value={this.state.value}
onChange={e => this.handleChangeValueLocal(this.textInputRef.current.value)}
/>
</div>
)
}
}