Tying Tailwind styling to ARIA attributes
Tags: tailwindcss, a11y, aria, css
Published: 2021-06-24 22:20:30

Note that [Tailwind 3.2 adds some built-in support for ARIA attributes](https://github.com/tailwindlabs/tailwindcss/discussions/9574#:~:text=%3C/div%3E-,New%20variants%20for%20aria%2D*%20attributes,-You%20can%20now) but most of this article still applies and is useful for more custom/advanced usage.

----

[Adrian Roselli](https://adrianroselli.com/bio) is an accessibility professional who I have followed for a while and have learnt a lot from. His blog is an amazing resource, and I will forever be grateful to him for his blog post [Link Targets and 3.2.5](https://adrianroselli.com/2020/02/link-targets-and-3-2-5.html) which I _always_ refer to when trying to convince a client that, no, external links don't _have_ to open in a new window/tab, and most users would really prefer to be in control of that sort of thing themselves, actually!

Recently he posted a question about Tailwind CSS, and relatively complex CSS selectors:

{% twitter 1408124998389317644 %}
{% twitter 1408125411209449484 %}

The idea here is that we deny the component visual styling and functionality unless the developer has remembered to set important accessibility related attributes such as `aria-expanded`, `role`, and `tabindex`. This is a nifty idea that has been around for a while and is commonly touted as a way to force developers to remember their ARIA attributes and to consider things like semantic markup and focus order/states. I am completely on board with this approach and have previously used it in my own work for exactly the same reasons.

Here's an similar idea from [Sara Soueidan](https://www.sarasoueidan.com) that references how she uses `aria-expanded` as a styling hook, so if someone forgets it (or misspells it!) then the component just doesn't work until they fix their mistake:

{% twitter 1319991802716393473 %}
{% twitter 1319992242262757377 %}

We can presume that someone like Sara might use something like this:

```css
.some-component__button[aria-expanded="false"] + .some-component__child {
  display: none;
}
```

Let's take a look at how we'd do that with Tailwind, and then later we can take a look at using some more complex selectors that are close to (but not quite the same as) Adrian's examples.

Before we get started, a quick disclaimer… Tailwind doesn't work for everyone, and it doesn't work for all projects. It makes me cringe everytime I see Tailwind users being too defensive or pushy on Twitter, and I cringe too when I see people who dislike the idea of utility classes reacting equally as vitriolically.

I tend to stay out of such discussions, but I thought I could make a thoughtful contribution to Adrian's question. I know from his writing that he's very thorough and knowledgeable, and I hoped that I might be able to show him something new about Tailwind that might have not been obvious from all the heated Twitter discourse of the past. Some of the Twitter discourse seems to come from people who don't have much Tailwind experience, or who don't grasp how it can be used as part of a 'balanced diet' alongside other CSS techniques. I also feel that Tailwind gets a bit of a bad reputation because it's sometimes used by people new to front-end development (maybe via a React bootcamp or similar) or self-proclaimed 'full-stack' developers who don't know enough about accessibility because of a lack of training, knowledge, time, or desire - but from my experience it's perfectly possible to use utility classes to create quality pieces of web development craft that are entirely accessible.

A second disclaimer is that it's **absolutely fine to use code snippets like the one below in regular CSS files (or Vue `<style>` blocks, etc) _alongside_ Tailwind**:

```css
[role="region"][aria-labelledby][tabindex]:focus {
  outline: .1em solid rgba(0,0,0,.1);
}
```

Tailwind is _utility-first_ not _utility-only_, and anyone who disagrees with that, in my opinion, is being unnecessarily obstructive. I have been using Tailwind on [large](https://www.ltmuseum.co.uk), [long-running](https://www.uwl.ac.uk) projects since 2018 and we use **plenty** of regular CSS, wherever regular CSS is the best tool for the job. It's definitely not something you need to shy away from doing, and this ability to add custom CSS is often something that gets lost (or perhaps ignored for the sake of trying to make a point) in tweets like the one below:

{% twitter 1404467229857665027 %}

Really, this whole blog post could just be **'It's okay to add your own hand-written custom CSS alongside the utility classes generated by Tailwind'**.

I think that it's best to think of Tailwind as a handy tool for generating a bunch of utility classes for you (rather than generating them all yourself with a load of Sass loops), but you don't have to use _only_ these classes to get your job done; if you need to use things like `:nth-child()`, `:first-of-type`, or complex selectors then a hybrid approach of utilities and hand-written CSS is often sensible.

With all that said, let's go back to Sara's example of working with Tailwind to tie the `display` CSS property to an `aria-expanded` value.

## Adding a `aria-expanded` custom component style that hides things, using the Tailwind API

It turns out that Tailwind _expects_ us to want to add styles like this. It gives us tools deliberately intended for us to add custom CSS, such as the [`@layer` directive](https://tailwindcss.com/docs/functions-and-directives#layer) which allows custom CSS to be inserted at the appropriate point in the generated Tailwind CSS file, depending on if it's a base style, a custom utility, or a component specific style. You can also, of course, just have a completely separate CSS file for your non-Tailwind classes – that's what I do at work at the moment. The separate file is managed by Sass and we use it to add custom styles and any Sass dependencies that we need.

Here's how we'd add a new component style in the CSS file that is processed by Postcss to add all the Tailwind utilities: 

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer component {
  .some-component__button[aria-expanded="false"] + .some-component__child {
    display: none;
  }
}
```

You can see an [example of this new component style in this Tailwind Play link](https://play.tailwindcss.com/NhN67vXfjQ).

If we were confident enough about applying our rules globally, we could add them as base styles like so:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  /* In reality, such a general selector is probably
     a bad idea! But this is just an example… */
  button[aria-expanded="false"] + * {
    display: none;
  }

  /* This is a safer selector for things like a div 
     wrapper around a scrollable/responsive table. */
  [role="region"][aria-labelledby][tabindex]:focus {
    outline: .1em solid rgba(0,0,0,.1);
  }
}
```

Now, let's try something a bit more complex. This next approach will rely on specific classes being added to the component which moves away a bit from Adrian's and Sara's idea of global rules to enforce accessibility attributes, but it still follows the principle of denying styling or functionality until the correct ARIA attribute(s) are added.

## Creating a custom Tailwind variant that uses `aria-expanded` to hide a sibling

In Tailwind terminology, a 'variant' is something that prefixes a CSS selector to have it only apply in certain situations. For example, a hover, focus, or active state, or where an element is the first-child of its parent. The `focus:ring-blue-600` class would correspond to this CSS:

```css
.focus\:ring-blue-600:focus {
  --tw-ring-opacity: 1;
  --tw-ring-color: rgba(37,99,235,var(--tw-ring-opacity));
}
```

There are a bunch of other variants too, for example a `motion-safe:hover:scale-110` class corresponds to this CSS:

```css
@media (prefers-reduced-motion: no-preference) {
  .motion-safe\:hover\:scale-110:hover {
    --tw-scale-x: 1.1;
    --tw-scale-y: 1.1;
    transform: var(--tw-transform);
  }
}
```

Let's make a new `predecessor-not-expanded` variant that targets the sibling of a button element that has the `aria-expanded` attribute value of `false`. We could do this with good-old regular CSS using the `@layer` directive that I mentioned earlier, or sticking this CSS in a separate CSS file, but for the sake of completeness let's look at how we _could_ do this using the Tailwind API. 

What we are going to do is produce a custom variant by creating a Tailwind plugin in our `tailwind.config.js` file, [like this example that targets the `required` HTML attribute for `input` elements](https://tailwindcss.com/docs/hover-focus-and-other-states#creating-custom-variants).

We can write some JavaScript inside `tailwind.config.js` that uses the Tailwind API to create our new `predecessor-not-expanded` variant:

```javascript
const plugin = require('tailwindcss/plugin')

module.exports = {
  mode: 'jit',
  plugins: [
    plugin(function ({ addVariant, e }) {
      addVariant('predecessor-not-expanded', ({ modifySelectors, separator }) => {
        modifySelectors(({ className }) => {
          return `[aria-expanded='false'] + .${e(
            `predecessor-not-expanded${separator}${className}`
          )}`
        })
      })
    }),
  ],
}
```

When Tailwind detects a class like `predecessor-not-expanded:hidden` it will generate CSS that looks like this:

```css
[aria-expanded='false'] + .predecessor-not-expanded\:hidden {
  display: none;
}
```

We can now use our new variant in our HTML markup, like so:

```html
<!-- https://www.w3.org/WAI/GL/wiki/Using_the_WAI-ARIA_aria-expanded_state_to_mark_expandable_and_collapsible_regions#Examples -->
<button
  id="button1"
  class="bg-purple-100 px-5 py-3 rounded"
  aria-controls="topic-1"
  aria-expanded="false"
>
  <span>Show</span> Topic 1
</button>
<div
  id="topic-1"
  role="region"
  tabindex="-1"
  class="predecessor-not-expanded:hidden pt-2"
>
  Topic 1 is all about being Topic 1 and may or may not have anything to do with
  other topics.
</div>
```

In the example above, the 'Topic 1 is all about…' text will be hidden, because the `button` element that precedes it has an `aria-expanded` value of `false`.

You can see an [example of how that HTML and CSS combination renders using this Tailwind Play link](https://play.tailwindcss.com/CHjiizeE3M).

Let's take it a step further and create a `group-expanded` variant so we can switch between 'show' and 'hide' text in our component. This will be similar to the [`group-hover` option](https://tailwindcss.com/docs/hover-focus-and-other-states#group-hover) in Tailwind already.

We can do this by creating two more custom variants:

```javascript
plugin(function ({ addVariant, e }) {
  addVariant('group-expanded', ({ modifySelectors, separator }) => {
    modifySelectors(({ className }) => {
      return `.group[aria-expanded='true'] .${e(`group-expanded${separator}${className}`)}`
    })
  })
}),
plugin(function ({ addVariant, e }) {
  addVariant('group-not-expanded', ({ modifySelectors, separator }) => {
    modifySelectors(({ className }) => {
      return `.group[aria-expanded='false'] .${e(`group-not-expanded${separator}${className}`)}`
    })
  })
}),
```

…and then adding a `group` class to our `button` element, and updating the content of the `button` to read:

```html
<span class="group-not-expanded:hidden">Hide</span><span class="group-expanded:hidden">Show</span> Topic 1
```

You can see an [example of the `group-expanded` and `group-not-expanded` variants in this Tailwind Play link](https://play.tailwindcss.com/a1jSNr5Gtt?file=config).

-----

These examples aren't _quite_ the same as the ones mentioned in Adrian's tweets, but they show how a similar thing can be done. 

Generally, I'd just do the sort of this Adrian is talking about in custom CSS. That said, one slight advantage of creating a custom variant rather than writing custom CSS is if you are going to be using the variant to do a lot of different things. Once we've made the variant, we can use it for anything else, just by adding new classes to our component template, and without having to go back into our CSS file and add new property and value pairs. Here's a [jazzier example where we are tying a whole bunch of various element's CSS properties to the `aria-expanded` attribute's value, without writing any new CSS ourselves](https://play.tailwindcss.com/wyVUjGZnKU) - the design is odd but it shows how the variant can be used to do whatever you want it to.

Hopefully this blog shows that a) Tailwind is not an all-or-nothing approach and that it's fine to add your own CSS alongside it, and b) that if you want to add custom, relatively complex selectors in front of your Tailwind utilities then that is absolutely possible too.

-----

# Postscript (added a day after publishing)

I like this approach of tying functionality and styling to ARIA attributes to enforce good HTML practices, but it's not one I always use personally (at the moment). On projects where I am reviewing pull requests (so I can keep an eye on the markup), and where we are already using JavaScript for a component, I would use a declarative and reactive/data-driven approach like the one shown in the Codepen below. Here, the styling and the `aria-expanded` attribute value are both controlled by JavaScript, based on the value of the `open` property within the component's data object. If we change the value of `open` then the class bindings and the ARIA attribute values automatically adjust - we don't need to remember to toggle them manually.

{% codepen https://codepen.io/philw_/pen/mdWNxLQ default-tab=html,result %}

This approach does mean you lose the failsafe of the component stopping to function or be styled correctly if the ARIA attribute is accidentally removed. The benefit is that it makes it much easier to make changes to the markup (for example adding a new element after the `button` but before the expandable element) without running into issues where our CSS is completely dependent on our HTML structure never changing.

This introduces a bit of a moral quandary… We have switched to an approach that is more developer-friendly (e.g. less tight coupling of markup and styles, easier to make changes without updating the CSS, less chance of regression bugs) at the expense of an approach that is more user-friendly (e.g. making it less likely for ARIA bugs to make it to production). In this case we could make sure that the ARIA attributes are always present and have the correct values on click by writing automated tests, which would mitigate my concern about favouring developer experience over user experience (which is normally a big no-no).