Data-Attributes Magic with Tailwind CSS & Stimulus
Tags: tailwindcss, stimulus, hotwire, rails
Published: 2024-03-28 20:00:00

This article was originally published on [Rails Designer](https://railsdesigner.com/data-attributes-magic/)

---

Changing the way some, or all, UI elements on an user interaction (click, hover, etc.) 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. As I use Tailwind CSS exclusively that's what these examples use.

```html
<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](https://tailwindcss.com/docs/hover-focus-and-other-states#differentiating-nested-groups), opposed to just `group` (which is a good thing to do any way).

I don't think this solution is really bad, especially if you name the CSS class (`.nav-opened`) well. But now check out the option with a data attribute.

```html
<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 Tailwind CSS. It uses the [`open`-modifier](https://tailwindcss.com/docs/hover-focus-and-other-states#open-closed-state).

```html
<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:

```html
<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](https://railsdesigner.com/)).

```js
// 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! With the above example, this is how it's used:

```html
<nav data-controller="data-attribute" 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.