Changing the way some, or all, UI elements on an user interaction (click, hover, etc.) look is really common. So you better have a good process to use this common flow.
There are basically three ways to go about this:
- add the CSS class(es) to every element that needs changing;
- add CSS classes to the parent element (eg.
<span class="nav-opened">
) to enable cascading of additional styles from it to child elements; - add a data attribute to the parent element (eg.
<span data-nav-opened>
) to enable cascading of additional styles from it to child elements.
I don’t think I need to go over option 1, as that would never be a maintainable option. So let’s check out option 2 first, followed by option 3. Withsome TailwindCSS data attribute magic this is really straight-forward.
<nav class="group/nav nav-opened">
<ul class="hidden group-[.nav-opened]/nav:block">
<li>Item here</li>
</ul>
</nav>
I’m using named grouping, opposed to just group
(which is a good thing to do any way).
I don’t think this solution is poor, especially if you name the CSS class (.nav-opened
) well.
But check out the option with a data attribute:
<nav data-nav-opened class="group/nav">
<ul class="hidden group-data-[nav-opened]/nav:block">
<li>Item here</li>
</ul>
</nav>
Quite similar, right? Actually it is a bit longer than the CSS class option. But the more important point is that it keeps concerns separated (CSS classes for aesthetics and data-attributes for “states” and behavior).
Quick pro-tip. The following works just as well with the Tailwind group data feature. It uses the open
-modifier.
<nav open class="group/nav">
<ul class="hidden group-open/nav:block">
<li class="text-orange-500">Item here</li>
</ul>
</nav>
So far, the examples shown were really simple. But there’s no reason you cannot expand what happens when the parent’s state changes. A common thing you see is a chevron that changes rotation. Something like this:
<nav data-nav-opened class="group/nav">
<button class="flex items-center gap-1">
Open
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" data-slot="icon" class="w-3 h-3 transition group-data-[nav-opened]/nav:rotate-180">
<path fill-rule="evenodd" d="M12.53 16.28a.75.75 0 0 1-1.06 0l-7.5-7.5a.75.75 0 0 1 1.06-1.06L12 14.69l6.97-6.97a.75.75 0 1 1 1.06 1.06l-7.5 7.5Z" clip-rule="evenodd"/>
</svg>
</button>
<ul class="hidden group-data-[nav-opened]/nav:block">
<li>Item here</li>
</ul>
</nav>
Notice the svg
inside the button that flips 180º
when data-nav-opened
is added? From here on out, it’s only your imagination that is the limiting factor.
How can we combine this with Stimulus?
Stimulus is a great choice for user interactions like this as it’s really declarative. This works great together with the Tailwind CSS setup explained above.
The following Stimulus controller is one I use often (and comes together with some of the Rails Designer Components).
// app/javascript/controllers/data_attribute_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static values = { key: String };
disconnect() {
this.element.removeAttribute(this.keyValue);
}
toggle() {
this.element.toggleAttribute(`data-${this.keyValue}`);
}
}
That’s the whole controller! This is how it’s used:
<nav data-controller="data-attribute" data-data-attribute-key-value="nav-opened" class="group/nav">
<button data-action="data-attribute#toggle" class="flex items-center gap-1">
Open
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" data-slot="icon" class="w-3 h-3 transition group-data-[nav-opened]/nav:rotate-180">
<path fill-rule="evenodd" d="M12.53 16.28a.75.75 0 0 1-1.06 0l-7.5-7.5a.75.75 0 0 1 1.06-1.06L12 14.69l6.97-6.97a.75.75 0 1 1 1.06 1.06l-7.5 7.5Z" clip-rule="evenodd"/>
</svg>
</button>
<ul class="hidden group-data-[nav-opened]/nav:block">
<li>Item here</li>
</ul>
</nav>
And that’s how, by combining two—really developer-friendly—tools, you can create usable (Rails) applications with a minimal amount of code.