DEV Community

Cover image for Different shades of CSS-in-TS
Marat Sabitov
Marat Sabitov

Posted on

Different shades of CSS-in-TS

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%;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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%'
    }
});
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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';
    };
};
Enter fullscreen mode Exit fullscreen mode

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%'
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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:

Different approaches

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)

Collapse
 
webdeveloperhyper profile image
Web Developer Hyper

CSS-in-TS seems to have many different approaches, and each one has its own strengths and characteristics. Great job! 👍

Collapse
 
effnd profile image
Marat Sabitov

Thanks for the comment! It's nice that you share my point of view!