Supercharge your CSS with Tailwind
Not often in the web development industry do we find tools that truly lead to a paradigm shift for our ways of working. Perhaps you've discovered this when adopting a new language; a few aha moments later and the purported benefits make sense. Some JavaScript developers have discovered this with TypeScript, for instance. There's a learning curve, but the benefits are suddenly all the more real upon a visit to an older JS-based codebase. The new way of working feels like home
Today, I'd like to discuss CSS. Innovations with stylesheets in recent years have focused on tooling and compilation, somewhat independently from innovations in other areas of the stack. Whereas most of us no longer write HTML, many do still write CSS. I'd like to cover the reasons why writing your own CSS is unproductive, and champion an alternative approach to styling.
Tailwind is a styling framework which provides bitesize classes which allow you to style your HTML entities without having to maintain a separate stylesheet. In this article, I'll discuss how the functional CSS ideology can improve productivity and consolidate the styling approach across a front-end team.
CSS
CSS has been around for a while. With applications nowadays like React Native and Electron, its once-limited reach now stretches far. No matter where we look, we can safely say that Cascading Style Sheets is widely adopted.
The problem with vanilla CSS, I'll argue, is that it is deceptively simple. It attempts to do too many things. The syntax is undeniably flexible and the way one experienced developer forges their CSS may look almost alien to another experienced developer. You may be able to relate to this on a more personal level if you've had to dig through your own CSS in the past and wondered "just what was I doing there?".
Writing your own CSS inherently has many disadvantages, which in general reduce productivity due to repetition and/or a reduction in intelligibility:
- Writing your own class names - takes lots of time and many selectors will never be reused
- Trying to match/replicate the structure of your components in styling code - the source of truth should be your components, not your styling
- Vendor prefixes - a cat-and-mouse game of keeping up with browser engines, polluting your codebase at the same time
- Hacks to get ordinary bits of CSS to work - many hacks are required (especially for Safari)
- Mobile breakpoints - pollutes your codebase with media queries at many different breakpoints
-
Specificity and !important - having to order your styling in a certain way, and just one
!important
breaks the cascade - Value repetition - lack of native support for variables
Existing approaches
Over the past twenty years, a number of approaches have been developed to both better organise your CSS and also process styling in a different way. Let's examine the compilation of CSS and a common naming convention (BEM).
Pre-processors
Between ten and fifteen years ago, the advent of CSS pre-processors like Sass, Less and Stylus was revolutionary. You could write your styles in a composited way, saving much repetition and scope for error. You could also have variables in CSS, for the first time.
In the simple case, you could transform this kind of CSS:
nav {
background-color: lightgray;
}
nav li {
padding: 2px 5px;
}
nav li a {
color: lightblue;
}
Into this:
nav {
background-color: lightgray;
li {
padding: 2px 5px;
a {
color: lightblue;
}
}
}
This compositional format shows the relationship between the selectors more naturally, like you'd see child components returned from a parent component in React.
With the pre-processors, you can shrink your CSS and increase reuse through variables. In almost all working cases, it will be an improvement above vanilla CSS. There are also implementations now, via PostCSS, that add vendor prefixes for you. The major drawback is, of course, that you have to compile your CSS beforehand; usually done via part of your tooling such as Grunt or Gulp.
And also, your implementation would still suffer from many of the aforementioned core disadvantages of writing your own CSS, such as writing class names, replicating your structure, hacks and mobile breakpoints.
BEM
Block Element Modifier is a common naming convention which subscribes to the idea that all CSS rules should be divided into blocks, sub-blocks (called elements) and variants (called modifiers). For simple applications, BEM can work rather well when you get used to its philosophy and the class names.
This is an example of a modifier rule name:
.nav__link--selected
Which looks interesting, if you've never seen that syntax before. Quickly, however, you'll find that the names get quite long:
.legal-cookie-banner__accept-or-reject-button--disabled
You'll naturally come to points where:
- An element should be split out to become a block
- Some elements naturally have their own elements
- Some modifiers might naturally have sub modifiers
And so on. There are actually a plethora of different approaches with BEM. It's arguably more a way of thinking than a watertight specification. Atomic BEM (ABEM) is one, for instance. The issues boil down to:
- Names generally become long and unwiedly
- The structure of your HTML is very strongly replicated, making it difficult to change and refactor
- Developer rules and approaches to block-element-element-modifier tend to vary a lot
- Nesting BEM in SASS/Less/Stylus can drastically reduce replication of block/element names but reduce intelligibility
BEM is a fairly old-school approach nowadays, though still common. It still doesn't address many of the core issues of writing your own CSS, and is essentially just a naming convention.
Tailwind
Tailwind is a framework that provides many small bits of CSS which are put together like lego. This approach is referred to as functional CSS. Instead of writing your own classnames, you are provided with pre-made classes which you can use in your HTML in combination to quickly achieve styling.
The beauty of the system is that you can mix and match prefixes, which make it easy and standardised to add states like hover, active or focus, mobile styling and the pseudo-selectors before and after.
Rather than providing off-the-shelf components like Bootstrap and Semantic UI, Tailwind merely provides the small building blocks to assist in building your own components. Much like Normalize.css, Tailwind strips out the default styling browsers apply to all elements, such as buttons, input fields and more.
Let's dive into the major concepts of the framework.
Piecemeal
The core concept of Tailwind is the provision of utility classes, which are really like blocks of lego. Here is an example of a simple link with an underline:
<a href="#" class="text-underline">Click me</a>
This uses the text-
utility prefix. Remember that Tailwind strips out all default browser styling, so links by default will not have an underline or any colour. The text-
utility prefix here groups together related CSS rules. Let's see another one in action by adding a blue colour to the link:
<a href="#" class="text-underline text-blue-500">Click me</a>
We can add colours to most utility prefixes, such as text
, bg
, border
and more. Tailwind supports a number of out-of-the-box colour names, and shades, from 100-900. Let's also add some padding:
<a href="#" class="text-underline text-red-500 px-5 py-2">Click me</a>
The prefix for padding is simply p-
and the value represents fractions of a rem, e.g. p-1
represents 0.25rem
of padding on all sides, equivalent to if you'd written padding: 0.25rem;
in vanilla CSS. Most importantly, you can also use:
-
px
to indicate padding in the x-dimension (e.g.top
andbottom
in CSS terms) -
py
to indicate padding in the y-dimension (e.g.left
andright
in CSS terms) -
pt
,pr
,pb
andpl
to indicate padding in the top, right, bottom and left directions
And, you are not just restricted to using Tailwind's rem fractions, you can also use:
-
-px
to add one pixel to the dimension you want, e.g.py-px
to add one pixel of padding to both thetop
andbottom
- Square brackets, e.g.
p-[5px]
to break out of Tailwind's design system, for those pixel perfect changes (JIT mode only)
Finally, let's add a border to match the colour:
<a href="#" class="text-underline text-red-500 px-5 py-2 border border-current">Click me</a>
The -current
suffix is quite useful in Tailwind, and instructs the system here to use the same colour as the text for the border. We can also add rounded corners:
<a href="#" class="text-underline text-red-500 px-5 py-2 border border-current rounded">Click me</a>
And if we wanted to make the border corners very rounded, like a pill, we can use one of Tailwind's stock size classes, which are xs
, sm
, md
(default), lg
, xl
, 2xl
:
<a href="#" class="text-underline text-red-500 px-5 py-2 border border-current rounded-xl">Click me</a>
These stock sizes also work on text sizes as mobile prefixes, as we will see next. And that's a very simple example of how the utility classes work.
Mobile styling
You could think of base classes being mobile-first. Write your styling representing the smallest screen size, and add Tailwind's stock size classes as prefixes to change the styling at particular breakpoints. For example, if we wanted to have the default text size for a paragraph of text to be text-xs
, but we wanted to bump up the font size to be text-sm
on tablets and larger displays, we can simply write it as follows:
<p class="text-xs lg:text-sm">
Some lovely text.
</p>
Here, lg
represents a breakpoint for all screen sizes that are at least "large" size, which by default is 1024px
. If we then wanted to make the text even bigger on huge displays (1536px and higher), we can use:
<p class="text-xs lg:text-sm 2xl:text-md">
Some lovely text.
</p>
Here, we don't have to worry about polluting our codebase with media queries. We can just quickly indicate when a particular property should change depdending on the screen size, using the same familiar stock sizes we use for text size, border radius and more.
Customisability
Not only does Tailwind provide a wealth of off-the-shelf utility classes, the framework allows us to modify the default assumptions it makes for properties such as colours and sizes. For instance, you could create an xxs
size for text, or a 4xl
responsive breakpoint, if you wanted to.
Tailwind implementations usually have a tailwind.config.js
file associated, which allows you to either replace whole properties of a class family (e.g. bg
for backgrounds), or extend them. Importantly, you don't have to write out or deal with lots of boilerplate code just to change one tiny thing.
One of the most recent additions to Tailwind was "just in time" mode. This allows the PostCSS compiler to "tree shake" the implementation by inspecting the exact classes that are used in your document, drastically reducing compilation time. It also allows you to break out of Tailwind's design system, using square brackets, which you may have noticed earlier. This allows further - pixel-perfect - customisation in a way that is declarative and dependable.
Adoption
Here at InsideOut, we've found that Tailwind has improved our speed of prototyping and development. We can now quickly produce front-end screens without having to worry about maintaining an unweidly companion stylesheet that doesn't align to our components. Our React components are the source of truth for the shape of the end solution, and Tailwind prevents us from "recreating" any of that shape in styling, promoting component reuse, not classname reuse.
Despite Tailwind's wealth of utility classes, there have been rare occasions where we have had to create a few of our own custom classnames. In our base css, for instance, we had to create one at the start to define a custom height for our pages that takes into account the height of our nav bar. We also have another one to make a nice-looking link. For our production-ready internal dashboard, this is the totality of our custom SCSS so far:
.custom-h-full {
height: calc(100% - 3rem);
}
.custom-explicit-link {
@apply underline;
@apply text-blue-500;
&:hover {
@apply text-blue-600;
}
}
And that's it. We adopted a convention of using the prefix custom-
to make it obvious when reading the classnames on a component that there's a bit of styling that is ours in there.
The other convention we found useful quite quickly, in the context of our React components, was grouping Tailwind's utilities into related classes, as shown below:
<div
className={`
absolute top-0 left-0
flex items-center justify-center
rounded
w-full
h-full
bg-yellow-100
opacity-90
`}
/>
Here, we can see that on different lines we've grouped:
- Positioning
- Display (and flex properties)
- Borders/rounding
- Width
- Height
- Background
- Opacity
If we wanted to add a border here, it would be placed neatly on the third line. This makes it clearer to read than this:
<div className="absolute top-0 left-0 flex items-center justify-center rounded w-full h-full bg-yellow-100 opacity-90" />
When it's hard to know what's going on. Especially if a future developer comes in and adds more classes at the end.
Final thoughts
Obviously, Tailwind doesn't aim to translate CSS rules into classes directly, because that would be quite verbose, and wouldn't solve some of the core issues in CSS I outlined at the start of this article, like mobile styling. Tailwind also doesn't aim to solve everything with providing off-the-shelf components. It merely provides a shorthand way to quickly style your components without having to remake the component structure every time in CSS code.
One of the main hurdles here at CSS was learning the Tailwind lingo. A lot of the utility classes provided are remarkably simple and intuitive, such as p
for padding, m
for margin, bg
for background, and so on. There are occasions where the naming is slightly different, e.g. align-items: center
becomes items-center
. But these have not been a big issue at all. The naming feels faithful to the original paradigms in CSS, which made learning Tailwind fairly straightforward. It differs, of course, from other approaches like Semantic UI where the classes read like an english sentence, but feel farther away from actual CSS.
At times the team has felt as if Tailwind is still very much a work in progress. Support for JIT was added in version 2.0, and we adopted it during that major release cycle, with a variant of Tailwind which was compatible with our version of Create React App. JIT required PostCSS 8.0, which CRA has only begun supporting recently. As such, beforehand, there were some extra hoops for integration. We've tried to upgrade to version 3.0 recently but had to roll back due to some impassable webpack issues.
Overall, we do feel that Tailwind has made a positive impact on our front-end development, and will continue to use it for future projects. We're confident that it's a production-ready framework, and are glad to say that it's made responsive development a breeze. We don't have to care any more about specificity, media queries, writing our own class names or browser hacks. The barrier to entry has been fairly low and the productivity boost has been quite high. Tailwind has been a boon to our development stack, and we're looking forward to developing with it in our next project.
Top comments (1)
I like the post, but I disagree with almost all the cons you're making about doing your own styling.
Yes, using something that someone else already made will ofc increase productivity but when you're in a project where you HAVE to override the default values the learning curve goes through the roof if you don't know basic CSS hierarchy.
Also, you can easily use SASS or LESS to make your CSS look better, like the first example you made. And when you start using SASS you can keep all your media queries in a seperate file, like every other framework does and now you have 100% control of your own project and don't have to be scared over possible breaking changes :)
But great post nonetheless!