How to style element descendants with Tailwind CSS
- Published at
- Updated at
- Reading time
- 4min
I'm maintaining a project built with Tailwind at work. I'm no daily Tailwinder, but usually, I manage just fine. But recently, a trivial task took me quite some time to get right. I wanted to rotate an SVG icon depending on the details
element's :open
state.
Getting started with Tailwind is straightforward (a CSS property just maps to a class name, right?), but I still have to wrap my head around advanced features like class-based element relationships.
How would you achieve the following CSS in Tailwind?
details:open svg {
rotate: 0.5turn;
}
When googling around, you'll find the advice to "just use" CSS and import a CSS file somewhere. Sure, this works, but what's the point of this? Tailwind promises to solve the CSS maintenance problem, so I don't have to think about class names and where to put which parts of my CSS.
I'm not gonna sabotage myself by adding custom CSS. So this option was a big "NO NO" for me. ๐
The second option I discovered was to rely on parent-based styles.
<details class="group">
<summary>
Open me
<svg class="group-open:rotate-180">
<!-- SVG stuff -->
</svg>
</summary>
</details>
You define a parent boundary (group
) and then style an element depending on the parent state (group-open:rotate-180
). The selector translates to "If an element's group
parent matches the [open]
attribute selector, rotate it by 180 degrees".
Here's the resulting CSS:
.group[open] .group-open\:rotate-180 {
--tw-rotate: 180deg;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
The group
class doesn't have styling and only matters when used with the group-modifier (group-open
).
At first, this approach felt a little icky to me, but it is very smart!
Here's a Tailwind example to see the group
selector in action .
While the group selector works, it twists my brain because it's somehow backward. Could you somehow style elements more like in the CSS way?
Arbitrary variants allow you to write custom combined CSS selectors that aren't included in Tailwind.
If you want to style all paragraphs in a div
and avoid adding a class to every p
element, use this funky Tailwind syntax โ [&_p]:text-xl
. It roughly translates to "Increase the font size (text-xl
) of all paragraphs in this element ([&_p]
)".
Here's it in action.
<div class="[&_p]:text-xl">
<p><!-- paragraph stuff --></p>
</div>
And here's the resulting CSS:
.\[\&_p\]\:text-xl p {
font-size: 1.25rem;
line-height: 1.75rem;
}
To solve the problem and rotate our SVG, we could rely on another arbitrary variant โ [&_svg]:open:-rotate-180
.
<details class="[&_svg]:open:-rotate-180">
<summary>
Open me
<svg>
<!-- SVG stuff -->
</svg>
</summary>
</details>
And this is the compiled CSS:
.\[\&_svg\]\:open\:-rotate-180[open] svg {
--tw-rotate: -180deg;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
The more I dive into custom Tailwind features, the more I'm amazed. Some features are very funky! Odd, but funky!
Here's a Tailwind example to see an arbitrary variant in action .
In the project, I went with the arbitrary variant solution. But after reading more about it, I'll try to get my head around the group selector. Why?
Tailwind doesn't only help with CSS maintenance but also with CSS performance. Large CSS codebases tend to grow and grow over time. Do you need another component? Great, add another 300 lines of CSS! Do you need a button variation? Easy, add another 50 lines...
Hand-written CSS is hard to maintain but also leads to slower sites. The longer you work on a project, the bigger the CSS becomes. And CSS blocks rendering. The more CSS you ship, the longer you wait for the first paint.
In Tailwind, all classes are already defined, and the CSS size remains somewhat the same. Sure, the HTML can look terrible, but if the render-blocking CSS isn't becoming enormous, I'm happy to take the ugly HTML.
Tailwind parses project templates to remove unused CSS classes. If you're starting, your CSS will grow because more Tailwind classes will make it into production. But I think the argument holds: if you used enough Tailwind classes your CSS bundle size will grow less than with hand-written CSS.
But what happens when you use fancy arbitrary variants like [&_svg]:mt-4
? Tailwind has to parse your markup and make up the CSS on the fly. This new class variant is then added to your compiled CSS and it increases the overall CSS size. And I don't like that. ๐
So for me, the winner is option 2 because it feels like the natural Tailwind approach and doesn't increase the CSS size. Win-win! ๐
Do you know of other options that I could have used here? If so, I'd love to hear them!
Join 5.5k readers and learn something new every week with Web Weekly.