Why Use Tailwind CSS for Your Next Project
Tailwind CSS is a low-level, customizable CSS framework that is on the rise. It's frequently compared to Bootstrap. However, they are fundamentally different. Instead of delivering pre-designed components as Bootstrap does, Tailwind gives building blocks that allow the developer to build designs in no time at all.
Before I came across Tailwind, I started with CSS, moved to SCSS, and then moved to Styled Components. Throughout my journey, I had a lot of great experiences with each technology, however, none brought me the satisfaction and productivity boost that Tailwind has.
A Long Journey
Writing pure CSS is very good as a beginner since one can fully understand and memorize the rules needed to achieve a specific layout. Even as an advanced developer, one can reap very good results by being able to write custom CSS that matches exactly what is needed. Nevertheless, these rules most likely won't scale. Each element in the UI will require its own sets of rules, and keeping track of hundreds of these rules can become daunting, and hard to maintain. This does not mean that it is impossible to keep them in check. With due practice and discipline, they can be organized and maintained very well.
To solve this problem, I moved from CSS to SCSS with a 7 in 1 architecture. At first, I loved how well my code was separated into 7 folders that represent specific components and sections of the site. Also, it opened a world of possibilities with new features like variables, functions, and nested rules. This made it a lot easier to guarantee that my site styles were easy to find and maintain. Nevertheless, I wanted to embed my styles inside my JavaScript, so I don't have to hop between files continuously. This is why I started using a CSS-in-JS solution like Styled Components.
Styled Components is my go-to whenever I cannot use Tailwind in a project. It is such an amazing library since it allows me to have all my styles exactly where they need to be used. Also, it makes the app use only the styles it needs, instead of importing all styles on each page. Even though it is by far a great upgrade from other solutions, it still had a problem: repetition. I felt I was writing the same rules over and over, and losing needed time. Also, My styles just kept getting bigger and bigger. When I realized this and found Tailwind, I chose to move on and I have never looked back since.
Syntax
Tailwind's syntax is very basic: append to HTML or JSX elements class names that represent one or more CSS declarations. Here is what it looks like:
<div class="max-w-sm mx-auto flex p-6 bg-white rounded-lg shadow-xl">
<div class="flex-shrink-0">
<img class="h-12 w-12" src="/img/logo.svg" alt="ChitChat Logo">
</div>
<div class="ml-6 pt-1">
<h4 class="text-xl text-gray-900 leading-tight">ChitChat</h4>
<p class="text-base text-gray-600 leading-normal">You have a new message!</p>
</div>
</div>
This will generate the following:
Each class name represents one or various CSS declarations. For example, flex represents display: flex;
; p-6 padding: 1.5rem
; mx-auto margin-left: auto; margin-right: auto;
; and so forth.
This paradigm is called utility-first. It consists of building complex components from a constrained set of primitive utilities.
By using this approach Tailwind obtains a myriad of benefits compared to custom CSS.
Smaller Styles
The first benefit of using Tailwind is writing CSS that does not grow linearly. By having utility class names that will be shared between elements, it guarantees a consistent bundle size. Furthermore, Tailwind paired with Purge CSS will remove any unused styles so the codebase will not be bloated with extra classes.
A site's CSS bundle with Tailwind.CSS will look like this:
Styles may start higher at a certain point, but in the future, with more features, these styles will remain constant and save a lot of Kilobytes.
Facebook adopted a similar approach, albeit with a custom CSS-in-JS library that leverages atomic CSS which is very similar to Tailwind's utility-first approach. With this approach, they reduced their CSS bundle size in their whole site from 413kb to 74kb. That is a ~82% reduction! And now we have this power in our hands without having to create a custom library.
💡 Atomic CSS classes can only have a unique CSS declaration. This differs greatly from utility-first CSS where classes can have multiple declarations.
Less Cognitive Load
Tailwind is designed to be component friendly. It is so much easier to separate a site's elements into smaller components and not pollute the codebase with objects or extraneous CSS classes. Furthermore, every class is inlined in the component, making it much easier to read and understand.
Take for example this navigation bar component with Styled Components:
const Navbar = () => {
return (
<Wrapper>
<Items>
<NavItem label="Home" icon={<Home size={16} />} />
<NavItem label="Blog" icon={<Bookmark size={16} />} />
<NavItem label="About" icon={<User size={16} />} />
</Items>
</Wrapper>
)
}
const Wrapper = styled.nav`
position: fixed;
width: 100vw;
bottom: 0;
justify-content: center;
`
const Items = styled.ul`
flex: 1;
padding: 0 2rem;
justify-content: space-around;
`
const NavItem = ({label, icon, ...otherProps}) => (
<Item {...otherProps}>
<span>{icon}</span>
{label}
</Item>
)
const Item = styled.li`
display: flex;
flex-direction: column;
align-items: center;
font-size: 1.2rem;
`
In my opinion, this snippet is very well organized, and it can be understood after it has been read.
let's look at its Tailwind's implementation:
const Navbar = () => {
return (
<nav className="fixed bottom-0 justify-center w-screen">
<ul className="justify-around flex-1 px-8">
<NavItem label="Home" icon={<Home size={16} />} />
<NavItem label="Blog" icon={<Bookmark size={16} />} />
<NavItem label="About" icon={<User size={16} />} />
</ul>
</nav>
)
}
const NavItem = ({label, icon, ...otherProps}) => (
<li className="flex flex-col items-center text-xl" {...otherProps}>
<span>{icon}</span>
{label}
</li>
)
Tailwind on the get-go consumes a lot less space. Also, its styles can be understood right away since they are read at the same time the developer scans the code. One does not have to jump over the file just to see its styles.
In this small example, it may not be obvious how cumbersome this can become in larger files. But on large codebases with multiple components per file, it is a nightmare to have all these separate styles between components. It takes a lot more time compared to Tailwind to understand how each element composes a UI.
You may be asking how one can reap the benefits of Styled Components' main forte: to permit the use of JavaScript in styling. This will depend on the context of the solution.
One can leverage the usage of inline style object or conditional class names.
// Inline object
<CustomComponent styles={{ color: fontColorByBackground('#000') }}/>
// Conditional classes
import clsx from "clsx"
<CustomComponent
className={clsx('flex', {
'text-black': luminance > 0.18,
'text-white': luminance < 0.18,
})}
/>
However, this may not be the most attractive solution for some teams. The good news is that since we are dealing with CSS, we can just use both Tailwind and Styled Components without cost (Emotion CSS prop or CSS modules can be a better match for Tailwind's paradigm).
💡 Keep in mind that Tailwind is the predominant solution, so one can reap its benefits. Styled Components is optional for heavy CSS/Javascript manipulations. Also, This makes it easier for codebases that want to move on to Tailwind at an incremental pace.
Better Composition
Tailwind by default makes the developer think in components. It makes us want to keep it small and avoid repeatability. This is possible thanks to the low amount of space it requires to achieve a layout; how it makes us notice when styles are repeated or bloated; and how easy it is to read and remember styles when split into small components.
Think of this practice as creating CSS class names for every feature or section of a website. Instead of doing it in terms of CSS, we would do it for HTML. For example, If we have a Hero, then it will be best to have a component that represents the Hero section. Likewise, we could do the same with blog postcards, navigation, header, etc.
Let's look at the next snippet:
const IndexPage = () => {
return (
<section className="container p-2 mx-auto lg:p-8 xl:p-0">
<h1 className="font-bold text-white text-11xl sm:text-12xl">Blog.</h1>
<div className="flex flex-col mx-auto space-y-8 md:grid md:row-gap-6 md:col-gap-8 md:grid-cols-2">
{posts.map(
({ frontmatter: { title, description }, slug, hasCoverImage }, i) => {
const isFirstItem = i === 0;
return (
<div
key={slug}
className={clsx(
"w-full bg-white rounded-md font-body relative overflow-hidden",
isFirstItem && "md:col-span-2",
!isFirstItem && "md:col-span-1"
)}
>
<a href="#">
{hasCoverImage && (
<img
className="object-contain w-full rounded-md md:object-cover"
src={src}
alt="cover image"
/>
)}
<header className="flex flex-col items-center p-12 space-y-4 text-center bg-white rounded-md">
<h1 className="max-w-6xl text-5xl font-bold">{title}</h1>
<p className="max-w-5xl text-2xl font-medium">
{description}
</p>
</header>
</a>
</div>
);
}
)}
</div>
</section>
);
};
You'll notice it is hard to read and understand what its intent is unless one reads it meticulously. Classes are all over the place and HTML elements lack coherence. Immediately one knows there must be a better way to represent it.
If we start separating it into components, we immediately see its benefits:
const IndexPage = ({ posts }) => {
return (
<div className="container p-2 mx-auto lg:p-8 xl:p-0">
<h1 className="font-bold text-white text-11xl sm:text-12xl">Blog.</h1>
<Posts posts={posts} />
</div>
);
};
const Posts = ({ posts }) => (
<div className="flex flex-col mx-auto space-y-8 md:grid md:row-gap-6 md:col-gap-8 md:grid-cols-2">
{posts.map((postInfo, i) => {
const isFirstItem = i === 0;
return (
<PostCard key={postInfo.slug} isFirstItem={isFirstItem} {...postInfo} />
);
})}
</div>
);
const PostCard = ({
frontmatter: { title, description },
hasCoverImage,
isFirstItem,
}) => (
<div
className={clsx(
"w-full bg-white rounded-md font-body relative overflow-hidden",
isFirstItem && "md:col-span-2",
!isFirstItem && "md:col-span-1"
)}
>
<a>
{hasCoverImage && (
<img
className="object-contain w-full rounded-md md:object-cover"
src={src}
alt="cover image"
/>
)}
<header className="flex flex-col items-center p-12 space-y-4 text-center bg-white rounded-md">
<h1 className="max-w-6xl text-5xl font-bold">{title}</h1>
<p className="max-w-5xl text-2xl font-medium">{description}</p>
</header>
<div className="absolute top-0 right-0 px-6 py-8 mr-8 text-white bg-black">
<p className="block font-semibold">Read more</p>
</div>
</a>
</div>
);
Not only it is easier to read in the short-term, but when we come back to it, we'll be able to understand it quicker. Also, when our code grows in size and maybe want to reuse elements like 'Read More', we can create a component of it easily and share it between elements.
ℹ This practice does not apply only to Tailwind. It should be used in all cases. However, Tailwind makes it easier to notice when to do it since its class names are agnostic to the implementation. CSS or CSS-in-JS libraries make you think the other way around: create classes or objects that are meant for only one context.
Higher productivity
It is no surprise that writing Tailwind classes is faster than writing CSS or CSS-in-JS. When we are adding declarations to selectors in a stylesheet, we have to write the property and value and add a colon. Thankfully, IDEs and text editors automatically help us with autocomplete and formatting. However, we still have to write the selectors and pieces of declarations to trigger autocomplete. By just writing styles inline, Tailwind in the long-term makes the process a lot faster.
Also, Tailwind has something called variants which are prefixes that can be added to classes, representing a specific CSS pseudo-class, a responsive breakpoint, or custom plugins.
Take for example media queries. In normal CSS, we would have:
/* Small (sm) */
@media (min-width: 640px) { /* ... */ }
/* Medium (md) */
@media (min-width: 768px) { /* ... */ }
/* Large (lg) */
@media (min-width: 1024px) { /* ... */ }
/* Extra Large (xl) */
@media (min-width: 1280px) { /* ... */ }
This may seem easy to read. However, imagine having hundreds of classes. Soon it may become hard to read and slow to modify.
Now, let's look at Tailwind's:
<!-- Width of 16 by default, 32 on medium screens, and 48 on large screens -->
<img class="w-16 md:w-32 lg:w-48" src="...">
Here we define a width for each breakpoint. What makes this incredible is that we are scoping the styles within an element. Later, it will be a lot easier to remove or modify the declaration since it will not mesh within hundreds of rules.
Likewise, pseudo-classes translate easier in tailwind than on CSS. In this case, it is not because of maintainability or readability, but by the ease of writing. Adding a prefix is a lot faster than adding a whole new rule:
<input class="w-16 bg-orange-500 hover:bg-blue-500 focus:bg-red-500" />
<!--
Translates to:
.block {
width: 4rem;
background: #ed8936;
}
.block:hover {
background: #4299e1;
}
.block:focus {
background: #f56565;
}
-->
Finally, we have custom plugins. They allow you to add custom prefixes that will enhance your class's behavior. For example, they allow the developer to add a class depending on a condition like dark mode or overriding other classes by assigning !important
.
<!-- Dark mode (Coming in v2) -->
<div className="bg-black dark:bg-white"/>
<!-- Important (https://github.com/chasegiunta/tailwindcss-important) -->
<div className="!bg-red"/>
Consistency
In addition to writing styles faster, Tailwind has a lot of pre-built classes for sizing and colors that will reduce or completely remove the need to implement a design system. This also means that there is a lower learning curve for an experienced developer in Tailwind to learn the styles on an existing project with it.
💡 You may be asking how consistent it is on browsers. Since Tailwind is such a low-level framework, it means that support for older browsers is up to the developer not to their implementation like bootstrap does.
High Customization
Tailwind gives the developer a generous default theme that will work very well for bootstrapped projects. Nonetheless, it exhorts developers to extend or modify these classes through the tailwind.config.js
file.
Tailwind's configuration file allows developers to tamper right into Tailwind. Any change will be reflected in the final stylesheet. It also allows adding custom plugins, which opens a world of possibilities for third-parties.
Many developers when starting to use Tailwind will complain about one-off styles. Contrary to plain CSS, Tailwind may seem constricted to new styles since the default classes are abundant and look like they follow a specification like Bootstrap. However, since classes in Tailwind normally represent one CSS declaration, it is possible to extend, add, or remove them easily.
On the other hand, developers sometimes refuse to extend this configuration since they don't want to delve into the documentation, or just want to add multiple declarations to one class mainly for a component.
Since Tailwind uses PostCSS as its pre-processor, it means that it can extend its capabilities well outside its configuration file. The majority of codebases will have a CSS file that imports Tailwind styles through PostCSS.
Inside this file, one can create normal CSS classes but also using Tailwind's generous amounts of classes. Like this:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn-blue {
@apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
.btn-blue:hover {
@apply bg-blue-700;
}
}
Overall, this means that everything that can be done in CSS is possible within Tailwind!
Great Upgrade Path
Tailwind's future is very bright. New updates come frequently. These add new CSS properties that are coming out for modern browsers, new prefixes, convenient PostCSS directives, new classes, and many more.
For example, in the upcoming 2.0 version, we'll be getting a new prefix for the dark mode. Previously, adding dark mode required some workarounds, however, now it will be native and easy to add by pre-pending dark:
to classes applied on dark mode.
<div class="bg-white text-black dark:bg-black dark:text-white dark:hover:text-gray-300"/>
Also, in version 1.7 (currently on version 1.9.6), we saw the addition of text background-clip for creating cool gradients texts. These are just a few examples of great updates that improve drastically the development experience and productivity.
Drawbacks
Tailwind CSS has a lot of benefits like increasing performance, consistency, and productivity. Regardless, it does not mean it is perfect for every codebase or developer team.
One of Tailwind's weakness can be its readability. At first, it may not seem obvious, but when a considerate amount of classes are added to an element, it can become daunting to read for some developers. Personally, I haven't had many problems with this, and I believe that with correct component abstractions this can be easily solved. Nonetheless, many developers can see this as a disadvantage.
Another of Tailwind's weakness is creating complex animations. Even though simple animations are included out-of-the-box and more can be added through the configuration, complex animation orchestrations are very hard to achieve.
For creating a complex animation, one would have to create a class name for each element. This will end up in more work than doing within CSS. Thankfully, this can also be easily solved using Plain CSS through stylesheets, an animation library like Framer Motion, or a CSS-in-JS library like Styled Components. I believe any of these is valid and encouraged with Tailwind, however, some developers would just prefer to just use plain CSS for their animation without any library.
Another weakness is the need to learn an opinionated naming convention. This is a double-edged sword. On one hand, having opinionated styles means once learned, knowledge is transferable between projects and productivity greatly increases. On the other hand, one has to dedicate time to learn all nuances and how it works before being able to create something useful.
In my opinion, if one knows CSS, the Tailwind naming convention will be very easy to learn since its class names are very similar to plain CSS. Also, their proprietary extension provides autocomplete so that developers can ease in when learning or remembering class names.
Nevertheless, some teams will still be reluctant to introduce a framework that requires a learning curve, especially when time is a luxury.
Conclusion
Tailwind as a low-level CSS framework has become my preferred styling solution. It has solved most of the drawbacks I have encountered in my journey through different CSS solutions. It has increased my productivity, reduced the bundle size of my apps, made me more consistent, and changed my development paradigm.
Even though Tailwind has worked very well for me and many other developers, it does not mean that it is the perfect solution for every project. Evaluating its benefits for a codebase and its effects on the team are necessary before considering to adopt it.
In the future, I see Tailwind growing more and being implemented in many projects even with an existing CSS solution. I've started seeing many modern projects that have started doing this like the Vercel e-commerce template.
In the end, Front-End developers are working with CSS. This means we can mesh different solutions as long as we guarantee maintainability, scalability, and performance. With this in mind, Tailwind is definitely one solution that must be considered for its myriad of benefits.
For more up-to-date web development content, follow me on Twitter, and Dev.to! Thanks for reading! 😎