Atif Aiman
Posted on June 9, 2023
Salam and hello everyone!
Whoa! It has been a long time, since I wrote the last article here, but here we are! This time, I will write about things that "I believe" some of the developers didn't know that you don't have to do conditions for classes in HTML!
To explain further, let us go deeper on what is the problem that I attempt to highlight here.
Style States For A HTML Component
Let me bring a use case here. Just imagine that you have tabs, and you need to track the state whether the tab is active or not. Of course, one way of doing it is using Javascript!
<nav id="tab">
<a href="#" />
<a href="#" />
<a href="#" />
</nav>
<script>
const activeTab = 0;
const navTab = document.getElementById("tab");
const targetTab = navTab.children[activeTab];
targetTab.className.add("tab-active");
</script>
For a vanilla Javascript, it makes sense that you can use className.add()
to add the tab-active
class, right?
So, let's head to the CSS part!
#tab a {
// Initial state of tab
}
#tab a.tab-active {
// Active state of tab
}
That's cool! With the change of activeTab
value, we will trigger the tab-active
based on that. Keep in mind that you need to remove the class if it is no longer active!
This will be one way of solving the conditional issue with vanilla code. Let's jump to one of the framework, and we will try to replicate this thing, shall we? And of course, I will choose React!
Things Are Going To Get Tough
By using React, stuffs are getting easier. But are we? Let us try to replicate a simple way of doing the same thing I previously attempted to do.
const Tab = (tabList) => {
const [activeTab, setActiveTab] = useState(0);
return (
<nav id="tab">
{
tabList.map((tabConfig, index) => (
<a href={tabConfig.href} className={activeTab === index ? 'tab-active' : ''} />
)
}
</nav>
);
};
Nice! Except, that it is unnecessary to set the className
to an empty string if it is not active, right? This might affect the readability as the component grows!
So, let me introduce to you, the data-attributes
!
Some of you might heard about data-attributes
, and you might be interested in this!
const Tab = (tabList) => {
const [activeTab, setActiveTab] = useState(0);
return (
<nav id="tab">
{
tabList.map((tabConfig, index) => (
<a href={tabConfig.href} data-active={activeTab === index} />
)
}
</nav>
);
};
Not much change huh? Okay, let us take this slow from here.
What I did just now, is that I set data-active
to the <a>
instead of className
. So, I don't have to declare unnecessary className
conditions.
Before we are going to do that, we need to change something to the CSS we had previously.
#tab a {
// Initial state of tab
}
#tab a[data-active] {
// Active state of tab
}
So, I removed the tab-active
class, and replaced it to a[data-active]
instead.
What are we improving right here? It is readability of the implementation. Instead of playing the guessing game of which class are we going to trigger, we just set the condition to the data attributes. With this, developer will understand which element/class/id is having states, as we treat pseudo selectors, like :hover
!
.myComponent {
// Initial state of component
}
.myComponent:hover {
// Hover state of component
}
.myComponent[data-active] {
// Active state of component
}
Can I use value other than boolean? Well, of course, you can set other values to the data attributes, with a condition that it should be string.
.myComponent {
// Initial setting of component
}
.myComponent[data-variant="primary"] {
// Primary state of component
}
This sounds interesting, right? Not convincing enough? Let us try to introduce a CSS framework - Tailwind CSS π
Here Comes The Trouble
Let us bring the original problem. Now with Tailwind!
const Tab = (tabList) => {
const [activeTab, setActiveTab] = useState(0);
return (
<nav id="tab">
{
tabList.map((tabConfig, index) => (
<a href={tabConfig.href} className={activeTab === index ? "text-blue-500" : ""} />
)
}
</nav>
);
};
Well, this is not wrong, except that this might work on your local machine, but not in production after build. You might find the glitch, only to learn that Tailwind fails to tree shake the class for production build. The classes that we add in the condition only works if the behaviour is expected, but that is not the case. So, what are my options?
Of course, you can just provide condition for the whole component, right? π€
const Tab = (tabList) => {
const [activeTab, setActiveTab] = useState(0);
return (
<nav id="tab">
{
tabList.map((tabConfig, index) => activeTab === index
? (
<a href={tabConfig.href} className="text-blue-500" />
) : (
<a href={tabConfig.href} />
)
}
</nav>
);
};
One, we are duplicating codes, and two, we are reducing the readability with all those ternaries. Just imagine that the next developer have to come in and guess what are you trying to do here. So, I am bringing the better suggestion! Let's use data attributes!
const Tab = (tabList) => {
const [activeTab, setActiveTab] = useState(0);
return (
<nav id="tab">
{
tabList.map((tabConfig, index) => (
<a href={tabConfig.href} className="data[active]:text-blue-500" data-active={activeTab === index} />
)
}
</nav>
);
};
Now, let's us break this into parts:
- I am adding
data-active
to thea
element to control the state. - I am adding
data[active]:
to theclassName
to trigger the style if thedata-active
returns true. - As the
className
is no longer in conditions, the behaviour can be expected, thus this approach is safe from tree shaking. Thus, it works better!
Okay, let's spice things up! I will add all needed styles using Tailwind to see how it will affect the readability when using data attributes.
const Tab = (tabList) => {
const [activeTab, setActiveTab] = useState(0);
return (
<nav id="tab">
{
tabList.map((tabConfig, index) => (
<a href={tabConfig.href} className="px-2 py-4 text-black bg-slate-200 data[active]:text-blue-500 data[active]:bg-zinc-200" data-active={activeTab === index} />
)
}
</nav>
);
};
As usual, the dilemma of using Tailwind is to increase the class names, right? To improve this, I will "kinda" introduce my own way of breaking classes into parts.
/**
* @param classNames string[]
**/
const c = (classNames) => classNames.join(" ");
const Tab = (tabList) => {
const [activeTab, setActiveTab] = useState(0);
return (
<nav id="tab">
{
tabList.map((tabConfig, index) => (
<a href={tabConfig.href}
className={c([
"px-2 py-4 text-black bg-slate-200",
"data[active]:text-blue-500 data[active]:bg-zinc-200"
])}
data-active={activeTab === index}
/>
)
}
</nav>
);
};
What I did, is I just separate it into lines, then use the join function to reassemble the class again. Since it doesn't involve any conditions, the class are still safe π. With this, I can separate className
to lines, which each represents each state. Readable, tree-shakable and working!
You can learn more about how data attributes can be used in Tailwind if you are interested!
Conclusion
Data attributes are beneficial for a lot of thing, and CSS string is one of them. We can leverage CSS data attributes for this purpose. Of course, this is just one of the way to do this, but data attributes are just too awesome to let it by. Now you know!
Well, I hope this strengthen your knowledge on CSS. Keep healthy folks, and peace be upon ya!
Posted on June 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.