I Created My Own CSS-in-JS Lib! Why? (Part 1)

Igor Stefano
12 min readJan 27, 2025

--

Hey there!

A while ago, I dedicated myself to a project called Kamishibai, a web app through which TTRPG players could keep track of quests completed by the group. I wrote the codebase using the T3 Stack, which uses React with TypeScript, NextJS, and tRPC. As of the writing of this text, this application hasn’t reached version 1.0, so I wouldn’t like to talk much about it now; instead, I want to talk about a distraction that kept growing in proportion regarding its styling.

This is the first of two texts I decided to write about my experience creating my own styling library, and in this one, I’d like to follow Simon Sinek’s advice and explain first: why the hell would I do this?

Introduction

These days we have so many options, for all tastes; we have Tailwind, beloved by some and hated by others, we have old but still maintained and widely used libraries like styled-components and vanilla-extract, we have new and eye-catching options like StyleX and Panda. And this is just talking about choices for creating your styles from scratch; if we get into the discussion of component libraries, there’s the veteran MUI, the hyped (rightfully so) shadcn/ui, the unfortunately not yet forgotten Bootstrap… well, several great and not-so-great options. All of this, of course, assuming I didn’t decide to simply use good old pure CSS.

To avoid analysis paralysis, I reduced my choices.

At the time I started working on the project, Tailwind had become the de facto standard styling option in the NextJS ecosystem. There are some not-so-simple, different, and cumulative historical reasons for this to have happened: 1) Starting with NextJS version 13, the framework implemented React Server Components. This change broke how most CSS-in-JS libraries worked at the time;

[!NOTE] Simplifying greatly, the libraries tended to generate styles on the client side, as JavaScript code was necessary for this (it is, after all, CSS-in-JS). As React now by default ran the code to generate pages only on the server, in short, everything broke.

Except, you know what didn’t break? Tailwind, which by default already had a pre-compilation step (after all, all classes are already declared).

2) Tailwind was already gaining popularity in the JavaScript ecosystem for being a framework-agnostic way to style applications. 3) The agility gained when using Tailwind, especially in the case of MVPs which historically are a common use case for Next made the lib gain enough notoriety to be the default styling option recommended by Vercel in create-next-app (in favor of CSS Modules which was the previous standard).

Although I wasn’t using the App Router in this specific project and consequently wasn’t worried about Server Components, I decided it would be good to use it both to facilitate a possible future migration and for another more practical reason: I was already used to Tailwind from using it at work.

I did analyze other CSS-in-JS options. After all, there was another tool that I was used to that had a sensational DX (which I even prefer to Tailwind’s): the late Stitches. Stitches is a lib that gained quite some popularity among developers working with NextJS as it had great performance with Server-Side Rendering; however, it ended up being discontinued at the time of Next 13’s launch, so it was never adapted to work with React Server Components.

It’s a shame it wasn’t possible to have Stitches’ DX in this project, after all, it made much more sense to use Tailwind. Well, maybe it wouldn’t be necessary to choose… 🤔 But I’m getting ahead of myself.

First, I think it’s good to understand more about how each of them works.

Stitches

Besides reminding one of a hit song by Shawn Mendes (and, in my case, a less successful one by Foo Fighters), Stitches stood out among the dozens of CSS-in-JS libraries of the time due to some factors that resulted in a great development experience with admirable performance — quite a distinction, since these libraries have been infamous for weighing down bundles.

The first factor was the compilation of styles in advance. Through Stitches, the generated CSS didn’t require JavaScript running on the browser side, which combines well with a framework that seeks to do as much work as possible on the server.

The second is being a design system-oriented library. This requires an initial time investment in configuration but greatly facilitates the maintenance, creation, and composition of new styles.

This all means it was very easy to create, say, a base button…

… And from it, create a confirm button and a cancel button.

Despite all these benefits, I haven’t yet talked about what I liked most about using Stitches, and, in fact, I separated the next section for just this reason.

styles.ts Pattern

