If you’ve been staying in the loop with the latest CSS frameworks, you’ve probably already heard of the newest kid on the block: Tailwind CSS. According to its documentation, “Tailwind is a utility-first CSS framework for rapidly building custom user interfaces.”
In practice, this means using a variety of classes that closely map to underlying CSS properties. For example, applying a class like .text-center
to an element means that we’re setting its text-align
property to center
. Simple enough, right?
Using utility classes like this allows us to spend more time iterating through designs and avoiding premature abstraction of our CSS. Then, once we’re happy with our designs, Tailwind makes it easy to extract our utilities into component classes.
Now, I’m sure you know that even mentioning a utility framework is an open invitation for a certain amount of brouhaha. Before we start yelling at me on Twitter or the comments for even mentioning a utility framework, let’s just take a moment to remember that Tailwind is just one possible tool for us to use.
If you don’t want to add it to your toolbox, then no worries—you do you! But, if you’re interested in at least understanding this new tool, let’s take a look at building a sign-up form together.
Without further ado, let’s set up a fresh project with Tailwind, code out some HTML, and style it up.
The Set Up
Let’s start by creating a directory for us to work from. Using your terminal, navigate to the directory you’d like to create your project and run mkdir <your-project-name>
. Now, let’s cd
into our new project and follow the Tailwind installation guide.
Since we want to see everything that Tailwind can do, let’s install it with npm or Yarn using the following.
# Using npm
npm install tailwindcss --save-dev
# Using Yarn
yarn add tailwindcss --dev
With Tailwind installed, we can now generate the configuration file by running ./node_modules/.bin/tailwind init
. This generates a tailwind.js
file for us in the root of our project. In this file, we can adjust Tailwind’s default settings to our project’s needs. For this project, we shouldn’t have to change a thing.
Now, let’s create a CSS file where we can manage our own CSS and inject the Tailwind styles. To do that we can run touch styles.css
from our project directory.
Inside of this new file, we can use Tailwind’s @tailwind
directive to inject the preflight
and utilities
styles into our CSS. preflight
contains all of the base styles and some browser style normalizations, while utilities
adds all of the utility classes we specified in our config file. So, our styles.css
file should look something like this:
@tailwind preflight;
/* Here we can add any custom overrides */
@tailwind utilities;
/* Here we can add our own utilities or custom CSS */
If you’re using PHPStorm and you’re annoyed by the red squiggles in your CSS file for @tailwind
, just add /*noinspection CssInvalidAtRule*/
above where you use it.
With that all set up, we can run ./node_modules/.bin/tailwind build styles.css -o main.css
to generate the CSS file we’d like to use in our project. This may seem a bit tedious, but don’t worry! Tailwind works with proper build tools like Webpack, Gulp, or Laravel Mix, so in a larger project you can just set it and forget it.
That’ll take care of our Tailwind set up! Now, we can start coding our HTML.
Our HTML
Before we can style our sign-up form, we need to build it! To start, we’ll need a simple index.html
file. So, from your root directory, you can run touch index.html
to create the file. Then, we can add the following snippet to get us started.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tailwind Intro</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
</body>
</html>
As you can see, it’s your typical HTML page. The only wrinkle here is that we’re importing our main.css
file and we’ve given our page a descriptive title. Now, let’s start building our sign-up form!
To start, let’s add two nested <div>
elements to the inside of our <body>
tag.
<body>
<div>
<div>
</div>
</div>
</body>
We’ll use the outer <div>
for our page positioning, while the inner <div>
will be the wrapper for our form. Now, inside the inner <div>
, we can add a <h1>
to label the form, and a <form>
.
<div>
<h1>Sign Up</h1>
<form action="/" method="post">
</form>
</div>
We’re really cooking with gas now! To finish the form, we just need to add the <label>
elements, <input>
elements, and <button>
. As we add them, let’s wrap each of our <label>
<input>
pairs in a <div>
so they stay together.
<form action="/" method="post">
<div>
<label for="first_name">First Name</label>
<input type="text" name="first_name" id="first_name">
</div>
<div>
<label for="last_name">Last Name</label>
<input type="text" name="last_name" id="last_name">
</div>
<div>
<label for="email">Email</label>
<input type="email" name="email" id="email">
</div>
<div>
<label for="password">Password</label>
<input type="password" name="password" id="password">
</div>
<button type="submit">Create Account</button>
</form>
Finally, let’s add a link to access the login page right below our form.
<div>
<!-- Form is here -->
<a href="/login">Already have an account?</a>
</div>
Putting that all together, our HTML will look like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tailwind Intro</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div>
<div>
<h1>Sign Up</h1>
<form action="/" method="post">
<div>
<label for="first_name">First Name</label>
<input type="text" name="first_name" id="first_name">
</div>
<div><label for="last_name">Last Name</label>
<input type="text" name="last_name" id="last_name">
</div>
<div>
<label for="email">Email</label>
<input type="email" name="email" id="email">
</div>
<div>
<label for="password">Password</label>
<input type="password" name="password" id="password">
</div>
<button type="submit">Create Account</button>
</form>
<a href="/login">Already have an account?</a>
</div>
</div>
</body>
</html>
Pretty straightforward, right? Now, when we see how that renders on the page, we should see something that looks like this:

