CSS Modules
Description
A CSS Module is a CSS file where class names are locally scoped. Though the name may make CSS Modules sound like an official spec that extends the CSS language, this is not the case — CSS Modules were invented by members of the community and must be compiled to normal CSS before being served on the web.
To begin using CSS Modules, create a file with the .module.css
extension and add a class name selector:
.appContainer {
background-color: #fafafa;
font-family: sans-serif;
}
Then import the CSS module in your JavaScript code and attach the class name to an element:
import styles from './App.module.css';
export function App() {
return <div className={styles.appContainer}>Content here</div>;
}
Some things to note about the above example:
- We used a camelCase class name — camel case is preferable when using CSS Modules since the class names need to be accessed from JavaScript.
- The CSS Module for
App.jsx
is calledApp.module.css
. This is a common pattern when using CSS Modules: for each[component].jsx
, you create a[component].module.css
file that contains the styles for that component.
Purpose and Use Cases
A common pitfall when using plain CSS (with or without a preprocessor) is that same class name may be accidentally used for two different purposes. For example, imagine page1.css
defines styles for an error alert like this:
.error {
display: block;
padding: 1rem;
background-color: #faf38f;
border-color: 1px solid red;
font-weight: bold;
}
You are working on page 2 and wish to add a small bottom margin to an error label. If you don't remember that .error
styles are also defined in page1.css
, you may accidentally reuse the error
class name and write something like this:
.error {
margin-bottom: 0.5rem;
}
If page1.css
and page2.css
are compiled into a single CSS bundle which is included in the <head>
of your single page app's HTML, page 2's simple error label will end up with a large padding, a background color, and a border, since these styles were defined in page1.css
. That isn't what we wanted! We really want page1.css
to only apply to page 1, and page2.css
to only apply to page 2.
CSS Modules fixes this problem by making all class names locally scoped. It does this by transforming your class names at build time. In the example above, the module loader would transform the class names like this:
error (from page1.css) ➙ _src_page1_module__error
error (from page2.css) ➙ _src_page2_module__error
While the exact transformation algorithm used may vary, the end result is the same: there will never be a collision between two CSS classes defined in different files.
When you import a CSS Module from a JavaScript file, you get an object which maps the human-readable class names to transformed class names. For example, this code,
import styles from 'page1.module.css';
console.log(styles);
will print
{ error: '_src_page1_module__error' }
to the console.
Tradeoffs
There are no major tradeoffs when using CSS Modules instead of plain CSS, though you need to make sure you are using a bundler which supports CSS Modules. CSS Modules are supported out of the box when using Webpack's css-loader
, Create React App, or Next.js.
CSS Modules are potentially less convenient than plain CSS if you want to attach styles to the same class name in different files. You can achieve this behavior by using the :global
directive as described here.
CSS Modules can be seen as a middle ground between plain CSS and a full-fledged CSS-in-JS library. Like plain CSS, CSS Modules have no runtime overhead since they are transformed to plain CSS at compile time. Like CSS-in-JS libraries, CSS Modules offer locally-scoped styles. That said, CSS-in-JS provides tighter integration between your CSS and JavaScript code:
- With CSS-in-JS, you can define a React component and its styles in a single
.jsx
file, whereas with CSS Modules, you need both a.jsx
file and a.module.css
file. - Styles defined in via CSS-in-JS have full access to JavaScript variables (e.g. color constants), while CSS Modules do not.
When Should I Consider Using This?
- You are working on a medium or large application, since class name collisions are more likely to occur in larger codebases.
- You want locally-scoped class names, without the larger bundle size and runtime performance costs that come with using a CSS-in-JS library.
- You are using a CSS preprocessor like Sass. Preprocessors and CSS modules can be used together.
Further Information
Example
This example shows how to create the following component using React and CSS Modules.
In your .jsx
file, import from the .module.css
file, which we'll add in a bit. Then write the JSX code and use the styles
object when setting the className
for each element.
import styles from './Card.module.css';
export function Card() {
return (
<div className={styles.card}>
<div className={styles.cardContent}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</div>
<button>OK</button>
</div>
);
}
Next, we add the CSS Module:
.card {
border: 1px solid #ccc;
border-radius: 0.5rem;
padding: 1rem;
max-width: 200px;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.card .cardContent {
margin-bottom: 1rem;
}
button {
border: 0;
background-color: #0d6efd;
color: white;
padding: 0.5rem;
font-size: 1rem;
border-radius: 0.25rem;
width: 100%;
}
button:hover {
background-color: #025ce3;
}
And that's it! If properly configured, your bundler will transform the class names in the CSS Module so that your styles are locally scoped.