For me, it's important that all resources I use in a project have a predictable API, so I don't have to dig into the source code later. So, when using CSS styles, I want to see whether I'm using the correct selectors in my components.
That’s why CSS-in-TS seems like the perfect tool for control and performance improvement. But in reality, it's not just one single tool but rather an entire ideology with various shades of implementation. In this article, we will look at 3 different CSS-in-TS tools and see how they differ. The main goal is to evaluate the impact of a tool on the development process, not to choose the best solution – everyone has their own. Let's start, perhaps, with the most popular approach.
CSS Modules + typed-css-modules
The CSS Modules library allows you to write styles in CSS files, but isolates those files from each other to prevent naming conflicts.
In a typical scenario, we first write styles in a file with the format *.module.css:
/* Base button */
.btn {
height: 1.5rem;
border-radius: 0.25rem;
color: white;
background-color: #58666d;
}
/* Background variants */
/* Button with primary bg */
.btn-primary {
background-color: #2192a7;
}
/* Button with secondary bg */
.btn-secondary {
background-color: #9d41ab;
}
/* Width variants */
/* Button with full width */
.btn-full {
width: 100%;
}
/* Button with half width */
.btn-half {
width: 50%;
}
Then we generate TypeScript types for these styles using typed-css-modules. For our module, we get the following TypeScript declaration style.module.css.d.ts:
declare const styles: {
readonly "btn": string;
readonly "btnFull": string;
readonly "btnHalf": string;
readonly "btnPrimary": string;
readonly "btnSecondary": string;
};
export = styles;
And then we can use it in our components, fully benefiting from TypeScript autocomplete functionality:
import { btn, btnFull, btnPrimary } from './style.module.css';
const Component = () => (
<button className={`${btn} ${btnPrimary} ${btnFull}`}>
Use CSS-in-TS
</button>
);
Thus, in this scenario, CSS styles are the primary source – types are created based on them, and our components depend on types.
vanilla-extract
vanilla-extract suggests describing styles directly in TypeScript files from the start, while questions of selectors uniqueness and isolation are resolved at the level of individual calls to its utilities.
In a typical scenario, we first write styles in a file with the format *.css.ts:
import { style, styleVariants } from '@vanilla-extract/css';
/* Base button */
export const btn = style({
height: '1.5rem',
borderRadius: '0.25rem',
color: 'white',
backgroundColor: '#58666d'
});
/* Background variants */
export const btnBackground = styleVariants({
/* Button with primary bg */
primary: {
backgroundColor: '#2192a7'
},
/* Button with secondary bg */
secondary: {
backgroundColor: '#9d41ab'
}
});
/* Width variants */
export const btnWidth = styleVariants({
/* Button with full width */
full: {
width: '100%'
},
/* Button with half width */
half: {
width: '50%'
}
});
There is no need to manually generate TypeScript declarations for these styles – it's sufficient to import them into your components and use them:
import { btn, btnWidth, btnBackground } from './style.css.ts';
const Component = () => (
<button
className={`${btn} ${btnBackground.primary} ${btnWidth.full}`}
>
Use CSS-in-TS
</button>
);
Thus, in this scenario, the primary source is the CSS styles from *.css.ts – they generate types and enable TypeScript autocomplete.
EffCSS
EffCSS generates styles using StyleSheet maker functions and suggests first explicitly declaring a function type, then implementing and using it. This library isolates styles obtained from different functions.
In a typical scenario, we first describe the maker type in a file with the format *.ts:
export type TBtnMaker = {
/**
* Button
*/
btn: {
/**
* Background
*/
bg: 'primary' | 'secondary';
/**
* Width
*/
width: 'full' | 'half';
};
};
Then we describe our styles according to TBtnMaker:
import { TStyleSheetMaker } from 'effcss';
export type TBtnMaker = {...};
export const btn: TStyleSheetMaker<TBtnMaker> = ({
select,
}) => {
return {
[select('btn')]: {
height: '1.5rem',
borderRadius: '0.25rem',
color: 'white',
backgroundColor: '#58666d'
},
[select('btn.bg:primary')]: {
backgroundColor: '#2192a7'
},
[select('btn.bg:secondary')]: {
backgroundColor: '#9d41ab'
},
[select('btn.width:full')]: {
width: '100%'
},
[select('btn.width:half')]: {
width: '50%'
}
}
};
All selectors are created using the select utility, so TypeScript will complain if you try to implement selectors not defined in the contract. Therefore, the TypeScript contract restricts both the person who writes styles and the one who uses them.
Next, we can use these styles in a component:
import { useStyleProvider } from 'effcss';
import { btn } from 'btn-maker.ts';
const provider = useStyleProvider();
const classNames = provider.cx(btn, {
width: 'full',
bg: 'primary'
});
const Component = () => (
<button className={classNames}>Use CSS-in-TS</button>
);
Thus, in this scenario, the primary source is the TBtnMaker contract – it checks selector correctness both when creating and using styles.
Conclusion
If we summarize, although all three tools essentially achieve the same result – TypeScript autocomplete – their usage scenarios differ significantly:
The arrows in the figure indicate the dependencies between the blocks - the source resources are on the left, and the consumers are on the right. This clearly shows that having a common goal doesn't make tools identical. Moreover, the considered scenarios demonstrate differences in initial conditions – you can use existing CSS, base your styles on CSS-in-JS, or even start with TypeScript types. Although choosing a library is often a matter of taste or corporate standards, it is often useful to choose something that will require less effort to integrate into a particular project.
Enjoy your Frontend Development!

Top comments (2)
CSS-in-TS seems to have many different approaches, and each one has its own strengths and characteristics. Great job! 👍
Thanks for the comment! It's nice that you share my point of view!