Data-Attributes Magic with Tailwind CSS & Stimulus

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:

  1. add the CSS class(es) to every element that needs changing;
  2. add CSS classes to the parent element (eg. <span class="nav-opened">) to enable cascading of additional styles from it to child elements;
  3. 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.

Product-minded Rails notes

Once a month: straightforward notes on improving UX in Rails—what to simplify, what to measure, and UI/frontend changes that move real usage.

Over to you…

What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!

Comments are powered by Chirp Form

Want to read me more?