Don’t be alarmed if it looks like the <input>
s are missing; that’s just the browser resets at work. At last, we’re ready to see what this Tailwind CSS is all about.
Using Tailwind CSS
Being the good developers that we are, let’s take a mobile-first approach to styling our sign-up form. So, at a smaller viewport width of 400px
, our page looks like this:

Styling Our Form Fields
Let’s start using Tailwind by styling our <input>
s. First, let’s add a border so we can see it on the page. To do that, we just need to add the .border
class. So, now our first name <input>
will look like this:
<input class="border" type="text" name="first_name" id="first_name">
Now we can see it on the screen!

How easy was that? Let’s continue by adding some padding and making the text color a touch lighter. To do that, we just need to add the following classes: .py-2
, .px-3
, and .text-grey-darkest
.
<input class="border py-2 px-3 text-grey-darkest" type="text" name="first_name" id="first_name">
With the first two classes, we’re taking advantage of the padding scale that comes with Tailwind and applying it vertically and horizontally to the element. If you want to define your own scale, just hop into your config file and change it to what you need. With the last class, we’re using Tailwind’s default color palette and changing the color of our element to the darkest grey.
Let’s take our form a step further. Now, we can position our <label>
s above our <input>
s and give them a bit of styling.
<div class="flex flex-col mb-4">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="first_name">First Name</label>
<input class="border py-2 px-3 text-grey-darkest" type="text" name="first_name" id="first_name">
</div>

Look at that, our first name field looks great! And the best part is that I really don’t have to explain what these classes are doing—they explain themselves! But just so we’re all on the same page, let me run through them quickly.
The outer <div>
has its display
property set to flex
via .flex
and its flex-direction
is set to column
with .flex-col
. Then it has a bit of margin on the bottom thanks to .mb-4
.
Meanwhile, our <label>
has a little less margin on the bottom thanks to .mb-2
. The rest of the classes make our text uppercased, bold, large (1.125rem
), and the darkest grey in our color palette.
Altogether, a pretty quick and easy way to style our fields! Now, let’s add these styles to the rest of our fields, style our button and link, and see what we’re working with.
<form class="mb-6" action="/" method="post">
<div class="flex flex-col mb-4">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="first_name">First Name</label>
<input class="border py-2 px-3 text-grey-darkest" type="text" name="first_name" id="first_name">
</div>
<div class="flex flex-col mb-4">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="last_name">Last Name</label>
<input class="border py-2 px-3 text-grey-darkest" type="text" name="last_name" id="last_name">
</div>
<div class="flex flex-col mb-4">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="email">Email</label>
<input class="border py-2 px-3 text-grey-darkest" type="email" name="email" id="email">
</div>
<div class="flex flex-col mb-6">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="password">Password</label>
<input class="border py-2 px-3 text-grey-darkest" type="password" name="password" id="password">
</div>
<button class="block bg-teal hover:bg-teal-dark text-white uppercase text-lg mx-auto p-4 rounded" type="submit">Create Account</button>
</form>
<a class="block w-full text-center no-underline text-sm text-grey-dark hover:text-grey-darker" href="/login">Already have an account?</a>

