Styling React Components with CSS Modules

CSS Modules are one of the most common ways of styling React applications. Because they consist of plain CSS, they are easily understandable.

CSS Modules achieves what is called weak scoping. This means that, although styles are scoped to their respective components, they can still be overridden by external styles. Depending on how you use this property, this is what makes CSS Modules so incredibly effective.

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 JavaScript object which is 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';

const Component: React.FC = ({ children }) => {
    return <Component className={styles.component}>{children}</Component>;
};

export default Component;
index.tsx
import Component from './Component';

const ParentComponent: React.VFC = () => {
    return <Component>Some Content</Component>;
};

export default ParentComponent;

That's all well and good. But what if you want to be able to adjust the styles from outside the component? That starts 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 indexing. 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'
}

const Component: React.FC<ComponentProps> = ({ children, variant }) => {
    return (
        <Component className={styles[`variant-${variant}`]}>
            {children}
        </Component>
    );
};

export default Component;
index.tsx
import Component from './Component';

const ParentComponent: React.VFC = () => {
    return <Component variant="1">Some Content</Component>;
};

export default ParentComponent;

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 concatenation — the joining of two strings together into one large string. 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'
}

const Component: React.FC<ComponentProps> = ({
    children,
    property1,
    property2
}) => {
    return (
        <Component
            className={[
                styles[`property1-${property1}`],
                styles[`property1-${property2}`],
            ].join(' ')}
        >
            {children}
        </Component>
    );
};

export default Component;
index.tsx
import Component from './Component';

const ParentComponent: React.VFC = () => {
    return <Component property1="2" property2="3">Some Content</Component>;
};

export default ParentComponent;

If distilling all component styling into props feels too limiting, there's a solution for that too. 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
}

const Component: React.FC<ComponentProps> = ({
    children,
    property1,
    property2,
    className,
}) => {
    return (
        <Component
            className={[
                styles[`property1-${property1}`],
                styles[`property1-${property2}`],
                className,
            ].join(' ')}
        >
            {children}
        </Component>
    );
};

export default Component;
index.tsx
import styles from './ParentComponent.module.css'
import Component from './Component';

const ParentComponent: React.VFC = () => {
    return (
        <Component property1="2" property2="3" className={styles.component}>
            Some Content
        </Component>
    );
};

export default ParentComponent;

One thing to look out for is the whitespace as the argument of .join(). Without it, the class names would just be concatenated into one long name that the browser cannot recognise. Adding the space avoids this by separating the class names into recognisable tokens.

Notice how have full control over how fine-grain the control should be? The first example only had one control point (the variant), while the second had a prop for each individual style property (property1 and property2). This gives us a lot of flexibility in terms of styling our components, which is especially useful when architecting a design system.

Last updated: 10/5/2021

Edit on GitHub