The reason for the name CSS-in-JS is that the most common way to use it is (or was) exactly the first way we would imagine when hearing this term: CSS styles being declared together with the rest of our JavaScript code.

Indeed, JavaScript is essential as it’s what enables the almost magical interactions that these libs allow. Want to set maximum width on the button only when the username is “Test123”? Sure, go for it!

… But another characteristic of our web language is being modular. This means we can also declare our styles in a separate JavaScript file…

… And then simply import it where needed.

I particularly like this pattern a lot. I can keep things separate, the code clean, and I don’t mind having to switch context in these cases. Of course, the tradeoff is that some things become difficult to use if not everything is in the same file; for example, if there’s some constant in any of the files that both need to access…

… It will need to be exported.

Cons

Not everything is rosy regarding the use of Stitches, even discounting the impracticality of updating the project to use React Server Components and the fact that it has been discontinued.

Linear Growth Bundle

Stitches’ performance is certainly better than its competitors at the time, but, critically, its weight in the CSS bundle scales almost linearly to the amount of styles written through it. At the beginning it doesn’t make much difference, of course, but this changes as the project grows.

Code Organization Complications

As I mentioned in the previous section, you need to choose which price to pay: export your styles from the styles.ts file, deal with context changes and have difficult access to JavaScript-dependent code, or keep everything in one large polluted file?

Tailwind

As I made clear above, I ended up opting for the probably most hyped option, deciding to use Tailwind. But this tool is more than just that, so let’s go, I think it’s fair to list here why someone might or might not want to use it.

Less Context Switching

A recurring selling point of Tailwind. Using its utility classes means it’s not necessary, 99% of the time, to think about class names, create new classes, or visit different files to create, extend, or check the styling of a component. All style information for a given component is visible right there.

There is a real productivity gain when this context switch is eliminated (after the brief period where it’s just replaced by the context switch of looking at Tailwind’s documentation), which has made it viewed favorably by many developers.

Strong Ecosystem

There are several advantages to using a popular tool. One of them is knowing that other people smarter than you also use it, and may have already solved problems you’re having. This is the case with Tailwind.

Where else is there a styling tool with a strong community of plugins, ranging from animations to forms? The Tailwind team might take time to adapt to new CSS specifications, but the community tends to intervene.

Not to mention, of course, that it’s much easier to ask for help if really needed, and much more knowledge has already been generated about it. Just compare the StackOverflow numbers between Stitches and Tailwind.

Cons

Of course, Tailwind has its share of problems, or half of Front-End developers wouldn’t hate it. I put the most common criticisms below (but I admit that, despite all the drawbacks, and there are many, I like using it).

Poor DevTools Visibility

There’s not much to say here, DevTools is made to facilitate the visualization of styles made by a class. Navigating utility class styles through it is a consistently miserable experience, especially if you’re already used to the more common way.

It’s important to say that there are extensions, focused solely on improving this aspect of Tailwind’s DX. I’ve never used them, so I can’t speak to the quality, but the fact that it’s a strong enough pain point to stimulate the creation of these products tells me a lot.

Aesthetically Unpleasant Markup

Anyone who’s already present in current web dev discussions has a high chance of having seen this image:

To be totally transparent, two things are true: 1) a lot of things need to go wrong to get to this point; 2) I myself have written code that has reached this point. 🥲

As far as my experience goes, these more dramatic examples can be avoided by adopting good practices with Tailwind. Although this isn’t obvious, there are cases — like this one — where it’s better to abstract the styles in a Tailwind CSS-in-JS file or use the @apply directive to declare specific styles, usually when they become very large or with many levels of nesting.

That said, detractors are correct in saying that using Tailwind can easily lead to pretty ugly markup. This is inevitable, it’s in the nature of the tool itself and we as users can only partially dodge it with extensions that hide styles or adopt radical componentization.

It can be argued that there is a positive side which is the guarantee that styles have an understandable meaning without requiring context switching, but without doubt it’s something to consider when thinking about what’s most important for your team’s code readability.

Complexity for Creating Dynamic Styles

