Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 14, 2022 09:50 pm GMT

Fixing Class Composition in Tailwind CSS

Ive been going back and forth on Tailwind CSS for quite a while. On one hand, I loved the utility-first design, on the other - I hated how my HTML, JSX, etc. was getting bloated by the number of class names required to style even a basic button.

At last, Tailwind has won me over, mainly because it gets rid of one of the things I struggle with the most when writing CSS - naming. After all, as if designing good UI/UX wasnt hard enough, you still have to find a good and architecturally-sound way to name all your classes - something that, for me at least, wasnt achievable in the long run - no matter how many different CSS methodologies I tried.

On top of that, after using Tailwind for over a year now, for various projects, I found that it boosted my productivity. When paired with component-based framework utility classes work really well, especially when you have all of them memorized already. There is one caveat though

Class Composition

Composing Tailwinds utility classes is hard.

Consider youre building an entire UI with basic components already having their styles defined through a set of Tailwind classes. What happens when you want to slightly alter the component just for a single use case? You cant apply m-0 class because m-1 which the component already has applied, has higher specificity.

There are many opinions on how to deal with such cases. Some recommend using the @apply directive for your components base styles. Others argue that you should already consider all the possible variants of your component and use JavaScript logic to apply proper classes based e.g. on the components props.

For me, both of those solutions feel like compromises. I dont like using @apply unless its absolutely necessary. On the other hand, thinking of all the variants ahead of time or including all of them in the base component either limits your designs or overuses client-side JS.

There are a couple of third-party solutions to this problem though.

Existing Alternatives

The thing is pretty much all existing solutions involve using more client-side JS. Thus, while theyre more developer-friendly than e.g. concatenating utility classes by hand, they somewhat unnecessarily slow down your website.

Ive already covered some of the more JS-heavy alternatives in one of my previous posts - most, if not all of them are still actively maintained. For this article, Ill focus more on the alternatives with limited use of JS and those Ive newly discovered.

twin.macro

One of the more promising alternatives is twin.macro - a Babel macro that processes Tailwind classes to generate JS objects understandable by various CSS-in-JS libraries. The developer experience (DX) of using it is amazing as you not only get all of Tailwinds features without much change to your code, but you also get much more flexibility - all that on top of the traditional benefits of CSS-in-JS. Heres an example code:

import tw, { css } from "twin.macro";const hoverStyles = css`  &:hover {    border-color: black;    ${tw`text-black`}  }`;const Input = ({ hasHover }) => (  <input css={[tw`border`, hasHover && hoverStyles]} />);

The problem is, twin.macro, while doing some processing during the build, still adds a lot of client-side JS - both by requiring a CSS-in-JS engine as well as putting all the style objects created through the macro into your JS code. This means not only slower performance, but also larger bundle sizes.

While twin.macro provided the best DX by far, there are a couple of other alternatives that got close to it, without adding as much additional JS code.

Vanilla-extract

Vanilla-extract landing page

Vanilla-extract allows you to utilize TypeScript as your CSS preprocessor, just like Sass. On top of that, it comes with Sprinkles - an atomic CSS framework that allows you to reproduce Tailwinds utility classes.

// sprinkles.css.tsimport { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";const colors = {  "blue-50": "#eff6ff",  "blue-100": "#dbeafe",  "blue-200": "#bfdbfe",  // etc.};const colorProperties = defineProperties({  conditions: {    lightMode: {},    darkMode: { "@media": "(prefers-color-scheme: dark)" },  },  defaultCondition: "lightMode",  properties: {    color: colors,    background: colors,  },});export const sprinkles = createSprinkles(responsiveProperties, colorProperties);export type Sprinkles = Parameters<typeof sprinkles>[0];

The TS stylesheets must be defined in separate .css.js or .css.ts files though for zero-runtime usage. With that said, in order to achieve truly zero-runtime usage, you can only use Sprinkles in other TS stylesheets. This somewhat limits your flexibility.

Although you can use Sprinkles directly in your JS/TS code, a small runtime will be required - a helper function and a mapping object containing all your atomic classes, necessary for class lookup at runtime.

If you want to reproduce the entire set of utility classes from Tailwind, this object will be huge.

Windi CSS

Windi CSS landing page

Windi CSS is a direct Tailwind alternative (with full Tailwind CSS v2 feature compatibility), with easier setup and faster compile times. While the advantages of this framework lessened as Tailwind matured to v3, its most promising feature (in regards to class composition) was a compilation mode. It was meant to compile utility classes applied on elements into singular, scoped classes.

<!-- input --><div class="py-8 px-8 max-w-sm mx-auto bg-white rounded-xl shadow-md space-y-2 sm:py-4 sm:flex sm:items-center sm:space-y-0 sm:space-x-6">  <img class="block mx-auto h-24 rounded-full sm:mx-0 sm:flex-shrink-0" src="/img/erin-lindford.jpg" alt="Woman's Face">  <div class="text-center space-y-2 sm:text-left">    <div class="space-y-0.5">      <p class="text-lg text-black font-semibold">        Erin Lindford      </p>      <p class="text-gray-500 font-medium">        Product Engineer      </p>    </div>    <button class="px-4 py-1 text-sm text-purple-600 font-semibold rounded-full border border-purple-200 hover:text-white hover:bg-purple-600 hover:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-offset-2">Message</button>  </div></div><!-- output --><div class="windi-15wa4me">  <img class="windi-1q7lotv" src="/img/erin-lindford.jpg" alt="Woman's Face">  <div class="windi-7831z4">    <div class="windi-x3f008">      <p class="windi-2lluw6">        Erin Lindford      </p>      <p class="windi-1caa1b7">        Product Engineer      </p>    </div>    <button class="windi-d2pog2">Message</button>  </div></div>

