Skip to main content

CSS Modules (Part 2): Composition and Global Styles #28

📖 Introduction

In the previous article, we discovered how CSS Modules solve the problem of global scope by making styles local by default. This is a huge step forward, but what if we want to share styles between classes or intentionally create a global style?

This article dives into the more advanced features of CSS Modules. We'll explore composition, a powerful feature for inheriting and combining styles, and learn how to escape the local scope when you truly need a global class.


📚 Prerequisites

Before we begin, please ensure you have a solid grasp of the following concepts:

  • CSS Modules Basics: You must understand how to create and use a .module.css file and apply its styles in a React component.
  • CSS Principles: A basic understanding of CSS inheritance and specificity.

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • The composes Keyword: How to share styles between classes within the same file.
  • Composition from Other Files: How to inherit styles from a different CSS Module.
  • Applying Multiple Classes: Techniques for combining several composed and local classes on a single element.
  • Escaping the Local Scope: How to use the :global keyword to create a standard, global CSS class from within a CSS Module.

🧠 Section 1: The Core Concept: Style Composition

A core principle of good CSS is to keep it DRY (Don't Repeat Yourself). Composition is the feature in CSS Modules that allows you to achieve this. It lets one class inherit all the styles from another class.

The composes keyword is the key to this feature. It tells the CSS Modules processor that a class should include all the styles from one or more other classes.


💻 Section 2: Composition in Practice

Let's explore the different ways to use composes.

2.1 - Composition Within the Same File

This is useful for creating variations of a base component style. Imagine a base .btn class and several color variations.

Button.module.css

/* Define a base class */
.btn {
padding: 10px 15px;
border: none;
border-radius: 5px;
font-size: 1rem;
}

/* Create a variation that composes the base class */
.primary {
composes: btn; /* Inherits all styles from .btn */
background-color: blue;
color: white;
}

/* Create another variation */
.secondary {
composes: btn; /* Also inherits all styles from .btn */
background-color: grey;
color: white;
}

Button.jsx

import React from 'react';
import styles from './Button.module.css';

function Button() {
return (
<div>
<button className={styles.primary}>Primary</button>
<button className={styles.secondary}>Secondary</button>
</div>
);
}

How it Works: When you use className={styles.primary}, the rendered HTML element will have two classes applied: the unique generated class for .btn and the unique generated class for .primary. The composes rule effectively merges the styles, with the properties in the local class (.primary) overriding any inherited properties if they conflict.

2.2 - Composition from Another File

This is powerful for creating a shared library of utility styles. Let's create a separate file for common layout styles.

utils.module.css

.flexCenter {
display: flex;
justify-content: center;
align-items: center;
}

Card.module.css

.card {
/* Inherit styles from another file */
composes: flexCenter from './utils.module.css';
padding: 20px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}

Card.jsx

import React from 'react';
import styles from './Card.module.css';

function Card({ children }) {
return <div className={styles.card}>{children}</div>;
}

Now, the Card component will have all the flexbox properties from utils.module.css applied to it, in addition to its own padding and box-shadow. This is an excellent way to create a reusable set of utility classes.


🛠️ Section 3: Applying Multiple Classes

What if you want to apply a base class and a modifier class dynamically? You can't use composes for dynamic logic in your CSS. Instead, you combine the classes in your JavaScript.

A simple way is to use a template literal.

// Button.jsx
import React from 'react';
import styles from './Button.module.css';

// Assume Button.module.css has .btn and .primary classes (without composes)
function Button({ isPrimary }) {
const classNames = `${styles.btn} ${isPrimary ? styles.primary : ''}`;

return <button className={classNames}>Click Me</button>;
}

This works, but can get messy. A very popular and recommended utility for this is the classnames library.

Using the classnames library: First, install it: npm install classnames

// Button.jsx with classnames library
import React from 'react';
import styles from './Button.module.css';
import cn from 'classnames';

function Button({ isPrimary, isActive }) {
const classNames = cn(
styles.btn, // Always apply the base .btn class
{
[styles.primary]: isPrimary, // Apply .primary only if isPrimary is true
[styles.active]: isActive, // Apply .active only if isActive is true
}
);

return <button className={classNames}>Click Me</button>;
}

The classnames library is the standard and cleanest way to conditionally apply multiple classes in React.


🚀 Section 4: Escaping the Local Scope with :global

Sometimes, you need to create a global class from within a CSS Module. This is common when you need to style a third-party component, or if you want to define a style that applies to the body or html tag.

The :global keyword allows you to do this.

GlobalStyles.module.css

/* This class will be local and hashed */
.myLocalClass {
color: blue;
}

/* This class will be global and NOT hashed */
:global(.my-global-class) {
padding: 20px;
font-weight: bold;
}

/* You can also style global elements */
:global(body) {
background-color: #f0f0f0;
}

When you import this file, only myLocalClass will be a property on the styles object. The .my-global-class and body styles will be injected into the document as-is, without hashing, and will apply globally.


💡 Conclusion & Key Takeaways

You've now explored the advanced capabilities of CSS Modules, moving beyond simple local styles to create sophisticated, maintainable, and reusable styling systems.

Let's summarize the key takeaways:

  • composes for Inheritance: Use composes: className; to inherit styles from another class, promoting DRY principles.
  • Composition is Powerful: You can compose from classes in the same file or from other .module.css files.
  • Use classnames for Dynamic Classes: For applying classes conditionally in your JSX, the classnames library is the industry standard.
  • :global for Escaping: Use the :global(...) syntax when you intentionally need to create a global style that won't be hashed.

Challenge Yourself: Create a typography.module.css file with base classes like .heading and .subheading. Then, create a Card.module.css file. In Card.module.css, create a .cardTitle class that composes the .heading class from typography.module.css and adds a unique color.


➡️ Next Steps

This concludes our two-part look at CSS Modules. You now have a robust tool for creating scalable and conflict-free CSS. In our next article, we'll look at another popular styling paradigm: "Introduction to CSS-in-JS (Part 1): The concept and benefits. A look at styled-components."

Thank you for your dedication. Stay curious, and happy coding!


glossary

  • Composition: In CSS Modules, the ability for one class to inherit the styles of another class using the composes keyword.
  • classnames: A popular and simple JavaScript utility for conditionally joining class names together.
  • :global: A keyword in CSS Modules used to declare that a specific class or selector should not be locally scoped and should instead be treated as a global style.

Further Reading