Due to the static nature of style compilation in Tailwind’s case — which decidedly doesn’t use JavaScript at runtime to define which styles to load — it’s not that simple. The example below illustrates a possible problem: what if I want to change my button’s width according to some condition?

The example above won’t work as expected. Only the condition that’s true at initial page load will be considered. To dynamically change classes, you need to add the full name of each one according to the desired condition:

This is a challenge specific to Tailwind, which also means that specific patterns need to be adopted if you want to adopt a design system-oriented approach that can contain variants, for example. One option is to declare a different component for each variant you want to use.

This approach by itself can be a bit tiring and kind of diminishes Tailwind’s advantage of avoiding context switches; however, there’s also the option to use a library like class-variance-authority (or cva) to handle variants for you.

This is all a big disadvantage compared to something like Stitches, where variants were part of the expected workflow of using the library.

High Investment in Learning Just One Tool

A final consideration to make is that productivity when using Tailwind is dependent on getting used to the language used by it, especially in the form of classes we use to declare styles but also in its ecosystem.

It happens, of course, that Tailwind is a library maintained by humans, and is, naturally, imperfect and prone to changes. The naming choices are demonstrably inconsistent, tool evolutions also risk bringing changes in how to use it.

This is all to say that the initial investment to see returns in using this tool is greater than a library like Stitches where CSS is declared in a very similar way to the traditional one, and — if all goes well — the tendency is that there will be a need for new investments as time passes.

In other words: when using a solution more aligned with CSS specifications, your evolution is coupled to CSS evolution. By choosing to evolve along with Tailwind, you are forced to evolve along with CSS and Tailwind. This also means a certain difficulty in future migration to another solution, since it’s hard to leave Tailwind once a large project is using it (unless LLMs like Claude and ChatGPT help with this in the future).

Well, and how did I end up with all this?

“What if I make my own lib? 🤔”

… Ok, I admit I didn’t think about this. It’s true that I ended up creating a lib for Kamishibai, but it’s also true that this wasn’t my initial plan, nor my initial thought. The real kickstart of this endeavor was:

“What if I tried to create a styles.tsx?”

At first, all I really wanted was to use the pattern of declaring styles in a styles.ts file.

The fact is that when styles are few, without nesting and no variants, I didn’t see any problem in simply using Tailwind, but from the moment these conditions started to become present, I felt it would be really helpful to separate things. In my case, I created a small design system to facilitate my own development journey, and I was feeling that the standard way of using Tailwind was making this difficult.

And that’s when I thought: nothing prevents me from doing this in React, right? A component declared with Tailwind is just a component like any other. I can easily declare a styles.tsx file and imitate how I did it using Stitches. This way, I can have the best of both worlds, right?

Spoiler: not everything worked so beautifully. The first point is that I’m creating a ready element, instead of providing a DOM element. This implementation needs to be corrected to pass forward the attributes that can be used in this element, as well as correct the TypeScript typing to encompass them.

It’s already improving, now my autocomplete works when I try to change this button’s type, for example, not to mention that if I pass an onClick, it's already able to work.

Well, this already solves quite a lot. Indeed, the styles are already separated and this works…

… But what if I had more? You know, an API similar to Stitches’, but that would allow declaring styles using Tailwind, with autocomplete and everything. And speaking of autocomplete, it could also have it in DOM element attributes, and with a more standardized way of declaring variants.

And what if I could declare this same button… like this?

Look at this neat API. It’s almost like using Stitches with Tailwind classes. It also abstracts if it’s necessary to use a React ref (like when using a library like react-hook-form, for example). It even has autocomplete for which DOM elements are possible to declare, just like Stitches!

How did I do this? What are the advantages and disadvantages? Was it worth it? Well… I think you’ll be interested in the second part of this text. 😉

That’s it for now. I hope you enjoyed this first part; I’ll see you in the next one, where we’ll analyze my own CSS-in-JS lib a bit more closely!

--

--

Igor Stefano
Igor Stefano

Written by Igor Stefano

Software developer. Also a historian and a lover of music.

No responses yet