A box with accessibility icon filled with UI widgets and documents

Cover illustration source – “And So Much More…” ARIA Authoring Practices Guide | W3C

Preface

Among the navigation items on the W3C homepage is a Mission page. On that page, accessibility is listed as the first principle. Thanks to this background, web standards specifications always consider accessibility together.

The power of the Web is in its universality. Access by everyone regardless of disability is an essential aspect.
– Tim Berners-Lee, Accessibility | Our mission | W3C

If the web content you want to create sufficiently reflects the structural meaning of HTML, most accessibility is guaranteed. However, when implementing complex UI components that HTML does not provide by default1, there is a limitation where accessibility can be omitted.

This limitation can be supplemented with WAI-ARIA, but improperly used ARIA can actually degrade accessibility, so caution is needed.2 This is because it may behave differently than originally intended.

Notes on ARIA Use in HTML | W3C provides a good introduction to when and how to use ARIA and situations where it should not be used. I recommend reading it.

For a more friendly guide, there is Things to Keep in Mind When Learning WAI-ARIA (Japanese). Although written in Japanese, you can read it sufficiently through browser translation features.

So What Will This Cover?

This article covers how to properly utilize WAI-ARIA when complex UI state representation is needed. I’ve prepared two best practices to help with explicit UI state management using ARIA State (representative examples include aria-invalid, aria-expanded, aria-selected, etc.).

Writing Meaningful CSS Rules

Moving pixels around in CSS has been pretty hard! But now using non-semantic class names, inline CSS in HTML, and CSS styles written in JavaScript is the truly awesome and rad way! Put the Family Guy CSS GIF here!

Modern CSS Explained For Dinosaurs | by Peter Jang | Actualize
Illustration from - Dinosaur Comics

If you want to understand through video first, watching “How to write Semantic CSS - YouTube” is also good.

Class selectors have the characteristic of being more versatile because they rank higher in CSS Specificity3 than element selectors but lower than ID selectors. Because of this characteristic, defining CSS rules by class names has become a common practice among web programmers.

However, the W3C CSS specification recommends avoiding the practice of replacing HTML’s meaning with class names, as class names do not carry meaning unlike HTML. In my personal opinion, beyond HTML, CSS pseudo-classes also provide sufficiently referenceable built-in states such as :checked, :default, :disabled, :invalid, :required.

CSS gives considerable power to the “class” attribute, authors could conceivably design their own “document language” based on elements with almost no associated presentation (such as DIV and SPAN in HTML) and assigning style information through the “class” attribute. Authors should avoid this practice since the structural elements of a document language often have recognized and accepted meanings and author-defined classes may not.“Class Selectors” Selectors Level 3 | W3C 4

This issue can be further deepened from an ARIA State perspective. This is because UI states can be semantically managed using attributes like aria-invalid(whether input is valid), aria-expanded(open/closed state), aria-selected(whether selected), mentioned at the end of the preface.

Case 1. Invalid Price Input State

In 2023, I worked on frontend development for a content submission web service called “Glyph” at a startup called “Penxle Company”, starting 3 months before its launch. I always worked with care to ensure that the features provided properly supported web accessibility.

I also wanted to persuade surrounding colleagues to care about UI design faithful to HTML’s basic functions and accessibility standards, so I consistently shared review opinions on accessibility standards such as WAI-ARIA across the company. As a result, my opinions were significantly adopted in the web service development guide.5

For the first best practice, I’ve prepared the “content pricing feature” from the UI created in “Glyph”.

UI Requirements for the “Content Pricing Feature”

  1. Prices can only be entered in multiples of 100, and can be set up to a maximum of 1,000,000.

  2. Any other input is invalid.

  3. Validation is delayed until after form submission. For invalid states, the following feedback is provided:

    • The input UI’s border and background color change to red with transparency.
    • A Tooltip UI appears to provide a detailed message about why the input is invalid.

Indicating Invalid State

For the validation feedback in the above requirements, you can define meaningful CSS rules by declaratively using ARIA State without injecting arbitrary class names.

