Tailwind CSS for frontend teams: From settings to rules
Tags: tailwindcss, webdev, frontend
Published: 2024-05-03 08:18:35

*The following article was originally written in Korean and translated into English with the assistance of DeepL Translator. You can find the original post [here](https://developer.comento.kr/post/frontend-tailwind-css-24-04-01)*

Hi, I'm Yoon Jongseok, a frontend developer at [Comento](https://comento.kr) from Korea. The Comento frontend team is preparing to transition our existing projects to Next.js this time, and we're introducing a lot of new stacks during the transition process, and I'd like to share with you how we've been adopting Tailwind CSS. It's a framework that's been very popular lately, and I've used it a lot on my own, but getting it up and running as a development team is a whole different challenge. I'll walk you through the various problems we encountered and how we solved them.

## Customizing it to fit our design

When you first install Tailwind, it has its own defaults. For example, `mt-4` means `margin-top: 1rem;`. However, the utility classes we were using had literal values that looked like numbers, such as `mt-4` becoming `margin-top: 4px`. It wasn't just the numbers that were different: the colors, text, and other names and values we used were all different from Tailwind's defaults.

Fortunately, Tailwind has a setting called [Theme](https://tailwindcss.com/docs/theme) that allows you to change the defaults. You can do this by setting the `theme` attribute in `tailwind.config.js`, which can be changed to any value you like, and it's handy to have autocomplete in the editor work based on the values you change.

```js
// tailwind.config.js
import { height } from './height.js';

module.exports = {
	theme: {
	  // If you change it right under theme like this, it will overwrite all the defaults.
		colors: {
			primary: '#2a7de1',
			...
		},
		extend: {
		  // If I set it under extend, it will keep Tailwind's class and only change the values I set.
		  // Classes like h-1/2 and h-full are useful, so we recommend using them as extends
			height: height, // You can also use the result of the code like this
		}
	},
}

// height.js
const height = {};

for (let i = 0; i <= 300; i++) {
	if (i % 2 === 0) {
		height[i] = `${i}px`;
	}
}

export { height };
```

As shown above, there are utility class values, but in previous projects, there was also CSS that applied globally to `html`, `body`, etc. or changed the attributes of the tag itself. `html`, `body` tags can be changed using the class name directly, and `@layer` can be used to specify a global style, as shown in the example below.

```jsx
// layout.tsx
import '@/styles/index.css';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <html>
    <!-- You can pass classes directly -->
	  <body className="min-h-screen">
		  {children}
		</body>
	</html>;
}
```
```css
// styles/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

// If it can't be done with classnames, you can write a CSS code.
@layer base {
 img, picture, video, canvas, svg {
   display: block;
	 max-width: 100%;
	}
}
```

## What if I want to use these settings elsewhere?

This worked well for my small project, but what if I wanted to take it elsewhere? The traditional approach of separating common components into separate libraries meant that I would have to redo the settings I made above. Of course, if it was a small project, it would be fine to create both, but by the time I did, it had already grown. Having multiple values that needed to be managed the same way was going to make maintenance difficult.

That's when I came up with the [plugin](https://tailwindcss.com/docs/plugins). The plugin allows you to share these settings across multiple places. By the way, [the blog where the article originally posted](https://developer.comento.kr/post/frontend-tailwind-css-24-04-01) also uses an official plugin called [typography](https://tailwindcss.com/docs/plugins#typography).

The syntax is almost identical to `tailwind.config.js`. We wrote it like this.

```js
import plugin from 'tailwindcss/plugin';
import { colors, spacing, borderRadius, fontSize, fontWeight, height, screens, boxShadow } from './theme';

export default plugin(
	function ({ addBase, theme }) {
		// The values you set in @layer base are set here, not in CSS.
		addBase({
			// Reference: https://www.joshwcomeau.com/css/custom-css-reset/
			'*, *::before, *::after': {
				'box-sizing': 'border-box',
			},
			'*': {
				margin: 0,
			},
			// Solve font issues that differ only on macOS
			body: {
				'font-family': theme('fontFamily.sans'),
				'-webkit-font-smoothing': 'antialiased',
				'-moz-osx-font-smoothing': 'grayscale',
			},
			'img, picture, video, canvas, svg': {
				display: 'block',
				'max-width': '100%',
			},
			'input, button, textarea, select': {
				font: 'inherit',
			},
			// To solve z-index issues
			'#root, #__next': {
				isolation: 'isolate',
			},
		});
	},
	// Add or override theme values.
	{
		theme: {
			colors,
			spacing,
			fontSize,
			fontWeight,
			screens,
			boxShadow,
			extend: {
				fontFamily: {
					sans: [
						'"Pretendard Variable"',
						'Pretendard',
						'-apple-system',
						'BlinkMacSystemFont',
						'system-ui',
						'Roboto',
						'"Helvetica Neue"',
						'"Segoe UI"',
						'"Apple SD Gothic Neo"',
						'"Noto Sans KR"',
						'"Malgun Gothic"',
						'"Apple Color Emoji"',
						'"Segoe UI Emoji"',
						'"Segoe UI Symbol"',
						'sans-serif',
					],
				},
				borderRadius,
				height,
			},
		},
	},
);
```
Unlike the setup above, which utilized both CSS and JS, the plugin looks a little different because all of the setup must be done in JS. You can see that the values we previously put in `@layer base` are now under `addBase`.

Now we'll import this plugin into the project we'll be using and put it into the settings.

```js
// tailwind.config.ts

import type { Config } from 'tailwindcss';

const config: Config = {
  content: [
	  ...
  ],
  plugins: [
    require('@comento/tailwind-plugin'),
		...
  ],
};
export default config;
```
## When there's a problem with external components

I'd built the plugin and was ramping up development, but one day I noticed that the CSS was applying strangely - not all of it, but certain classes weren't working at all, or the imported component wasn't applying the design correctly.

Using developer tools to analyze it, I found that it looked like the CSS was being applied twice. As I reverted back through the commits to find the point at which the problem occurred, I realized that it was when I installed the component library. It turns out that I was trying to fix a problem that was caused by the way Tailwind works, but I was actually creating a different problem.

Tailwind works by looking for occurrences of a class name in a file and applying the class. The following mistake is common when first using Tailwind. Tailwind can't figure out what class will be used in the example below, so it won't use the class if it is dynamically determined later.

```jsx
// ❌
<p className={`text-${props.color}`}>Text</p>

// ✅
<p className={color === 'primary' ? 'text-primary' : 'text-info'}>Text</p>
```
When I was using component libraries, Tailwind wasn't applying the component's CSS correctly because it wasn't checking the component file, so I compiled the CSS from the library and loaded it, but then I realized that libraries and projects using the same plugin would have conflicting CSS, or CSS would be loaded twice.

It seems like a complicated problem, but the solution is really simple and it's in the official documentation. You can check it [here](https://tailwindcss.com/docs/content-configuration#working-with-third-party-libraries).

```js
// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
    // Specify the path to the library like this
    './node_modules/@comento/design-system/dist/index.{cjs,js}',
  ],
  plugins: [
    require('@comento/tailwind-plugin'),
    require('tailwindcss-animate'),
    require('tailwind-scrollbar'),
  ],
};
export default config;
```
Tailwind analyzes the files in the path set in `content` to determine which classes to use, so `node_modules` is not normally referenced for performance reasons, but whenever you want to write to this library separately or import another library, you just need to set it to reference that path.

FYI, in version 4, which is in alpha at the time of writing, this is supposed to be fixed [without requiring any configuration] (https://tailwindcss.com/blog/tailwindcss-v4-alpha).

## Prevent conflicts between classes

To make class-based Tailwind easy to use, we used the [clsx](https://github.com/lukeed/clsx) library to help manipulate class names. However, using `clsx` and Tailwind together often doesn't work as intended.

```jsx
const isBold = true;
const classNames = clsx('text-regular text-body1', { 'text-semi-bold': isBold });

<p className={classNames}>Bold Text</p> // Text may not appear in bold
```

If you write code like the example above, there's no guarantee that it will work, because the order of applying styles is not related to the order of the classes. It's a harder problem to figure out because sometimes it doesn't work and sometimes it does.

So the library I use is [tailwind-merge](https://github.com/dcastil/tailwind-merge), which is a class that eliminates conflicts between classes when using Tailwind. So I created the following utility function.

```ts
export function cn(...args: ClassValue[]) {
  return twMerge(clsx(...args));
}
```
However, we couldn't use it right away because, as you can see, we're using a custom theme, and some of the classes are renamed so twMerge doesn't know what they are. Fortunately, there is a setting to handle this. I followed the documentation and set it up as follows.

```js
const customTwMerge = extendTailwindMerge({
  extend: {
    classGroups: {
      // This ensures that the color and text classes we set don't merge.
      'font-size': [
        'text-display1',
        'text-headline1',
        'text-headline2',
        'text-headline3',
        'text-headline4',
        'text-headline5',
        'text-headline6',
        'text-headline7',
        'text-body1',
        'text-body2',
        'text-caption1',
        'text-caption2',
      ],
      // I was also having issues with a library called tailwind-animate, so I added below.
      animate: ['animate-in', 'animate-out', 'animate-none'],
    },
  },
});

export function cn(...args: ClassValue[]) {
  // Use the customTwMerge you set up above.
  return customTwMerge(clsx(...args));
}
```

## Make sure all developers use the same rules

After that, I rarely had any issues with Tailwind not working the way I wanted it to, but I started to wonder what conventions we should use to organize Tailwind's classes so that we could use them consistently. I felt like I was writing class order without a clear convention on my own, and I knew it would be a harder problem to solve if everyone was doing it.

Since this was a problem that many people had already encountered, there was already a solution. The first thing I found was [Tailwind's official Prettier plugin](https://github.com/tailwindlabs/prettier-plugin-tailwindcss), which ensures that [class names are always ordered according to a certain rule](https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted). Best of all, it's auto-correcting, so I don't accidentally miss something or commit a different order.

However, after a little more searching, we found [this plugin](https://github.com/francoismassart/eslint-plugin-tailwindcss), which is maintained by the community and has [slightly more rules](https://github.com/francoismassart/eslint-plugin-tailwindcss) than the official plugin's class name ordering rules. Again, most of the rules support auto-correction. Here are the rules the team chose to use

1. `classnames-order`: Enforces the order of class names. Everyone sorts classes by the same rule.
2. `enforces-shorthand`: Shortens multiple classes if they can be reduced to one. For example, `pt-4 pb-4` can be shortened to `py-4`.
3. `migration-from-tailwind2`: Tailwind is now in version 3, but still supports version 2 class names. This rule automatically replaces the old version's classnames with the new version's classnames.
4. `no-custom-classname`: Prohibits the use of classes that are not Tailwind's classes. This is a good rule to catch if you accidentally type the wrong class name. 
5. `no-contradicting-classname`: Don't use class names that contradict each other. This prevents accidental overwriting or writing classes that don't apply.

This ensures that Tailwind looks the same for all developers. As an additional setting, it can be used not only in the className property, but also in things like the cn function we created above.

```js
// .eslintrc.js
module.exports = {
  ...
	settings: {
		tailwindcss: {
		  // This will also work for the functions below
		  callees: ['classnames', 'clsx', 'cn'],
		  // I couldn't use it when declaring variable, so I created a template function called tw and used it
		  tags: ['tw'],
		}
	}
	...
}

// Example of using variables
const classNames = tw`bg-white p-4`;
```

## More configuration than I expected

This is how I finished configuring Tailwind for our new project. When I first started using Tailwind in my team, I didn't think it would require so much configuration. It's a framework that I thought was easy to use without much configuration when I was playing around with it by myself, but it's much more complicated to configure it for a team. But this experience also cleared up a lot of my doubts about whether Tailwind would be able to handle the scale. I hope this helps anyone who is thinking about setting up Tailwind or who has encountered problems while doing so. This was quite a long post, so thank you for reading.