If implemented well, this approach could fix all the issues related to class composition, while still providing DX similar to that of Tailwind. The only downside would be that, as described, itd be an all-in or all-out solution that doesnt allow you to pick which utility classes to compile and which to leave as is - leading to possibly large CSS bundles.

Still, this is by far the solution with the least negative impact. The only problem is, the development of the compile mode seems to have gone stale, leaving it available only to a few of Windi CSS dedicated integrations and barely documented. Whats worse, over the last few months development on the entire Windi CSS project has slowed down.

Zero-runtime solution

After exploring all the above alternatives (and more), Ive decided to implement my own, custom solution. For that, Ive turned to Linaria - a true zero-runtime CSS-in-JS library.

Linaria landing page

After Linaria does its processing in the build step, all you get is just a string in JS, while the actual styling is extracted into a dedicated CSS file. On top of that, it integrates well with most preprocessors, such as Sass or PostCSS - together with the PostCSS Tailwind plugin.

Thanks to that, using Linaria, I was able to get something like the following:

import { css } from "@linaria/core";const flexCenter = css`@apply flex justify-center items-center`;// Use in React JSX<h1 className={flexCenter}>Hello world</h1>;

With Linaria and JSX I got my entire component - including styles and structure - in one file, while not sacrificing performance or code quality. The only problem left is with the syntax.

While not too long, css`@apply is a lot to repeat for every class you want to compile. Thats why using Vite and some Rollup plugins (Vite uses Rollup under the hood) I was able to shorten it into this:

const flexCenter = tw`@apply flex justify-center items-center`;// Use in React JSX<h1 className={flexCenter}>Hello world</h1>;

With no need for any additional imports and tw template literal tag syntax (inspired by twin.macro), this became my go-to solution for all projects using Tailwind.

To summarize the advantages of this solution over previous alternatives:

  • Opt-in class name compilation;

  • Truly zero-runtime (just strings);

  • Still using Tailwind under the hood (with all its utility classes and access to the latest updates);

  • Great DX (compact syntax, autocomplete in VS Code just by adjusting the regex in the Tailwind extension);

There is one drawback though - the setup. Linaria, while amazing in its functionality, turns out to be quite problematic to install, even with the available guides and integrations. Ive almost always run into issues when setting it up in a new project. On top of that, if you want the shorthand tw-based syntax, you need some additional configuration as well:

import { defineConfig } from "vite";import linaria from "@linaria/rollup";import replace from "@rollup/plugin-replace";import inject from "@rollup/plugin-inject";export default defineConfig({  plugins: [    replace({      "tw`": `css\`@apply${" "}`,      preventAssignment: true,    }),    inject({      css: ["@linaria/core", "css"],      include: "**/*.ts?",    }),    linaria({      exclude: ["**/*.html", "**/*.css"],    }),  ],});

The above config is what I used to get the shorthand and Liniaria working in Vite. To give you an overview of how it works - the twis first replaced with css\@apply and then the css template literal tag is auto-imported where necessary. Ive only used this setup with Vite and Astro (Vite-based) so I dont know how hard itd be to get it working in other bundlers (though pure Rollup should work as well).

Because of such a difficult setup and all the potential bug-fixing involved, its not something Id recommend for everyone. With that said, Im willing to go through it, just to later enjoy good DX, performant styling, and easy class composition. I havent yet explored extracting the entire setup into a separate Vite plugin, but it might be worth considering.

UnoCSS - the newest alternative

Now, Id like to mention one more Tailwind alternative that might be worth your attention - especially in regards to class composition - UnoCSS.

UnoCSS landing page

UnoCSS seems to be the spiritual successor of Windi CSS, sharing some of its features and contributors. Its an atomic CSS engine, meaning that it allows you to create your own utility CSS frameworks on top of it. Additionally, its super-fast, thanks partially to its architecture but also its general simplicity.

The best thing about UnoCSS is that it looks to be the best tool for atomic CSS, with various presets for defining utility classes (including one for Tailwind CSS) and great features, that cant be found in Tailwind - including compilation mode!

Thats right, UnoCSS has a compilation mode, thats even better than what was originally intended for Windi CSS, as its opt-in (with a configurable prefix), meaning you only compile the class names that you want!

<!-- input --><div class=":uno: text-center sm:text-left">  <div class="text-sm font-bold hover:text-red" /></div><!-- output --><div class="uno-qlmcrp">  <div class="text-sm font-bold hover:text-red" /></div>

UnoCSS seems like the go-to option to replace my scrappy setup. Its working really well in my initial experiments and, in the future, Ill likely move some, if not all, of my projects to UnoCSS.

Conclusion

This post is a result of a year or two of research, experimentation, and actual usage of different tools in various projects of mine. It didnt even cover all alternatives I tested nor whats possible with the likes of UnoCSS and other, bleeding-edge solutions. With that said, if youre dealing with class composition and are in search of great development experience, I hope you found this article helpful!

Vrite

Blogging for developers | coming 2023

Join the waitlist now https://vrite.io

Professional editor and powerful headless CMS in one. Vrite is a writing platform by developers, for developers.

Vrite landing page


Original Link: https://dev.to/areknawo/fixing-class-composition-in-tailwind-css-1k4d

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To