DEV Community

Cover image for Tailwind CSS for frontend teams: From settings to rules
LuiGee3471 for comento

Posted on • Originally published at developer.comento.kr

Tailwind CSS for frontend teams: From settings to rules

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

Hi, I'm Yoon Jongseok, a frontend developer at Comento 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 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.

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

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.

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

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. The plugin allows you to share these settings across multiple places. By the way, the blog where the article originally posted also uses an official plugin called typography.

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

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

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.

// tailwind.config.ts

import type { Config } from 'tailwindcss';

const config: Config = {
  content: [
      ...
  ],
  plugins: [
    require('@comento/tailwind-plugin'),
        ...
  ],
};
export default config;
Enter fullscreen mode Exit fullscreen mode

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.

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

// ✅
<p className={color === 'primary' ? 'text-primary' : 'text-info'}>Text</p>
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

Prevent conflicts between classes

To make class-based Tailwind easy to use, we used the clsx library to help manipulate class names. However, using clsx and Tailwind together often doesn't work as intended.

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

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, which is a class that eliminates conflicts between classes when using Tailwind. So I created the following utility function.

export function cn(...args: ClassValue[]) {
  return twMerge(clsx(...args));
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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, which ensures that class names are always ordered according to a certain rule. 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, which is maintained by the community and has slightly more rules 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.

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

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.

Top comments (0)