Why Tailwind CSS is not good at data-driven styles? And how to deal with it?
- Published on
- Authors
- Name
- Jerry
After trying out Tailwind CSS, I quickly fell in love with it. No more dealing with CSS class names, no more toggling between CSS files and HTML/JSX/TSX files - making styling a breeze.
Whenever I encounter a visually appealing design, I always wonder how to implement it in Tailwind CSS. Recently, I came across something like this:
60%
Its essence lies in displaying a percentage through a gradient background with angles:
Then, by adding rounded corners, it turns into a circle:
Adding a pseudo-element creates a ring:
And adding the percentage text in the center completes the effect we saw initially. When using native CSS, implementing this effect is straightforward:
<div class="box">
<div class="circle">
<h2>85<small>%</small></h2>
</div>
<h3>Loading</h3>
</div>
.box {
padding: 40px 0;
width: 240px;
background: #57534e;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 20px;
color: #ffffff;
border-radius: 12px;
}
.box .circle {
position: relative;
width: 150px;
height: 150px;
background: conic-gradient(from 0deg, #fbbf24 0%, #fbbf24 0% 85%, #333333 85%, #333333 100%);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.box .circle::before {
content: '';
position: absolute;
inset: 10px;
background: #57534e;
border-radius: 50%;
}
.box h2 {
position: relative;
font-size: 2em;
font-weight: 600;
}
On top of this, we can dynamically control color and progress using two CSS variables:
<div class="box" style="--progress:60%;--color:#04e762">
<div class="circle">
<h2>60<small>%</small></h2>
</div>
<h3>Loading</h3>
</div>
/* ... */
.box .circle {
/* ... */
background: conic-gradient(
from 0deg,
var(--color) 0%,
var(--color) 0% var(--progress),
#333333 var(--progress),
#333333 100%
);
/* ... */
}
/* ... */
If you look closely, you'll notice that the text content in the ring, "60%," does not change with the CSS variables.
So, I wondered if I could use React + Tailwind CSS to achieve this effect by controlling both the text and the percentage in the style with a single variable. First, let's start with fixed styles, which can be implemented with the following code:
export default function CircleProgressIndicatorBasicTailwind() {
return (
<div className="flex flex-auto flex-col items-center gap-3 rounded-xl bg-stone-200 p-7 dark:bg-stone-600">
<div className="relative grid h-40 w-40 place-items-center rounded-full bg-[conic-gradient(var(--tw-gradient-stops))] from-green-400 from-0% via-green-400 via-10% to-gray-700 to-10% before:absolute before:inset-3 before:rounded-full before:bg-stone-200 before:content-[''] before:dark:bg-stone-600">
<h1 className="absolute m-0">
10<small>%</small>
</h1>
</div>
<h2 className="m-0">Loading Progress</h2>
</div>
)
}
10%
Loading Progress
65%
Rendering Tracker
85%
Syncing Status
However, when attempting to drive styles with data, I quickly found that Tailwind CSS struggles with this situation. Its design philosophy relies on numerous preset class names for style reuse. For instance, in the above code, to-10%
is a preset class name that changes the Tailwind CSS variable --tw-gradient-to-position: 10%;
. to-15%
corresponds to --tw-gradient-to-position: 15%;
, and so on.
But when trying to write a class name that does not exist in Tailwind, such as to-13%
, Tailwind does not recognize it (it increments in 5% intervals). To achieve this, one might use arbitrary values。
export default function CircleProgressIndicatorArbitrary() {
return (
<div className="hidden flex-auto flex-col items-center gap-3 rounded-xl bg-stone-200 p-7 dark:bg-stone-600 sm:flex">
<div className="relative grid h-40 w-40 place-items-center rounded-full bg-[conic-gradient(var(--tw-gradient-stops))] from-[#04e762] from-0% via-[#04e762] via-[13%] to-gray-700 to-[13%] before:absolute before:inset-3 before:rounded-full before:bg-stone-200 before:content-[''] before:dark:bg-stone-600">
<h1 className="absolute m-0">
13<small>%</small>
</h1>
</div>
<h2 className="m-0">Rendering Tracker</h2>
</div>
)
}
13%
Loading Progress
63%
Rendering Tracker
84%
Syncing Status
Then, if you want the progress of this bar to be passed externally, Tailwind CSS falls short.
Note: The following approach does not work:
interface Props {
progress: number
color: string
title: string
}
export default function CircleProgressIndicatorDynamicClassNames() {
return (
<div className="hidden flex-auto flex-col items-center gap-3 rounded-xl bg-stone-200 p-7 dark:bg-stone-600 sm:flex">
<div
className={`relative grid h-40 w-40 place-items-center rounded-full bg-[conic-gradient(var(--tw-gradient-stops))] from-[${color}] from-0% via-[${color}] via-[${progress}%] to-gray-700 to-[${progress}%] before:absolute before:inset-3 before:rounded-full before:bg-stone-200 before:content-[''] before:dark:bg-stone-600`}
>
<h1 className="absolute m-0">
{progress}
<small>%</small>
</h1>
</div>
<h2 className="m-0">{title}</h2>
</div>
)
}
According to the official documentation on dynamic class names, do not dynamically construct class names; any class names used should exist in full.
The most important implication of how Tailwind extracts class names is that it will only find classes that exist as complete unbroken strings in your source files.
If you use string interpolation or concatenate partial class names together, Tailwind will not find them and therefore will not generate the corresponding CSS.
Instead, make sure any class names you’re using exist in full.
If you’re using a component library like React or Vue, this means you shouldn’t use props to dynamically construct classes.
For more detailed explanations and examples, you can click on the link above to read.
To overcome this limitation, my solution is to use Tailwind CSS for basic styles and resort to inline styles when Tailwind CSS cannot achieve the desired effect. This way, you can enjoy the convenience provided by Tailwind while also implementing complex effects like dynamic styles.
// CircleProgressIndicator.tsx
interface Props {
progress: number
color: string
title: string
}
function CircleProgressIndicator({ progress, color, title }: Props) {
return (
<div
className="flex flex-auto flex-col items-center gap-3 rounded-xl bg-stone-200 p-7 dark:bg-stone-600"}
>
<div
className="relative grid h-40 w-40 place-items-center rounded-full transition-all before:absolute before:inset-3 before:rounded-full before:bg-stone-200 before:content-[''] before:dark:bg-stone-600"
style={
{
'--circleProgressColor': `${color}`,
'--circleProgress': `${progress * 100}%`,
background:
'conic-gradient(from 0deg, var(--circleProgressColor) 0%, var(--circleProgressColor) 0% var(--circleProgress), #333333 var(--circleProgress), #333333 100%)',
} as React.CSSProperties
}
>
<h1 className="absolute m-0">{(progress * 100).toFixed()}%</h1>
</div>
<h2 className="m-0">{title}</h2>
</div>
)
}
0%
Loading Progress
Conclusion
Tailwind CSS provides a fresh way of writing styles. Its design philosophy, relying on numerous preset class names for style reuse, works well in most cases. However, when trying to implement dynamic styles, such as controlling styles based on external variables, Tailwind CSS falls short. In such cases, we can use inline styles to achieve the desired effect on top of Tailwind CSS's convenience.