Skip to content

Enforcing Accessible States in CSS

Published on

Historically, I’ve been a fan of using state classes, prefixed with a namespace like .is- or.has- . And I’ve seen a lot of developers go this route as well.

However, the more I work with state classes the more I realize they often incur in a rather pernicious form of code redundancy.

To understand this, let’s take a look at an obvious example of redundancy in code:

<!-- Hey! Just so you know, this button is disabled. Also, this comment is unnecessary, stating the obvious and adding another thing to be maintained w/ no benefits -->

<button class="c-button" disabled>Hello</button>

Now take this example:

<button class="c-button is-disabled" disabled>Hello</button>

Both of these examples are cases of redundancy, only #2 is much more subtle. Here’s why:

You’ll still need the disabled attribute to properly indicate your button’s state to assistive technology. So, in this case, .is-disabled is redundant and introduces an extra step in our code (we have to toggle both the attribute and the class) without any added benefits.

What I’m proposing is that we use ARIA states and HTML5 state attributes as styling hooks for our UI States. This is not new or revolutionary, and has been with us for a long time:

.c-input {
  color: black;

.c-input[aria-invalid="true"] {
  color: red;

Using the attribute as a styling hook won’t do any harm in terms of specificity since attribute selectors (e.g: [disabled] or [aria-disabled]) carry the same specificity as a normal class.

However, there’s a lot to win from this approach, namely:

  • Less code to maintain: an attribute + a class vs just an attribute.
  • Enforced accessibility: the state and the styles that communicate that state become tightly coupled, preventing developers from overlooking and forgetting about a11y.

Yes, this will probably cause a few headaches, but I’m willing to take that trade-off for the sake of keeping the UI accessible.

Here’s a small list of ARIA States to get you started

Taken from Steve Faulkner’s post “HTML5 and the myth of WAI-ARIA redundance”:

  • aria-busy: Indicates whether an element, and its subtree, are currently being updated.
  • aria-checked: Indicates the current “checked” state of checkboxes, radio buttons, and other widgets. Also see aria-pressed and aria-selected.
  • aria-disabled: Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable. Also see aria-hidden and aria-readonly.
  • aria-expanded: Indicates whether an expandable/collapsible group of elements is currently expanded or collapsed.
  • aria-grabbed: Indicates an element’s “grabbed” state in a drag-and-drop operation.
  • aria-hidden: Indicates that the element is not visible or perceivable to any user. Also see aria-disabled.
  • aria-invalid: Indicates the entered value does not conform to the format expected by the application.
  • aria-pressed: Indicates the current “pressed” state of toggle buttons. Also see aria-checked and aria-selected.
  • aria-selected: Indicates the current “selected” state of various widgets. Also see aria-checked and aria-pressed.

Final thoughts: Best practices vs “best practices”

It’s never the fault of the methodology, but of the developers. Just because you could do something in a certain way (e.g: Making a <div> look and work like a button), doesn’t mean you should.

However, I think there is a distinction to be made between best practices that make for better collaboration and more maintainable code, versus “best practices” that are there just to allow developers to get away with doing a half-assed job.

The web calls for a more sensible approach.

I believe this to be one of those cases where the reward of a more accessible UI is much bigger than the trade-offs. Even if it means forcing developers to learn about accessibility in order to collaborate. Because they should do that at some point anyway.

Join my newsletter and get articles like this delivered straight into your inbox.