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:
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.
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'; }
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> </> )
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.
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;
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 */
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> </> )
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!
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 .",
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 .",
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
Before every commit, the hook will run and verify that the types are up to date. Handy!
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!
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 is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. 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 and mobile apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
One Reply to "How to write type-safe CSS Modules"
This is good