Imagine that you’re building a UI component with React and Tailwind CSS.
That component needs to support multiple style variants.
A fairly common instinct here would be to construct dynamic classes based on properties passed to the component.
Here’s a simplified example:
We’re creating dynamic classes by interpolating the
size prop values, which may sound like a good idea.
But it won’t work with Tailwind CSS.
text-lg classes are present somewhere else in our markup, these classes will not be generated, and the expected styles will not be applied.
Damn. But why?
To answer this, let’s talk about Tailwind’s Just-in-Time engine.
Tailwind CSS v3 generates styles on demand, based on what classes are being detected in your templates.
This ensures that even during development, you never deal with more CSS than you need to.
That translates to very fast build times and opens doors for many cool features that would simply not be possible if all permutations of utility classes were built upfront in a giant CSS file.
Want to add a bunch of new colors? A few extra breakpoints? Maybe add some custom variants to your config file?
Tailwind only generates the CSS for the classes you’re using in your project.
The Just-in-Time engine (JIT) is why all colors, variants, and dark mode are enabled out of the box in Tailwind CSS v3.
Oh, and these variants can all be stacked together, too. That’s a lot of cumulated permutations 😅
But that’s totally fine – there is no need to ever worry about a bloated CSS output.
This is awesome.
But, how exactly does the JIT engine determine what classes are actually used?
How Tailwind looks at your code
The way the JIT engine scans your project to figure out what CSS classes to generate is intentionally simple and relies on two key principles:
- You need to tell Tailwind where to look in your config’s
- Tailwind will only find classes that exist in a full, uninterrupted string in the files it’s looking at.
That second principle is the reason our
<Text /> example is not working properly.
When writing a
text-purple-700, given a
color prop of
Matter of fact - if you’ve tried this and inspected the
<p> tag generated in your browser, you will see that exact class,
text-purple-700, applied to the paragraph tag.
It’s there, in the browser.
But it’s not anywhere, as far as Tailwind’s JIT engine is concerned.
Just like if everything was a
Fun fact, the following code will generate the
<p>My favorite class in Tailwind is text-purple-700, I love it.</p>
The class is there, in its entirety, in plain text. Therefore, it will get generated.
And that’s the one and only rule the JIT engine lives by.
Can I find this Tailwind class in a full string as plain text? 👀 If yes, I’ll generate the CSS for it 👍 If not, I won’t 🤷♀️
So... I can’t use dynamic style?
Wait, no – of course, you can!
You just have to be a little creative and find ways to provide dynamic styles in a JIT-friendly way.
Good news: that’s what we’re going to do now!
Let’s build a relatively simple
Button component. That component will support multiple style variants in a way that works great with Tailwind’s JIT engine on-demand generation strategy 🎉
A JIT-friendly, multi-style Button component
We’ll keep things relatively simple. Our Button will support the following style variants:
Here’s what our button variants will look like:
Let’s start by scaffolding the Button component.
We’ll accept two props,
size, and set some default values for those, to make those optional:
Note that our
className attribute is currently empty for now.
We will use the
size prop values to decide what Tailwind classes should go in there.
Remember: we need to make sure every single Tailwind class we’re using is present, somewhere, as a full text string for the JIT engine to find.
There are many ways to achieve this, but one approach I particularly enjoy is to use “lookup” objects.
The values represent a string of composed Tailwind utility classes that achieve the desired styles for that particular variant.
Here’s what such a lookup object would look like for the
Same deal for the
Tailwind will definitely look at these lookup tables 👀
The whole idea behind this approach is to ensure that every single class you use for your different variants is listed somewhere as a plain text string.
These lookup objects are sort of an inventory of every class you’re using across every possible style permutation.
You will enjoy looking at those too!
From a developer standpoint, lookup objects are also great: you can “see” at a glance, in a clutter-free way, what every style variant will look like.
className attribute! 😅
But what about common styles between all variants?
No matter what component you’re building, you’ll inevitably have some classes that need to be applied to every style variant, regardless of the permutation of props.
It’s a good idea to identify those classes and abstract them away.
These could stay in the Button’s
className attribute directly, but it’s kinda nice to store them in their own variable, sort of a “mini lookup” of its own, so everything style-related is colocated.
There aren’t many common styles to all variants in our
Button component case.
Here’s what it looks like:
We’ll handle focus styles in each variant, so it makes sense to opt-out of the default focus outline in our
Beware of competing classes between different variant types!
One thing to look out for is that you don’t have competing styles between different variant types.
For example, you may have:
Situations like this could lead to sneaky and unexpected results.
It’s worth spending some time thinking about splitting the style concerns of each style variant.
And make sure you have a clean separation between those.
Our styles inventory
In the case of our
Button, here’s what our complete styles inventory - our lookup objects - look like:
100% of the Tailwind classes used to build these buttons are present in this inventory.
All in plain text.
100% JIT-friendly ⚡️
All we need to do now is correctly compose those styles together in our
Composing style variants together
This step is relatively straightforward.
Essentially, we want to “merge” Tailwind classes from our
baseClasses with the appropriate
size classes from our lookup objects.
Our lookup objects represent each possible value for both these props.
That means we can use our
size prop value to reach for the correct key in both those lookup objects, like so:
We want to merge these two strings of classes with our
We’re combining three strings together, so we can do this in a template string:
That’s it! This combined string of class will dynamically adjust to the props passed to our component. This will take care of applying the right styles for any permutation of these props 🎉
Let’s put that string in our
Annnd... voila! Our
Button component now supports the styles for all the different variants.
Here’s the entire code for our component:
On the other side, what does it look like when “consuming” the component?
Here’s what actually using this component feels like for the developer:
Notice the second
Button has no props at all.
Here’s what these three buttons look like in our browser:
Since we have set
defaultProps, it will use the
primary variant if no variant prop is passed. And if no
size prop is passed, it will use the
Where are the docs, though?
Button component is nice and all, but where can developers read about what props are available and what values are expected for each props?
Should we create a documentation website?
What about TypeScript?
What would you do to improve the situation and give the consumer of our component a few more hints on how to use our
Here’s a link to the code representing where we’re right now:
Why don’t you play with it and see if you can improve the prop documentation situation?