User Facing State

Avatar of Scott O'Hara
Scott O'Hara on (Updated on )

Let’s talk about state. Communicating state to the user that is, not application stores state in JavaScript objects, or localStorage. We’re going to be talking about how to let our users know about state (think: whether a button is disabled or not, or if a panel is active or not), and how we can use CSS for that. We’re not going to be using inline styles, or, as much as can be helped, class selectors, for reasons that will become clear as we go.

Still here? Cool. Let’s do this.

All dynamic components of an application have a default user-facing state, and that state needs to be stored and updated as users interact with these components.

For example, when a button is pressed, things happen (that’s what buttons are for). When these things happen, they are typically represented in a visual manner in the interface. The button’s background may change to indicate it was pressed. If the button controls other components in the interface, those components likely visually change in style, or in some cases their visibility is toggled. An item gets deleted, a notification pops up, an error style is applied, etc.

You may have noticed that we’ve been mentioning the “visual” state of components quite a bit. That’s exactly the kind of problem I’ve been finding with a lot of tutorials, articles and general talking about state.

More often than not, developers are using “stateful” classes to manage a component’s state. But this is sorely inadequate, as a component is composed of more than just how it looks. There are underlying semantics that need to be managed along with the component’s visual representation. The failure to manage those underlying semantics becomes apparent as soon as you interact with it via keyboard and/or screen reader.

This is an article about appropriately conveying state so that users, beyond sighted, mouse-using users, can interact with our interfaces.

State is more than just how it looks

Outside of using CSS to appropriately hide content from sighted users and assistive technologies, CSS doesn’t have many intentional effects on an element’s semantics or accessible state. What I mean by that is outside of properties like the unsupported ‘speak’, before/after pseudo content, and media queries to specifically restyle components based on user preferences, like the Reduced Motion Media Query and other proposed User Queries, CSS alone is not meant to change an element’s semantics, content or appropriately convey the state of an element in a meaningful way.

Why do I bring all this up? Because managing state with CSS classes alone is, mostly, inadequate for conveying state to all users. Being a language for presentational purposes, giving an input a class of .has-error to change the border color to a shade of red, has no semantic value to it. For all CSS cares, “That’s how you wanted to style that input. Cool. I got your back! Just don’t ask me to style upwards in the DOM. I draw the line there, buddy…”

Instead, to manage and convey state, we should be updating attributes on the appropriate elements. And no, I don’t mean data-attributes. Those don’t mean anything either. If we take this approach, in many cases we won’t even need stateful classes, outside of classes that toggle an element’s display.

Did we forget we can style with attribute selectors?

HTML and ARIA have a whole bunch of attributes that should be used to appropriately convey the current state of a component.

Thinking about using an .is-disabled class on your <button></button> or <input type="text" />? That’s only going to visually disable it. You’re still going to have to programmatically turn off click and keyboard events to that element. Instead use the [disabled] attribute and you’ll have yourself a CSS selector to style your element, and the browser will do all the appropriate work to disable that element for you!

