CSS Modules in React

In keeping with the rest of React, how you style it is extremely flexible. There are many many varied solutions out there. Some focus on inline styles, some on JS generated styles, and some on keeping styles and JS separate. Most of which tend to resort to implementations that focus on CSS-in-JS (creating JS objects representing CSS rules). While it seems that CSS-in-JS is the future, it is still somewhat young, and as such, most of the libraries that provide CSS-in-JS are either missing, or have less than desirable abstractions for some useful CSS features. For example, pseudo-selectors and media queries. An alternative is CSS modules. CSS modules have the benefit of not only allowing for writing pure CSS, but also achieving component level style scoping.

With the introduction of CSS-in-JS and CSS modules out of the way, it should be said that this post will not focus solely on comparing the two. Instead, it will focus on providing an introduction to CSS modules, and a tutorial on how to get them going with Webpack in a new React app. So let’s get started!

CSS Modules

Before we jump into the tutorial, we’ll run through what CSS modules look like in implementation, and how they work. The basic idea of styling a React component with CSS modules is fairly simple. You create a CSS file for your component, then import it in to the component file, and use the styles as needed. See the following example.

/* Heading.css */

.heading {
    font-size: 36px;
    color: grey;
}
/* Heading.jsx */

import React from 'react'
import styles from './Heading.css' // Bring in stylesheet

export default = ({ children }) => (
    <h1 className={styles.heading}>{children}</h1>
)

The above example will, at compile time, take the stylesheet we imported, scope the class names, and inject the styles into the head. It will then apply those scoped class names to the designated elements.

Now that we know how CSS modules work, let’s get going on implementing them!

Utilizing CSS Modules in React

To start, let’s make a new directory for our app, then initialize it with Yarn.

mkdir react-css-modules && cd react-css-modules && yarn init -y

Next, we’ll want to bring our dependencies.

yarn add react react-dom serve

And now, our dev dependencies.

yarn add --dev webpack babel-core babel-loader style-loader css-loader babel-preset-react-app html-webpack-plugin babel-plugin-transform-runtime babel-plugin-transform-react-jsx

With all of our dependencies installed, let’s get to creating our app! The next step is to create a Webpack config. It should be noted that this tutorial will not provide the most complete Webpack config, with all of the bells and whistles. Instead, it will provide only what is needed to get a React app going with CSS modules for styling. For a more comprehensive guide to Webpack, please checkout our post on it.

Top level in your app’s directory, create a new file named webpack.config.js. In it, enter the following.

/* webpack.config.js */

const path = require('path')

module.exports = {
    devtool: 'eval',

    entry: [
        path.resolve('src/index.js'),
    ],

    output: {
        path: path.resolve('build'),
        filename: 'static/js/bundle.js',
        publicPath: '/',
    },

    ...
}

The previous code starts the base of our Webpack config by defining where our precompiled source code and our compiled output code will live in our project. We’ll build on to this by adding rules for JS/JSX and CSS files that specify what loaders need to run for each.

/* webpack.config.js */

module.exports = {
    ...
    
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                include: path.resolve('src'),
                loader: 'babel-loader',
            },

            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1,
                            modules: true, // Note. This prop enables CSS modules.
                        },
                    }
                ],
            },
        ],
    },
}

For JS/JSX files, we will run them through the babel loader. This will transpile our ES6 code and give us access to import/export. For CSS files, we will run them through the CSS loader, then the style loader. This will provide us the ability to import CSS files in our JS files. It will also scope the imported CSS to said files. This is what is needed for CSS modules.

Finishing out our Webpack config, we’ll add a definition for plugins and file extension resolvers.

/* webpack.config.js */

...
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    ...

    plugins: [
        new HtmlWebpackPlugin({
            inject: true,
            template: path.resolve('src/index.html'),
        }),
    ],

    resolve: {
        extensions: ['.js', '.jsx'],
    },
}

The sole plugin we’ll use keeps us from having to worry about injecting our built JS files into our application HTML entry point. The plugin, at compile time, will see what files Webpack has run through, and inject them into the index.html as needed. Very handy! With that, we have completed our Webpack config file. Here it is in its entirety.

/* webpack.config.js */

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    devtool: 'eval',

    entry: [
        path.resolve('src/index.js'),
    ],

    output: {
        path: path.resolve('build'),
        filename: 'static/js/bundle.js',
        publicPath: '/',
    },

    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                include: path.resolve('src'),
                loader: 'babel-loader',
            },

            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1,
                            modules: true,
                        },
                    },
                ],
            },
        ],
    },

    plugins: [
        new HtmlWebpackPlugin({
            inject: true,
            template: path.resolve('src/index.html'),
        }),
    ],
}

Next, we’ll create a .babelrc file. This purpose of this is to define the options, plugins, and presets we’ll be using in the compiling of our JS.

