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.

Normally, styling components with CSS modules looks something like this:

Component.module.css
.component {
    property-1: 'value-1';
    property-2: 'value-2';
}
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 Home: React.VFC = () => {
    return <Component>Some Content</Component>;
};

export default Home;

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 Home: React.VFC = () => {
    return <Component variant="1">Some Content</Component>;
};

export default Home;

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 Home: React.VFC = () => {
    return <Component property1="2" property2="3">Some Content</Component>;
};

export default Home;

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 separates the class names into recognisable tokens.

Notice how have full control over how fine-grain the control should be? The previous example only had one control point (the variant), but it could just as well have a prop for each individual style property. This flexibility is especially useful when architecting a design system.

Last updated: 9/22/2021

Edit on GitHub →