So instead of: { 
  opacity: .65; 

Which only visually modifies an input, use:

input[disabled] { 
  opacity: .65; 

This achieves the same visual effect as using the .is-disabled class, but instead we’re utilizing the attribute selector from the attribute we need to set to convey the current state to the browser and users. All while not having to do any of the aforementioned extra work, with JavaScript, to disable the input, if we were simply toggling a class.

Example: Being “Active”

To provide some deeper context, let’s look at a situation where you might use an .is-active class. For different components, being “active” can mean completely different things, which is why I can appreciate wanting to use a single, reusable class name, instead of determining which attribute needs to be managed to appropriately convey state. But making state management easier for developers doesn’t necessarily help users, so let’s do this the right way.

Active Navigation Links

First let’s look at declaring the currently active link in a navigation. The following Pen has two examples. The first using an .is-active class to indicate the current navigation item. The second uses aria-current="page".

See the Pen .is-active vs aria-current=’page’ by Scott (@scottohara) on CodePen.

While they both look exactly the same, if you use either Jaws 18, Voice Over, or NVDA 2017.2 (when it’s released) when navigating the example, you’ll hear something like: “Features, current page.” when interacting with the example using aria-current. Check out Léonie Watson’s article on [aria-current] for many other examples of where one could use this attribute for styling, in place of an .is-active class.

Active Buttons

Depending on the purpose of the button, the active state of the button may need to be augmented for screen reader users via one of the following ARIA attributes:

  • aria-expanded – indicates that the button controls another component in the interface, and relays that component’s current state.
  • aria-pressed – indicates that the button behaves similarly to a checkbox, in that it has its state toggles between being pressed or unpressed.

Without using one of the above attributes, a button has no inherent way of communicating whether it had been interacted with or not. That is totally fine if a situation doesn’t require it, but if you do need to communicate a button has been activated, then here’s how we can do that using aria-pressed:

See the Pen Toggle Button Example by Scott (@scottohara) on CodePen.

In the above example, we have a button that can be interacted with to add an item to a shopping cart. To indicate when an item has been added, instead of using a class, we’re instead toggling the boolean value of the aria-pressed attribute, and using the [aria-pressed="true"] as our styling hook to visually convey the active state. When interacting with the button via a screen reader, it will be announced as “checked” or “unchecked”, add to cart, toggle button.

For a deep dive into the considerations, one should take when developing accessible toggle buttons, one need look no further than Heydon Pickering’s Toggle Buttons article. Heydon outlines, in great detail, why it’s not a good idea to change the visible label of the button, and even brings to light that you may not actually want a toggle button, but instead should consider using a switch.

Managing Accordion State

For our final example, let’s take a look at how we’d manage state in an accordion component:

See the Pen ARIA Accordion Example by Scott (@scottohara) on CodePen.

If you read through the comments in the CSS and JavaScript, you’ll note that this demo is doing a few things.

First, the markup pattern of the accordion is built in a way so that if JavaScript ever becomes disabled for any reason, none of the content will be inaccessible to those users, as the panels are only hidden if the .js class is present.

Second, to circumvent the need for actual <button></button> elements within each accordion panel heading, we’re instead converting their nested <a>s into “buttons”, by applying the ARIA role="button", and then adding in all the expected keyboard functionality via the keydown event listener. Additionally, to ensure the “button” can be accessed by keyboard users, a tabindex="0" has been set to each of the ARIA buttons.

Finally, here we use the aria-expanded attribute to communicate the current state of the accordion panel, so when a user focuses on the accordion trigger with a screen reader, it will announce “Accordion Heading, collapsed (or expanded) button”.

You will notice that the accordion panels are utilizing an .is-active class to toggle their visible state. Egads! But wait, this is the one thing we can count on CSS alone to help us with. If we take a closer look at the selectors at work here:

.js .accordion__panel {
  border-bottom: 1px solid;
  overflow: hidden;
  padding: 0 1em;
  max-height: 0px;
    max-height .2s ease-in-out,
    visibility .2s ease-in-out;
  visibility: hidden;

.js { 
  max-height: 100vh;
  visibility: visible;

The first selector, the one contingent on JavaScript being available, utilizes visibility: hidden to inclusively hide the panel’s contents from both sighted users and users of assistive technologies. The overflow, max-height, and transition properties are then set to collapse the panel, and prepare it to grow into it’s expanded form, once the .is-active class is added to the accordion panel. I could have toggled display: none or programmatically added and removed the hidden attribute from the panels, instead, but we would have lost out on the ability to transition the panel open. And everyone likes a good transition, right?

In Closing

The main thing I want you to take away from all of this is that if you are only toggling classes to visually manage state of your components, you are likely not appropriately conveying that state to users of assistive technologies.

You need to be using the appropriate elements (<button></button>s are your friend!), and managing the appropriate attributes and their values to make truly accessible user experiences. Sure, you could to do those things and continue to toggle stateful classes to control your styling. But if we have to update attributes and their values, and those are also valid CSS selectors, then why would we do more work than needed by toggling classes too?