I’ll show you a simplified version of the UI I implemented at the time. (The original can be found at penxle/withglyph#1296.)

Note: The @ui/Tooltip module is a hypothetical Tooltip UI component assumed to help understand the pseudocode.

<script lang="ts">
  import Tooltip from '@ui/Tooltip';

  interface Props {
    errorMessages: string[];
  }

  let { errorMessages }: Props = $props();

  let invalidPrice = $derived(errorMessages.length > 0);

  const errorTooltipId = 'price-input-error';
</script>

<Tooltip enabled={invalidPrice}>
  <!--
    aria-live="assertive":
    Assistive Technology tools immediately read content whenever it changes inside the element.
    -->
  <span slot="message" id={errorTooltipId} aria-live="assertive">
    {errorMessages.join('<br/>')}
  </span>
  <input
    name="price"
    aria-errormessage={errorTooltipId}
    aria-invalid={invalidPrice}
  />
</Tooltip>

<style>
input[name="price"][aria-invalid="true"] {
  border: 1px solid red;
  background-color: rgb(255, 0, 0, 0.5);
}
</style>

When an invalid value is entered into the price Input UI, the aria-invalid attribute is injected as true. Looking at the code inside the <style /> block, when the aria-invalid attribute value of the price Input UI is true, a red colored border and rgb(255, 0, 0, 0.5) background color are styled.

Relationship Between Input UI and Tooltip

The second sub-item is “feedback that displays error messages with Tooltip UI”. When delivering detailed error messages using a Tooltip, you can specify the relationship between the error message and the input field by assigning the aria-errormessage value as the constant value errorTooltipId, which is the ID of the error message element.

This relationship specification not only improves accessibility for Assistive Technology users but also becomes a useful clue when constructing automated UI component tests. In fact, in testing-library, a representative component testing library, getByRole query priority based on the accessibility tree is specified as the highest.

As a related domestic best practice, there is the article “Accessibility-Driven Web Frontend Test Automation : Wonderwall Tech (Korean)”, so if you’re interested in this usage method, I recommend reading it thoroughly.

Case 2. WYSIWYG Editor Toolbar Items

As another case study, I’ll introduce an implementation case of font format Select UI requirements in the toolbar UI of “Glyph’s” WYSIWYG editor.

Select UI Requirements

  1. The Select UI’s button label displays the current cursor’s font information.
  2. When the Select UI button is pressed, the listbox opens. The button’s background color changes to gray, and the 🔼 icon next to the button label rotates 180 degrees.
  3. Display a check ✅ icon to the right of the font item in the list that matches the font selected by the cursor in the editor.

Note: The @ui/Toolbar module is a hypothetical toolbar UI component assumed to help understand the pseudocode.

Second Example Markup
<script lang="ts">
  import Toolbar from '@ui/Toolbar';

  interface Props {
    fonts: { label: string; value: string }[]
    selectedFontName: string;
  }

  let { fonts, selectedFontValue }: Props = $props();
  let isFontListOpen = false;

 const onSelectFontValue = (event: MouseEvent & { target: HTMLLIElement }) =>
  (selectedFontValue = event.target.dataset.value);

  const listboxId = 'font-select-listbox'
  const makeOptionIdByFontValue = (value: string) => `font-option-${value}`
</script>

<Toolbar>
  <button
    aria-expanded={isFontListOpen}
    aria-controls={listboxId}
    aria-activedescendant={makeOptionIdByFontValue(selectedFontValue)}
    onclick={() => isFontListOpen = !isFontListOpen}
  >
    {selectedFontValue} <i class="chevron-icon" />
  </button>
<ul role="listbox" id={listboxId}>
  {#each fonts as font}
    <li role="option"
      tabindex={font.value === selectedFontValue ? 0 : -1}
      aria-selected={font.value === selectedFontValue}
      id={makeOptionIdByFontValue(font.value)}
      data-value={font.value}
      onclick={onSelectFontValue}
    >
      {font.name}
      <i class="selected-icon" />
    </li>
  {/each}
</ul>
</Toolbar>

Expressing Open and Closed with ARIA State aria-expanded

aria-expanded is a boolean state that indicates whether the UI controlled by a specific button is currently expanded or collapsed. For Select UI, if the list is collapsed the attribute value is false, and if expanded it is true.

Expressing This State on the Select UI Button
button[aria-expanded='true'] {
  background-color: lightgray;

  & > i.chevron-icon {
    /* Rotate the 🔼 icon next to the button label upside down */
    transform: rotate(0.5turn);
  }
}

button > i.chevron-icon {
  transition: transform 0.5s;
}

The above CSS rule styles the font selection button’s background color as lightgray when in the open state. And while rotating the 🔼 icon next to the button label 180 degrees (rotate(0.5turn)), it specifies a 0.5 second transition effect for that element’s transform property.

This results in a transition animation that rotates clockwise to change to a 🔽 icon playing for 0.5 seconds.

Showing the Listbox
ul[role='listbox'] {
  display: none;
}

button[aria-expanded='true'] + ul[role='listbox'] {
  /* top, left specification code omitted: implemented with popover library at JS execution time */
  display: block;
  position: absolute;
}

The above CSS rule controls whether the listbox is opened by using the button’s aria-expanded state as a selector. When the button’s aria-expanded attribute value is true, the listbox wrapped in the ul element located immediately after via the next-sibling combinator + is applied to appear on screen.

Through this selector combination, control over whether the listbox is opened can be statically separated without performing it directly at JavaScript execution time.

Expressing Selected Items with aria-selected

li[role='option'] > i.selected-icon {
  visually: hidden;
}

li[role='option'][aria-selected='true'] > i.selected-icon {
  /* Display ✅ icon next to font name when selected */
  visually: visible;
}

aria-selected is used to indicate whether an item is selected. Looking at the code block above, CSS rules are defined to visually display or hide the ✅ check icon based on this attribute for all option elements in the list.

Therefore, feedback about the selected option can be conveyed visually along with the ARIA attribute, providing equal information regardless of whether the user has a disability.

Aside: I consider it a bad code smell when default state CSS rules are overridden by other rules according to specificity, so I prefer defining styles separately according to branches.

- ul[role='listbox'] {
+ button[aria-expanded='false'] + ul[role='listbox'] {
    display: none;
}

button[aria-expanded='true'] + ul[role='listbox'] {
  /* top, left specification code omitted: implemented with popover library at JS execution time */
  display: block;
  position: absolute;
}

- li[role="option"] > i.selected-icon {
+ li[role="option"][selected="false"] > i.selected-icon {
    visually: hidden;
}

li[role="option"][selected="true"] > i.selected-icon {
  /* Display ✅ icon next to font name when selected */
  visually: visible;
}

Introduction to Other Used ARIA

Both of the attributes below, like the Relationship Between Input UI and Tooltip from the first case, can be used as useful clues for constructing automated web browser tests with the same benefits.

  • aria-controls is an attribute that specifies that another HTML element is being directly controlled. In the example button, aria-controls references the ID of the listbox that appears when the button is clicked.
    • This helps Assistive Technology users, such as those using screen readers, clearly understand the relationship between the button and the controlled UI.
  • aria-activedescendant is used in complex UIs to communicate to Assistive Technology which child element is virtually activated without actually transferring focus.
    • Feedback during list navigation: When navigating the list through Assistive Technology tools, information about the currently active item is always available.
    • Roving Tabindex: According to accessibility standard guides, movement between options in a listbox is limited to arrow keys. And pressing the Tab key should exit the listbox, which is the correct interaction. Therefore, if you carefully read the second example markup code block, only the activated option has a tabindex of 0, and all other options are set to -1.
      • If you’re curious about best practices for keyboard operation, please refer to the conversation in penxle/readable#1128 that I shared during the process of consulting an external Open Source project.

Feature Summary

You can see that this writing method allows for clear separation between state and CSS rules in frontend UI. Summarizing the characteristics into four points:

  1. Semantic CSS selectors: By combining CSS selectors that include meaning according to ARIA standards, the frequency of worrying about how to construct CSS rules is reduced.
  2. Clear separation of state and style: State specification based on HTML attributes allows CSS rules to be statically separated from JavaScript execution time.
  3. Providing extensibility to HTML: Roles and meanings for complex UI patterns not supported by HTML by default can be expressed with ARIA.
  4. Useful for test automation: Since relationships between UIs (ARIA Relationship) and states (ARIA State) are explicitly expressed through ARIA, they can be used as useful clues for component test construction.

Quick Application

The ARIA Authoring Practices Guide created by W3C WAI provides good examples of many commonly used UI patterns with accessibility in mind.

The ARIA Authoring Practices Guide created by W3C WAI provides good examples of many commonly used UI patterns with accessibility in mind.

Implementing UI directly with all accessibility in mind can be a cumbersome problem. The external libraries listed below are tools called Headless UI, which have the common characteristic of providing only UI control logic and accessibility features, excluding styles.

  • Ark UI: A tool created for use by the Chakra UI community itself.
    • Internally, it uses Zag, a framework-agnostic FSM-based Headless UI tool, as control logic.
    • Thanks to its framework-agnostic design, it operates multiple distributions including React, Solid, Vue, and Svelte.
  • React Aria: A collection provided by Adobe that prioritizes accessibility standards and provides various React UI implementation patterns as Headless UI.
  • Melt UI: A Headless UI tool operated by the Svelte user community.

Most Headless UI libraries support dependency injection for boilerplate code6 (repetitive code) of many commonly used UI patterns, so I recommend using them.

Closing

Could you shovel the ramp? Don't you see the kids waiting here to use the stairs? I'll shovel this first and then shovel the ramp for you, so wait. But if you shovel the ramp right away, everyone can enter. Clearing the path for those who need special help is the same as clearing the path for everyone.

“Clearing the path for those who need special help is the same as clearing the path for everyone."
Inspired by public school students with disabilities - Illustrator Kevin Alley

Takashi Hasumi, who pursues universal design, served as jury chairman in a public facility architectural design competition. Although the public facility was a two-story building, there was no elevator in the design. So he asked the jury members whether an elevator should obviously be installed. The jury members who heard this question showed negative reactions, with some opposing because there were no disabled people among building users, and some criticizing that young people just seek convenience. Takashi, wondering if there was a clever way to persuade the jury members, said this: “There is a meeting room on the second floor that can accommodate many people. An elevator can be used when moving luggage to this meeting room.” Only after hearing this did the jury members’ minds gradually move.

Ordinary people think that disabled lifts installed in subways, disabled toilets in public places, and disabled ramps are unrelated to themselves. This perception can also be found in developers and designers who make products (including software). That is, developers with this perception tend to make products that only disabled people can use when making things used by disabled people. For example, lifts that take disabled people in wheelchairs to subway platforms originated from this perception. However, if we shift from the special solution of making products used only by disabled people to the general solution of making products that ordinary people can also use, we would have installed elevators rather than wheelchairs for disabled people.

(omitted) What we need is a general or integrated solution that can be used by all people, including not only disabled people but also children, adults, and the elderly.7 Until now, as a first step to derive a more perfect general solution, we’ve learned about ‘users’ that we developers didn’t know about. From now on, let’s look at what problems exist in the software we’ve made.

Chapter 2 “Developers Don’t Know Users”, 『Software Built by Humble Developers, Yet Arrogant in Design』, 2009, Shin Seung-hwan - Publisher detail page (Korean)

The reason I wrote this article was to convey my mental model8 about web accessibility in writing to inform the outside world that “web accessibility is not simply for screen readers only(special solution) but can be used comprehensively(integrated solution) as a design tool for UX and DX”.

I believe that caring about web accessibility together creates a better experience. After all, the basic composition of a web page is also a semantic markup document, and while structuring the composition of this article myself, I gained the insight that “web accessibility is a subset of user experience”.

Web accessibility technology is in a situation where interest and best practices are relatively uncommon compared to other web frontend technologies. I feel regret about the reality that best practices are still uncommon outside of institutionally mandated airline web services (Korean). I hope that awareness spreads that web accessibility is also an important frontend technology.

Finally, before considering ARIA usage, I remind you again of the “No ARIA is better than Bad ARIA.”2 principle that you must carefully decide after first securing basic accessibility through HTML.

Acknowledgments

  • After completing the first draft of the article, the opinions of accessibility expert Hyunjin were very helpful during the editing process.
    • There is a community called A11YKR that deals with accessibility and inclusive web design.
    • Through this channel, I was able to receive editing opinions from Hyunjin several times. It’s a newly created community, so please participate a lot through the Discord server.
  • msy suggested mentioning testing-library, a tool that prioritizes the accessibility tree.

Useful Reference Materials

Accessibility-Based UI Design

I chose materials centered on those that are easy to practice immediately, as I want to recommend learning through hands-on practice.


  1. Switch UI, implemented by extending the <input type="checkbox" /> element, is a representative UI pattern not provided by HTML. In this case, accessibility hints can be provided through switch among ARIA Roles↩︎

  2. The paragraph with the message “No ARIA is better than Bad ARIA” was written by directly referencing the “No ARIA is better than Bad ARIA.” principle introduced in the top banner of the W3C APG Practices page and the linked document.

    An illustration of a woman with brown skin smiling faintly and pointing her hand to the right

    No ARIA is better than Bad ARIA. Before using any ARIA, read this to understand why

     ↩︎ ↩︎

  3. If you don’t know the CSS term Specificity, please refer to the blog post I previously introduced on my blog: “Understanding CSS Specificity”. ↩︎

  4. This recommendation is content I accidentally first learned from the CSS2 Selectors specification, and it is mentioned identically in the CSS3 specification linked in the main text, word for word. ↩︎

  5. For example, recommending design based on basic Form Web API for text input interactions, using aria-pressed for toggle buttons, aria-busy for loading components, managing and styling disabled states based on disabled and aria-disabled attributes, and recommending the use of CSS pseudo-classes already introduced earlier. ↩︎

  6. Boilerplate code refers to code snippets that can be reused in various situations with little or no modification. See What is boilerplate code? | Amazon Web Services (AWS) ↩︎

  7. Universal Design: For Creating a Warm Society (『유니버셜 디자인: 따뜻한 사회 조성을 위한』), 2005, Sejong Publishing, Hasumi Takashi ↩︎

  8. Mental models refer to personal understanding and beliefs about the system currently being used (web, app, or other products). It’s a term derived from user experience research. See Mental Models and User Experience Design | nngroup ↩︎