DEV Community

Sanna Jammeh
Sanna Jammeh

Posted on • Edited on

Im merging css-in-js and Tailwind

I'm merging css-in-js and Tailwind. Let me explain.

Why merge Tailwind with CSS-in-JS when twin.macro exists?

The first reason is reusability. Creating a button component which can vary via props requires manually mapping props to tailwind classnames. This gets very tiring very fast and makes it quite difficult to create React UI libraries with pure Tailwind.

The second reason is to improve my Typescript knowledge. It's not easy creating Typescript libraries, making them infer props, configurations all under the hood is even harder. And thus, my journey creating the library began.

The requirements

I made a list of requirements for this library, these are specific to challenges I faced developing UI libraries and React components with Tailwind.

  1. It must be able to merge multiple classnames
  2. It must have support for toggling different classnames based on input
  3. It must be able to create JSX like button or span
  4. First class TypeScript support

Inspiration

When trying to come up with a good API I only had to look in one direction. Stitches.js. In my opinion, this is the best CSS in JS library made to date, and as such, it had the exact API I was looking to create. Please check their library out and give Modulz/WorkOS the credit they deserve!

Introducing @tw-classed/react & @tw-classed/core public alpha

These are both in alpha stages as I'm still ironing out the Typescript types required to make them work. The types in core are not stable enough for advanced usage just yet.

@tw-classed/react is quite stable even for advanced Typescript usage, though I haven't performed any tsc performance tests nor heavy testing just yet. As such I've decided to only release these two libraries in a public alpha while I'm testing.

The full documentation is here. Below is a tiny introduction to the libraries.

@tw-classed/core

This is the framework agnostic core of the react library. It can be used standalone in vanilla JS to create dynamic Tailwind classes (or in other libraries when not using react). It simply acts as a class merger and toggler and can be used in any way imaginable.

import classed from "@tw-classed/core";

const simpleButton = classed(
  "flex items-center py-3",
  "hover:bg-gray-200 focus:bg-gray-200" // Accepts any number of arguments
);

console.log(simpleButton()); // => "flex items-center py-3 hover:bg-gray-200 focus:bg-gray-200"

const button = classed(
  "flex items-center", // Apply your base styles
  {
    // Define your variants
    variants: {
      size: {
        sm: "py-2 px-4",
        md: "py-3 px-6",
        lg: "py-4 px-8",
      },
      color: {
        primary: "bg-blue-500 text-white",
        secondary: "bg-gray-500 text-white",
      },
    },
    defaultVariants: {
      size: "md",
      color: "primary",
    },
  }
);

console.log(button()); // => "flex items-center py-3 px-6 bg-blue-500 text-white"
console.log(button({ size: "sm" })); // => "flex items-center py-2 px-4 bg-blue-500 text-white"
console.log(button({ size: "lg", color: "secondary" })); // => "flex items-center py-4 px-8 bg-gray-500 text-white"
Enter fullscreen mode Exit fullscreen mode

@tw-classed/react

This is the React library. It's a wrapper around @tw-classed/core provides a React component creator similar to styled-components or stitches.js.

import classed from "@tw-classed/react";

const Button = classed("button", "flex items-center", {
  variants: {
    size: {
      sm: "py-2 px-4",
      md: "py-3 px-6",
      lg: "py-4 px-8",
    },
    color: {
      primary: "bg-blue-500 text-white",
      secondary: "bg-gray-500 text-white",
    },
  },
  defaultVariants: {
    size: "md",
    color: "primary",
  },
});

const App = () => {
  return (
    <div>
      <Button>Default</Button>
      <Button size="sm">Small</Button>
      <Button size="lg" color="secondary">
        Large Secondary
      </Button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Down the road

There is still lots of things to do with this library before I feel confident in releasing a V1 stable release. Please don't use it in production just yet. Here is a list of future things.

  • Class merger/differ (Omits classNames when a variant overrides)
  • compoundVariants - Stitches inspired API to toggle variants when other variants are defined
  • Typescript performance testing
  • Stable core typescript API - currently only react is stable
  • Improve TailwindCSS Extension class RegEx for intellisense

Footnote

Thank you for reading this article! I hope many will test the library and provide feedback as its essential to improving. Any PR's and issues are welcome!

Here is the GitHub

I can be reached at My Website or through Twitter

Top comments (4)

Collapse
 
benrogerson profile image
Ben Rogerson • Edited

Hey there, looks like some great work went into this project.
I like stitches too, particularly they way they do their variant styling.

Also thought I'd let you know there's been some updates and the SWC compiler is now usable alongside Twin with this config - so the first sentence in your intro is out of date 👍

Collapse
 
sannajammeh profile image
Sanna Jammeh

Cheers! Updated :)

Collapse
 
ssbb profile image
Sviatoslav Bulbakha

Looks cool!

Just wondering - I am right there is no a lot of runtime overhead like in classic css-in-js and it's just a class merging?

Collapse
 
sannajammeh profile image
Sanna Jammeh • Edited

Hey! There is very little runtime overhead, you're right. All variants and classed are pregrouped ahead of rendering in the most performant way I could manage. Then once rendering the component will compute the correct classNames and thats it.

Additionally the loop is not very taxing. You usually dont have hundreds of props, thus the loop generating the class names is only looping over at most 100 props, most likely just 2 or 3.