{
    "presets": [
        "react-app"
    ],
    "plugins": [
        "transform-react-jsx",
        "transform-runtime"
    ]
}

Enough with the configs! Let’s get to writing the main part of the app. Create a new directory named src. In it create an index.html. Like stated above, there will be no need to inject any application files here. That will all be taken care of. We just need to provide a basic template.

<!-- src/index.html -->

<!DOCTYPE html>
<html>
    <head>
        <title>CSS Modules in React</title>
    </head>

    <body>
        <div id="app"></div>
    </body>
</html>

#app will be the mount point for our application. In the same directory, create an index.js. This is where we’ll bootstrap React.

/* src/index.js */

import React from 'react'
import ReactDOM from 'react-dom'

const MainComponent = () => <h1>I am the main component!</h1>

ReactDOM.render(
    <MainComponent />,
    document.getElementById('app')
)

Now that we have something that we can run, let’s compile, then run this to see if we’ve gotten everything right so far.

In your package.json file, add the following.

/* package.json */

"scripts": {
  "pack": "NODE_ENV=development webpack"
},

This gives us a way to compile our app without needing to install Webpack globally. To build, we’ll run yarn run pack. Now, you should see a directory named build appear. We now have a built app. Now, we’ll start a server for it with the package “serve.” Back in our package.json, we’ll add another script.

/* package.json */

"scripts": {
  ...
  "serve": "server build"
},

Run yarn run serve, then in your browser, go to http://localhost:5000. Here you should see.

With our entry point and build process in order, we can continue. Let’s create a component that utilizes CSS modules. We’ll get the heading component we discussed above working. In your src directory, create a new directory named components. In it, create yet another directory for the component named Heading. Within our component directory, add an index.jsx .

/* src/components/Heading/index.jsx */

import React from 'react'

export default ({ children }) => (
    <h1>{children}</h1>
)

With this, we’ll alter our app entry point to bring in our new component.

/* src/index.js */

import React from 'react'
import ReactDOM from 'react-dom'

import Heading from './components/Heading'

const MainComponent = () => (
    <div>
        <Heading>Using the heading component!</Heading>
    </div>
)

ReactDOM.render(
    <MainComponent />,
    document.getElementById('app')
)

Rebuilding our app and navigating to http://localhost:5000 again should produce the following.

Now, let’s style the component. In the Heading directory, create a styles.css file.

/* src/components/Heading/styles.css */

.heading {
    font-family: Helvetica, Arial, sans-serif;
    font-size: 36px;
    color: grey;
    text-decoration: underline;
}

From here, we’ll import the stylesheet into our JSX component file and utilize the styles as needed. You’ll notice that when we import a style file, the classes in it become properties for us to use on our new style object.

/* src/components/Heading/index.jsx */

import React from 'react'
import styles from './styles.css'

export default ({ children }) => (
    <h1 className={styles.heading}>{children}</h1>
)

Recompiling should give us the following page.

Pretty cool, right? If you open your browser’s dev tools and inspect the heading element you should see something like this.

That scrambled string is the generated class name of the .heading rule. Because it is scrambled and unique, it is completely scoped to the heading component. There is no chance of style bleed here.

Before we call it a day, let’s add one more thing. While what we have is perfectly fine, it does have a few limitations. The first being that we have to use camelCase class names. Another limitation is the fact that we have to interact with an object to access our styles. With that noted, let’s improve this.

First run yarn add --dev babel-plugin-react-css-modules. What we just installed is a plugin for Babel that provides us with a little syntactical sugar that will clean up our component a little bit.

Now, we need to register the Babel plugin in our babel configuration. In your .babelrc, add the following to the plugin list.

/* .babelrc */

{
    ...
    "plugins": [
        ...
        "react-css-modules"
    ]
}

Next, we need to add a naming scheme for our CSS files to our Webpack config.

/* webpack.config.js */

module.exports = {
    ...
    module: {
        rules: [
            ...
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1,
                            modules: true,
                            localIdentName: '[path]___[name]__[local]___[hash:base64:5]', // Add naming scheme
                        },
                    },
                ],
            },
        ],
    },
    ...
}

That’s all the configuration we need to do. Now let’s alter our component to use the new syntax.

/* src/components/Heading/index.jsx */

import React from 'react'
import './styles.css' // We are able to forego constructing a style object

export default ({ children }) => ( // We use styleName instead of className
    <h1 styleName="heading">{children}</h1>
)

Recompiling and running this in the browser will look the same, but we made our development process a little easier.

In successfully creating a React app that utilized CSS modules, we’ll conclude this post. There is still much we can do to improve this app. If you’re feeling confident, go ahead and add in a CSS preprocessor to make our CSS modules even more useful and easy to develop.

Leave a Reply

Your email address will not be published. Required fields are marked *