Adding Hover Styles
Now things are starting to look better! In this block of code, everything should be pretty self-explanatory. However, we have added one new thing: a state variant. If we take a look at our <button>
, we can see one in action.
<button class="block bg-teal hover:bg-teal-dark text-white uppercase text-lg mx-auto p-4 rounded" type="submit">Create Account</button>
If you look at the class right after .bg-teal
, you can see that we’ve added a hover:
prefix to .bg-teal-dark
. These prefixes let us style elements on hover and focus, and they let us use breakpoints too! All in all, this is a pretty powerful feature of Tailwind and it lets us create dynamic, responsive UI very quickly.
Now, let’s wrap our mobile view by positioning our form in the middle of the screen and adding a colorful page background.
<div class="flex items-center h-screen w-full bg-teal-lighter">
<div class="w-full bg-white rounded shadow-lg p-8 m-4">
<h1 class="block w-full text-center text-grey-darkest mb-6">Sign Up</h1>
<form class="mb-4" action="/" method="post">
<div class="flex flex-col mb-4">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="first_name">First Name</label>
<input class="border py-2 px-3 text-grey-darkest" type="text" name="first_name" id="first_name">
</div>
<div class="flex flex-col mb-4">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="last_name">Last Name</label>
<input class="border py-2 px-3 text-grey-darkest" type="text" name="last_name" id="last_name">
</div>
<div class="flex flex-col mb-4">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="email">Email</label>
<input class="border py-2 px-3 text-grey-darkest" type="email" name="email" id="email">
</div>
<div class="flex flex-col mb-6">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="password">Password</label>
<input class="border py-2 px-3 text-grey-darkest" type="password" name="password" id="password">
</div>
<button class="block bg-teal hover:bg-teal-dark text-white uppercase text-lg mx-auto p-4 rounded" type="submit">Create Account</button>
</form>
<a class="block w-full text-center no-underline text-sm text-grey-dark hover:text-grey-darker" href="/login">Already have an account?</a>
</div>
</div>

Bada bing bada boom, we’ve got ourselves a good-looking mobile sign-up form! But, what happens when we take a look at this on a bigger screen?

Responsive Design
It’s certainly better than our plain HTML, but it needs some work. Let’s use some of the responsive state variants and style this for larger screens.
<div class="flex items-center h-screen w-full bg-teal-lighter">
<div class="w-full bg-white rounded shadow-lg p-8 m-4 md:max-w-sm md:mx-auto">
<h1 class="block w-full text-center text-grey-darkest mb-6">Sign Up</h1>
<form class="mb-4 md:flex md:flex-wrap md:justify-between" action="/" method="post">
<div class="flex flex-col mb-4 md:w-1/2">
<label class="mb-2 uppercase tracking-wide font-bold text-lg text-grey-darkest" for="first_name">First Name</label>
<input class="border py-2 px-3 text-grey-darkest md:mr-2" type="text" name="first_name" id="first_name">
</div>
<div class="flex flex-col mb-4 md:w-1/2">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest md:ml-2" for="last_name">Last Name</label>
<input class="border py-2 px-3 text-grey-darkest md:ml-2" type="text" name="last_name" id="last_name">
</div>
<div class="flex flex-col mb-4 md:w-full">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="email">Email</label>
<input class="border py-2 px-3 text-grey-darkest" type="email" name="email" id="email">
</div>
<div class="flex flex-col mb-6 md:w-full">
<label class="mb-2 uppercase font-bold text-lg text-grey-darkest" for="password">Password</label>
<input class="border py-2 px-3 text-grey-darkest" type="password" name="password" id="password">
</div>
<button class="block bg-teal hover:bg-teal-dark text-white uppercase text-lg mx-auto p-4 rounded" type="submit">Create Account</button>
</form>
<a class="block w-full text-center no-underline text-sm text-grey-dark hover:text-grey-darker" href="/login">Already have an account?</a>
</div>
</div>

