Styling with CSS Modules
Nico Bachner
Posted on September 10, 2021
CSS Modules are one of the most common methods to style React applications. Because they consist of plain CSS, they are easily understandable.
CSS Modules achieve what is referred to as weak scoping. Although styles are scoped to their respective components, they can be overridden by external styles. Depending on how you use this property, this is what makes CSS Modules so useful.
The usual approach to styling components using CSS Modules looks something like this:
The CSS is written in a .module.css
file, and target class names.
/* Component.module.css */
.component {
property-1: 'value-1';
property-2: 'value-2';
}
These classes are then imported as a JS object – usually given the name styles
. The imported object is has the class names defined in the .module.css
file as keys.
// Component.tsx
import styles from './Component.module.css'
export const Component: React.FC = ({ children }) => (
<div className={styles.component}>{children}</div>
)
// index.tsx
import { Component } from './Component'
export const ParentComponent: React.VFC = () => (
<Component>Some Content</Component>
)
That's all well and good. But what if you want to be able to adjust the styles from outside the component? Now things start to get tricky.
Fortunately, CSS Module imports are regular JavaScript objects. That means we can manipulate them as we usually would. One possible manipulation that is especially useful for us is string indexing1. It allows us to choose which style to apply based on a string input.
If we apply string indexing to the previous example, we get the following:
/* Component.module.css */
.variant-1 {
property-1: 'value-1-1';
property-2: 'value-2-1';
}
.variant-2 {
property-1: 'value-1-2';
property-2: 'value-2-2';
}
.variant-3 {
property-1: 'value-1-3';
property-2: 'value-2-3';
}
// Component.tsx
import styles from './Component.module.css'
type ComponentProps = {
variant: '1' | '2' | '3'
}
export const Component: React.FC<ComponentProps> = ({ children, variant }) => (
<div className={styles[`variant-${variant}`]}>{children}</div>
)
// index.tsx
import { Component } from './Component'
export const ParentComponent: React.VFC = () => (
<Component variant="1">Some Content</Component>
)
We now have the ability to change the styling of the component through one of its props.
But why stop there? What about styling through multiple props?
It is possible, and can be achieved through string concatenation2. Applied to our example, it looks like so:
/* Component.module.css */
.property1-1 {
property-1: 'value-1-1';
}
.property2-1 {
property-2: 'value-2-1';
}
.property1-2 {
property-1: 'value-1-2';
}
.property2-2 {
property-2: 'value-2-2';
}
.property1-3 {
property-1: 'value-1-3';
}
.property2-3 {
property-2: 'value-2-3';
}
// Component.tsx
import styles from './Component.module.css'
type ComponentProps = {
property1: '1' | '2' | '3'
property2: '1' | '2' | '3'
}
export const Component: React.FC<ComponentProps> = ({
children,
property1,
property2,
}) => (
<div
className={[
styles[`property1-${property1}`],
styles[`property1-${property2}`],
].join(' ')}
>
{children}
</div>
)
// index.tsx
import { Component } from './Component'
export const ParentComponent: React.VFC = () => (
<Component property1="2" property2="3">
Some Content
</Component>
)
If distilling all component styling into props feels too limiting, there exists a solution. It is possible to give a component custom styles by passing it a className
prop:
/* Component.module.css */
/* omitted for brevity — same as the above example */
// Component.tsx
import styles from './Component.module.css'
type ComponentProps = {
property1: '1' | '2' | '3'
property2: '1' | '2' | '3'
className?: string
}
export const Component: React.FC<ComponentProps> = ({
children,
property1,
property2,
className,
}) => (
<div
className={[
styles[`property1-${property1}`],
styles[`property1-${property2}`],
className,
].join(' ')}
>
{children}
</div>
)
// index.tsx
import styles from './ParentComponent.module.css'
import { Component } from './Component'
export const ParentComponent: React.VFC = () => (
<Component property1="2" property2="3" className={styles.component}>
Some Content
</Component>
)
One thing to look out for is the whitespace as the argument of .join()
. Without it, the class names would be concatenated into one long name which the browser cannot recognise. Adding the space separates the class names into recognisable tokens.
Notice how have full control over how fine-grain the control should be? While the first example only had one prop (the variant
), the second had a prop for each individual style property (property1
and property2
). The ability to choose the level of abstraction can be useful in many situations, such as architecting a design system.
Posted on September 10, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.