DEV Community

loading...

Why you don't need every CSS pseudo-selector in Tailwind CSS

wheelmaker24 profile image Nikolaus Rademacher ・4 min read

Tailwind is great. And it is often misunderstood. If you want to know why Tailwind doesn't have a lot to do with CSS inline styles, read my previous post.

But today it's about something else: People often get confused when it comes to CSS pseudo-selectors in Tailwind.

Tailwind supports the most important pseudo-selectors out of the box, e.g. :hover, :focus and :active. You may use these selectors as "variants" in front of your utility-class like this:

<button class="bg-black hover:bg-gray-500 text-white">
  my button
</button>
Enter fullscreen mode Exit fullscreen mode

As expected, this example would add a gray background on hover to your button element.

It makes sense to use Tailwind variants for different states (e.g. active, hover, visited, focus, …), as rebuilding these states via JavaScript will not give you the same performance. But when it comes to other pseudo selectors than these, it is at least worth discussing:

::before and ::after elements in Tailwind

::before and ::after elements are not supported by Tailwind out of the box. While you can add them via plugins, I would not recommend doing so.

Let's think about what these CSS selectors do: They render pseudo elements that are not directly visible in your DOM. Traditionally this makes sense in order to keep the markup clean. But: "Utility-first" means "component second". And the component abstraction allows us to put any element inside.

Take an external link for example: If you want to add a right arrow (→) after the link, traditionally you could do this with the ::after selector:

/*a*/.external-link {
  text-decoration: underline;
  color: red;
}
/*a*/.external-link::after {
  content: '\u2192';
  color: gray;
}
Enter fullscreen mode Exit fullscreen mode

Nowadays, creating an <ExternalLink> component with Tailwind and React for example would look somewhat like this:

const ExternalLink = ({ children, href }) => (
  <a href={href} classNames="underline text-red-500">
    {children}
    <span className="text-gray-300" aria-hidden="true">
      &#8594;
    </span>
  </a>
);
Enter fullscreen mode Exit fullscreen mode

It is perfectly fine to add an additional element to your render function. Just remember to add an aria-hidden attribute to make the element invisible to screen readers and search robots, just like CSS pseudo-elements would be.

The component would be used like this:

<ExternalLink href="https:://site/path">Link text</ExternalLink>
Enter fullscreen mode Exit fullscreen mode

Numeric operators (nth-child()) in Tailwind**

Next, let's have some fun with numbers: What if you want to style a list with alternating background colors? Traditionally with CSS, you would probably do something like this:

li:nth-child(even) { background-color: gray; }
li:nth-child(off) { background-color: white; }
Enter fullscreen mode Exit fullscreen mode

Well, Tailwind got you covered and provides variants for this:

<li class="even:bg-gray-500 odd:bg-white">
Enter fullscreen mode Exit fullscreen mode

Since even: and odd: variants are not enabled by default, you need to configure them in your tailwind.config.js file. Just remember that every variant will increase the size of your CSS output.

Similarly you may activate first: and last: variants if you want to target the first or last element like you would traditionally do with the CSS :first-child and :last-child selectors.

However, if you want to do special ones like nth-child(myIndex) or nth-child(myFormula) (e.g. nth-child(3n+1)), you won't find any Tailwind variants for that.

But: "Utility-first" also means "component second": You will almost certainly use a component abstraction for your Tailwind styles anyways – be it React, Angular, Vue, Svelte or anything else.

Having a component stitched together via JavaScript also means that you already have a place where to put programming logic. So if you don't want to increase your output CSS file size by adding Tailwind variants for every possible utility-class, you actually don't need to do so.

Take the list example: In React, you will probably use a .map() function to map over the list items. Just add the index as the second argument to your arrow function and use it to create booleans (flags) that you can use in your classNames array:

const MyList = ({ items }) => {
  const renderListItems = (item, index) => {
    const isSecond = index === 1;
    const isThird = index === 2;
    const classNames = [
      isSecond && "bg-gray",
      isThird && "bg-white"
    ].join(" ");
    return <li className={classNames}>{item.text}</li>;
  };
  return <ul>{items.map(renderListItems)}</ul>;
};
Enter fullscreen mode Exit fullscreen mode

Admittedly, this example seems a lot more complex than the CSS one, but sometimes using JS logic gives you more flexibility than using CSS logic. This makes sense especially when you need to target the same elements in your JavaScript code anyway – why duplicate this logic in your CSS file? And with the utility-first approach this is perfectly fine as everything will be abstracted away into your component anyway.

Using array as the third argument of your map function you could also target the last element of your list like this:

const MyList = ({ items }) => {
  const renderListItems = (item, index, array) => {
    const isFirst = index === 0;
    const isLast = index === array.length - 1;
    const classNames = [
      isFirst && "bg-gray",
      isLast && "bg-white"
    ].join(" ");
    return <li className={classNames}>{item.text}</li>;
  };
  return <ul>{items.map(renderListItems)}</ul>;
};
Enter fullscreen mode Exit fullscreen mode

So you might not need to activate any needed variant in Tailwind. Just weigh CSS output size against JavaScript complexity. Not only when using JS to build static HTML pages the latter might even be an advantage.

So yes, you actually do not need any CSS pseudo-selector when using Tailwind and a component abstraction :)

Concerns?

But CSS rendering is quicker than JavaScript rendering!

Yes, that's true in most cases. especially for interactive states like :active, :hover, :visited – that's why I would always recommend using Tailwind variants for these.

But when it comes to pseudo-elements (::before and ::after) or numeric operators (:nth-of-child, ::last-of-type, ...), it doesn't make a difference, because you are already using JavaScript for rendering the component and mapping over its children anyway. In fact it's probably even quicker as you don't need to rely on CSS overwrites and can avoid CSS specificity issues.

But I don't like to flood my component's markup with thousands of classnames!

Well, stay tuned for my next post with strategies for structuring Tailwind classes then ;)

Discussion (4)

pic
Editor guide
Collapse
cairofelipedev profile image
cairofelipedev

Há um mês iniciei no TailwindCSS com Laravel. Até hoje eu não consigo utilizar o taildiwncss.config se puder me ajudar ficaria muito grato

Collapse
wheelmaker24 profile image
Nikolaus Rademacher Author

Hey – I've never used Tailwind with Laravel. I can just recommend following this guide: tailwindcss.com/docs/guides/laravel – hope this helps.

Collapse
giovannyds profile image
Giovanny • Edited

this is actually solved with Tailwind JIT. their new brand compiler. almost all, but nth. which makes sense is not covered

Collapse
wheelmaker24 profile image
Nikolaus Rademacher Author

In a way it is, regarding the dev build. For production at least when using the purge mode nothing will really change.

No matter how your CSS gets generated, each Tailwind variant that you need will create a bigger CSS file. It's just worth thinking about whether you really need it.