Thanks to our responsive prefixes, our sign-up form is looking much better! Let’s take a look at our <form>
for some examples.
<form class="mb-4 md:flex md:flex-wrap md:justify-between" action="/" method="post">
<!-- ... -->
</form>
Just like with our hover:
prefix, we’re only applying the prefixed classes when that condition is met. In this case, that means we’re only applying the flex styles to our <form>
when the page’s min-width
is 768px
.
Extracting Utilities Into Components
Now that we’ve finished prototyping our form, we can extract our utility classes into component classes. Let’s start by extracting our <input>
classes.
<input class="border py-2 px-3 text-grey-darkest" type="password" name="password" id="password">
As we can see, our <input>
has a couple of classes on it. We can extract these to our CSS using Tailwind’s @apply
directive. @apply
allows us to apply the same styles that our utility classes use to a new class. So, at the bottom of our styles.css
file, we can add the following:
.field {
@apply .border .py-2 .px-3 .text-grey-darkest;
}
Then, once we’ve re-complied our Tailwind files, our <input>
can just have the .field
class.
<input class="field" type="password" name="password" id="password">
As you can see, with Tailwind we get the best of utility and component classes! We can iterate quickly with utility classes, and still extract component classes when we start to see a pattern.
Even better, we can blend them to handle those one-off cases where dedicated component classes don’t make sense.
The Final CSS
Applying this thinking to the rest of our code, our CSS and HTML will look like this.
@tailwind preflight;
/* Here we can add any custom overrides */
.field {
@apply .border .py-2 .px-3 .text-grey-darkest;
}
.field-label {
@apply .uppercase .font-bold .text-lg .text-grey-darkest .mb-2;
}
.field-group {
@apply .flex .flex-col;
}
.btn {
@apply .block .text-white .uppercase .text-lg .p-4 .rounded;
}
.btn-teal {
@apply .bg-teal;
}
.btn-teal:hover {
@apply .bg-teal-dark;
}
.link {
@apply .block .no-underline .text-sm;
}
.link-grey {
@apply .text-grey-dark;
}
.link-grey:hover {
@apply .text-grey-darker;
}
@tailwind utilities;
/* Here we can add our own utilities or custom CSS */
The Final HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tailwind Intro</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="flex items-center h-screen w-full bg-teal-lighter">
<div class="w-full bg-white rounded shadow-lg p-8 m-4 md:max-w-sm md:mx-auto">
<h1 class="block w-full text-center text-grey-darkest mb-6">Sign Up</h1>
<form class="mb-4 md:flex md:flex-wrap md:justify-between" action="/" method="post">
<div class="field-group mb-4 md:w-1/2">
<label class="field-label" for="first_name">First Name</label>
<input class="field md:mr-2" type="text" name="first_name" id="first_name">
</div>
<div class="field-group mb-4 md:w-1/2">
<label class="field-label md:ml-2" for="last_name">Last Name</label>
<input class="field md:ml-2" type="text" name="last_name" id="last_name">
</div>
<div class="field-group mb-4 md:w-full">
<label class="field-label" for="email">Email</label>
<input class="field" type="email" name="email" id="email">
</div>
<div class="field-group mb-6 md:w-full">
<label class="field-label" for="password">Password</label>
<input class="field" type="password" name="password" id="password">
</div>
<button class="btn btn-teal mx-auto" type="submit">Create Account</button>
</form>
<a class="link link-grey w-full text-center" href="/login">Already have an account?</a>
</div>
</div>
</body>
</html>
We’ve extracted our duplicate classes and left the rest. As we find ourselves building similar components, then we can extract more classes!
Wrap Up
Whew! We certainly covered a lot in this post. We started out stretching our HTML muscles by building a quick form, and then we took a mobile-first approach to our styles with Tailwind. Along the way, we saw how to use Tailwind’s utility classes to quickly style an element. Then, we used state variants to add some dynamic styles. And finally, we saw how we could have our cake and eat too when we extracted our duplicate classes to component classes.
I hope that you were able to get a taste of how Tailwind can make an impact in your own projects. If you’d like to mess around with this project, feel free to clone the repo and experiment with Tailwind yourself. As always, feel free to ask me any questions on Twitter. And until next time, happy coding!
It strangely strikes me (“strikes me strangely?”) that this method — along with a lot of these modern Framework methods — reverses the usual computer-programming paradigm. We usually compile human-readable, simpler, code to machine-readable, more obtuse, code. But here you’re doing the opposite — short-handing as much as possible which then gets “compiled” down the the simpler CSS standards.
Here’s a starter kit for any Jekyll folks: https://github.com/taylorbryant/tailwind-jekyll
Worth mentioning other frameworks with similar concepts:
Inuit.css: https://github.com/inuitcss/inuitcss
Ekzo: https://github.com/ArmorDarks/ekzo
Tachyons: http://tachyons.io/
Basscss: http://basscss.com/
Atomic CSS: https://acss.io/
Very interesting framework. Not for me though. It’s far too much CSS in HTML. Most of these classes represent a CSS property and value pair. At that point, you’re very nearly doing inline style.
I love Tailwind, and this is a great overview. One question: If you’re using ~~PHPStorm~~ Visual Studio Code and you’re annoyed by the red squiggles, what do you do to get rid of them?
In VS Code, at least until there are better extensions or native support for PostCSS, you just need to add this in your user settings:
"css.validate": false
I can’t be the only one who thinks this is madness?
You need a reference guide to work like this….
Also this?
Are we using atomic css and then combine them and then add that class to the html? What the? Doesn’t this defeat the whole point of atomic css?
And then why not apply this to the first div?
consistency failure or am I missing the point somewhere?
Shortcuts in class names always concerned me too. They usually making hard to understand a code base without some proficiency with the system, and this related not only to atomic approaches.
For instance, in Ekzo I’ve tried to avoid shortcuts at all. With clever usage of objects, it mostly doesn’t make
class
attribute too long. And it keeps their meaning clear for newcomers.For instance, this class name should be pretty much self-explaining:
Note that helper classes like
.text-lg
,.text-grey-darkest
and.mb-2
are in fact mapping not directly to their bare CSS counterparts, but also caring as a specific settings. For instance, thatlarge text
is afont-size
andline-height
of specific values, thatgrey darkest
is quite a specific color andmargin bottom
helper most likely based on a specific vertical rhythm value. If we’d think of them as of mixins or variables, it would be easier to understand that in fact they encapsulate specifically predefined rules of our projects and allows to reuse them everywhere.Generally, I’d say there is indeed an issue with Tailwind CSS. It lacks the layer of abstraction and relies on composition too much.
This issue better solved by Inuit.css or Ekzo, or other Atomic frameworks, where not only bare helpers provided, but also more complete, but still design-free elements — objects (named differently in different approaches). Than you mostly use objects to construct the output, which are much less verbose. And helpers serves as a meaning only to adjust those objects or other instances, which require scarce, pattern-less adjustment. Than you simply never have such ridiculously overloaded DOM elements with the only helpers, but still keeping great deal of flexibility and ability to rapidly built output without writing a single line of CSS and not inventing class-names for each DOM element you’d like to style. That’s saves a lot of time, after all.
Here is example of a layout with more abstraction-oriented approach. Here helpers used only for adjustments or building of very primitive things, which doesn’t justify a time to write full CSS for them, nor are a clear patterns, which can’t be extracted into an object.
I get how this makes you scratch your head a bit. The basic idea, as one of Tailwind’s creators put it, is that with approaches like BEM, we are doing a lot of premature optimization. We assume that everything needs to be componentized and will be reused. However, in practice, this can get out of hand quickly.
Using BEM, I found myself always having to create new “modifier” classes just to change something like padding or margin in one particular instance of a component.
And then there’s the issue of naming things (I hate naming things).
Tailwind’s approach:
– Get straight to writing markup and building out pages with utility classes
– When you find yourself copying & pasting the same classes over and over, simply copy the classes and extract a component out of them (like your .container example above)
– If you need to adjust something like margin or padding on just one instance of the component, instead of writing new CSS, use one of the already available utility classes
I recommend just trying to build something with it. I was initially very skeptical of it, but I love it now.
Adam Wathan, one of Tailwind’s creators, makes an excellent case for the use of utility classes: https://adamwathan.me/css-utility-classes-and-separation-of-concerns/.
You have traded in unique declarations to specific dom elements for class soup. This is not a good thing
Looks like a class too many, a healthy balance between unique declarations and helper classes is ideal i find.
Look at how much code noise there is. For a basic form too. There’s no way any project other than a small one could use and scale with this. CSS has Cascading in it’s name, this method is junk.
This is not the future. This ist the past.
Why not write inline css directly?
Because inline styles would not enforce consistency. For instance, by allowing to use
font-size
everywhere, your developers will placefont-size: 10px
in one place andfont-size: 11px
in another. By providing them with predefined set of classes, they can use only.font-size-sm
for small sizes, which encapsulates exactlyfont-size: 10px
, and may even include needed line-height for it.Think of them as of mixins, or constants.