DEV Community

Cover image for How to write type-safe CSS Modules
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

How to write type-safe CSS Modules

Written by Alain Perkaz✏️

One of the benefits of using TypeScript is that it significantly reduces the occurrence of specific bugs, like typos; it even makes it easier to access prototype methods and perform refactoring. Bugs caught at compile time make for more uptime, happier customers, and less on-call stress for developers.

With TypeScript, it's easy to type our application’s business logic and control flows, but what if we could make our CSS classes safe too? Having the correct CSS class names in place ensures that the intended styles are applied to a given component, preventing the styles from being misplaced due to typography errors.

In this article, we’ll discuss what CSS Modules are, explore their developer experience shortcomings, and learn how to address them by using automation with TypeScript. Let’s get started!

Jump ahead:

What are CSS Modules?

CSS Modules provide an approach to writing modular and scoped CSS styles in modern web apps. These styles are specific to your application's particular component or module. You can write CSS Modules by using regular CSS.

At build time, with Vite or other similar tools, the CSS Modules generate unique class names for each class defined in the CSS files. The generated class names are then used in JavaScript to refer to the CSS, thereby making the CSS modular and reusable without class name conflicts or unnecessary duplications.

At the time of writing, CSS class names are no longer global, solving many of the pains that methodologies like BEM were designed to solve, but without the manual effort. However, following BEM within CSS Modules can still be beneficial depending on the use case.

Adding CSS Modules to your project

If you want to use CSS Modules in your next TypeScript app, you have several options.

Modern build tools like Vite and Snowpack support CSS Modules out of the box, but you may need to include some minor configurations if you’re using webpack.

Once the build setup is done, you can add CSS files with the module.css extension following the CSS Modules convention:

// button.module.css
.green {
    background-color: 'green';
}

.blue {
    background-color: 'blue';
}

.red {
    background-color: 'red';
}
Enter fullscreen mode Exit fullscreen mode

To apply those styles and leverage the benefits mentioned above, we should import the CSS Module from a TypeScript file and bind the HTML. Keep in mind that the example below is written in React, but the syntax is very similar to other UI libraries:

// Component.tsx
import styles from './button.module.css'

const Component = () => (
    <>
        <button className={styles.green}>I am green!</button>
        <button className={styles.blue}>I am blue!</button>
        <button className={styles.red}>I am red!</button>
    </>
)
Enter fullscreen mode Exit fullscreen mode

If you run the code above locally, you’ll notice that the returned styles are not typed restrictively. Instead, they are typed as any. Additionally, the TypeScript compiler won’t notify you if the class name doesn’t exist. Let's discuss what that means for the developer in detail.

Developer experience improvements

CSS Modules are a great tool, but since class names are generated at runtime and change between builds, it’s hard to use them in a type-safe way.

You could manually create types for each CSS Module using TypeScript definition files, but updating them is tedious. Let's suppose that a class name is added or removed from the CSS Module. In that case, the types must be manually updated, otherwise, the type safety won't work as expected.

For the example above, the typings would be as follows:

// button.module.css.d.ts - 👈 the CSS Module types
declare const styles: {
  readonly green: string;
  readonly blue: string;
  readonly red: string;
};
export default styles;
Enter fullscreen mode Exit fullscreen mode

These types will work well until we modify the related CSS Module. Once we modify it, we’ll have to update the typings. If we forget to update the typings manually, some nasty UI bugs might appear:

// button.module.css
.green {
    background-color: 'green';
}
.blue { 
    background-color: 'blue';
}

/* 👈 the `red` classname is removed */
Enter fullscreen mode Exit fullscreen mode

We forgot to modify the related typings file:

// button.module.css.d.ts
declare const styles: {
  readonly green: string;
  readonly blue: string;
  readonly red: string; // 👈 we forgot to update the types! 😔
};
export default styles;

// Component.tsx
import styles from './button.module.css'

const Component = () => (
    <>
        <button className={styles.green}>I am green!</button>
        <button className={styles.blue}>I am blue!</button>
        {/* 👇 `red` does not exist, but since we forgot to update the types, the compiler wont fail! */}
        <button className={styles.red}>I am red!</button>
    </>
)
Enter fullscreen mode Exit fullscreen mode

The situation shown in this example might not seem relevant, but as the codebase and number of contributors grow, this repetitive and error-prone process will hinder trust in the type-system. Referencing non-existent or mistyped CSS classes won't style the HTML as expected, which can quickly snowball into developers losing trust in the tooling. Let's learn how to automate it!

Automatic typings

In this case, the automation solution is straightforward. We’ll generate the types automatically instead of manually, and we’ll provide a script to verify that the generated types are up-to-date to avoid incorrect CSS Module typings leaking into the compilation step.

There are multiple ways to achieve this. For example, we could build a CSS to TypeScript definition extractor. However, to avoid re-inventing the wheel, we’ll leverage the open source package typed-css-modules. Let’s get to it!

Install the package in your project with npm i typed-css-modules, then add the type-generation to your main development script in the package.json scripts:

"watch": "vite & tcm --watch .",
Enter fullscreen mode Exit fullscreen mode

Add the check for up-to-date types. If the generated types are not correct in the package.json scripts, it will fail:

"check:up-to-date-types": "tcm --listDifferent .",
Enter fullscreen mode Exit fullscreen mode

With these two scripts, it’s now possible to automatically keep the CSS Module type definitions in sync and check if the types are kept up to date.

Depending on the project, you may prefer to run these scripts locally or in a server, perhaps as a part of your CI pipeline. To round out the example, we’ll describe how to run them as a Git Hook using husky:

Install and set up the Git Hook runner with npx husky-init && npm install. To set up a pre-commit Hook to run the CSS Module type checking before every commit, modify the .husky/pre-commit file to the following:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run check:up-to-date-types
Enter fullscreen mode Exit fullscreen mode

Before every commit, the hook will run and verify that the types are up to date. Handy!

Conclusion

Working within the TypeScript ecosystem has great potential, but, when leaning too much on manual processes, it's easy to blow trust in the type-system or generate unnecessary friction.

CSS Modules are great, and with a little bit of extra configuration, its easy to add type safety to the generated classes. You should automate the boring stuff so that your team can focus on building a great products instead. I hope you enjoyed this article, and be sure to leave a comment below if you have questions. Happy coding!


Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.

LogRocket Dashboard Free Trial Banner

LogRocket is like a DVR for web apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web apps — Start monitoring for free.

Top comments (0)