Using Aria States To Toggle Tailwind Classes
Tags: a11y, javascript, tailwindcss
Published: 2024-03-04 11:13:35

---
title: Using Aria States To Toggle Tailwind Classes
published: true
description: "I've been refactoring my UI code to utilise aria states instead of classes or data attributes, here's how it went"
tags: accessibility, javascript, tailwind
# published_at: 2024-03-04 10:39 +0000
---

I maintain an internal UI library for a number of large sites. It's got a number of JavaScript interactions for menus, search buttons and similar.

In my first iteration of the project I used event handlers to add and remove classes directly in the code base, like this:

```javascript
desktopSearchButton?.addEventListener("click", event => {
        // Show the search box
        desktopWrapper?.classList.toggle("hidden");

        // Add active state to button
        let button = event.target.closest("button");
        button?.classList.toggle("bg-blue-800");
        button?.classList.toggle("rounded-tr-none");
        button?.classList.toggle("rounded-br-none");

        if (!desktopWrapper?.classList.contains("hidden")) {
                desktopWrapper?.querySelector("input[type=search]")?.focus();
        }
});
```
Apart from being verbose this is quite fragile code. I'm looking for a _style_ class and initializing an interactive state that is fixed inside the JS.

This means if the style of the button changed, I would also have to change the JS. Also it could break the JavaScript if you weren't very careful.

## Second iteration

I started to refactor the code to instead use data attributes:

```javascript
desktopSearchButton?.addEventListener("click", event => {
        // Show the search box
        if(desktopWrapper) {
                desktopWrapper.dataset.open = desktopWrapper.dataset.open === "true" ? "false" : "true";
        }

        // Add active state to button
        let button = event.target.closest("button");
        if(button) {
                button.dataset.open = button.dataset.open === "true" ? "false" : "true";
        }

        if (desktopWrapper?.dataset.open === "true") {
                desktopWrapper.querySelector("input[type=search]")?.focus();
        }
});
```

This is a lot more robust and easily re-useable. Now I can use [Tailwind arbitrary selectors](https://tailwindcss.com/docs/hover-focus-and-other-states#data-attributes) in the template to toggle the states:

```html
<button data-open="false" class="hidden data-[open]:block" ...>
```

## Third iteration

With this one, I really wanted to make sure that accessibility was intrinsic to the project, and not an optional extra. To that end, instead of data attributes wherever possible I used [aria states](https://tailwindcss.com/docs/hover-focus-and-other-states#aria-states) to toggle the visuals.

```javascript
desktopSearchButton?.addEventListener("click", event => {
        // Show the search box
        if(desktopWrapper) {
                desktopWrapper.ariaExpanded = desktopWrapper.ariaExpanded === "true" ? "false" : "true";
        }

        // Add active state to button
        let button = event.target.closest("button");
        if(button) {
                button.ariaPressed = button.ariaPressed === "true" ? "false" : "true";
        }

        if (desktopWrapper?.ariaExpanded === "false") {
                desktopWrapper.querySelector("input[type=search]")?.focus();
        }
});
```

```html
<button aria-pressed="false" class="hidden aria-pressed:block" ...>
```
The only disadvantage here is just an inconvenience: they have to add the aria state to the DOM. And to be honest, that is a positive friction to make them more aware of how aria states work.

Also my compiler complains when I try to make left-hand assignments with optional chaining:

```javascript
button?.ariapressed = "true"
// The left-hand side of an assignment expression may not be an optional property access.ts(2779)
```
I could escape this rule but instead opted for an `if` statement rather than risk making my entire codebase less type safe.

This way, any developers using my library would need to use aria states too, making it at least a bit better for all of our users.