JavaScript
Article
By Artem Tabalin

How to Make Accessible Web Components — a Brief Guide

By Artem Tabalin

This article was peer reviewed by Mallory van Achterberg. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

In a previous article I demonstrated how to create a multiselect web component. In the comments section of that article, readers touched on the very important subject of web component accessibility. Undoubtedly, for today’s web it’s vital to be accessible, so let’s talk about what accessibility stands for and see (by means of a real-world example) how to make a web component more accessible.

The code in this article will build upon the code from my previous article. You can grab a copy of it from our GitHub repo, or check out a demo of the accessible component at the end of the article.

What Does Web Component Accessibility Entail?

When talking about the accessibility of a component we usually consider the following aspects:

  1. Markup semantics
  2. Keyboard support
  3. Visual accessibility

Let’s discuss each aspect in a little more detail.

Markup Semantics

I’m sure you’ve heard about screen readers. A screen reader is a piece of assistive software that allows blind or visually impaired people to use applications by reading aloud information displayed on the screen. There are many screen readers out there, among them NVDA and JAWS for Windows, ChromeVox for Chrome, and VoiceOver for OS X.

When an element receives focus, the screen reader offers information about it to the user. Thus when an HTML <input type="text"> is focused the user knows from the screen reader that they are dealing with text field (and can input something). But if the element is just a bare <div>, the screen reader has nothing to say about it.

To solve this issue we can use WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications) to add special ARIA attributes to extend the semantics of the component markup. These additional semantics help assistive technologies to identify properties, relationships, and states in your user interfaces. The practical guidelines for using ARIA can be found here: WAI-ARIA Authoring Practices, alternatively (for a quick refresher) you could read our Introduction to WAI-ARIA.

Keyboard Support

The goal is to make it possible to interact with a component using only the keyboard. WAI-ARIA defines behavior and the keyboard interactions for many UI controls. To know which keys should be supported by which component, find the description of your component or a similar one in the specification and use that. For instance, the multiselect is similar to the combobox.

Even with keyboard accessibility in place, it’s good practice to let users know which keys/key combos to use to interact with the component (for example by providing some instructions in the application) since this might not be obvious.

Visual Accessibility

Here we are talking about accessibility aspects related to the component’s appearance. Ensure that you can answer ‘yes’ to the following questions:

  • Are the elements and text big enough to clearly see them?
  • Does your component look as expected in high contrast mode?
  • Is it possible to use your component without colors?

Remember, not all visually impaired users are legally blind. There are many users out there who (for example) have low vision or color blindness.

Making the Multiselect Web Component Accessible

Now we’re going to make the multiselect more accessible using all of the techniques outlined above. Specifically, we’re going to:

  • extend the markup semantics
  • add keyboard support
  • validate its visual accessibility

Don’t forget, you can view the demo of the component at the end of the article, or download the code from our GitHub repo.

All of the code snippets can be found in the multiselect.html file.

Extending Markup Semantics

The accessibility rule of thumb is to use native HTML elements over custom ones. This means, if you can use a native HTML control with built-in accessibility, do so. Add ARIA attributes only if you really need to create a custom component. If you’d like to find out more about this, read Avoiding Redundancy with WAI-ARIA in HTML Pages.

In our case the multiselect is a custom component, so we need to add ARIA attributes. First, let’s find a component similar to the multiselect in the ARIA specification. After a little research, it appears that the combobox looks and behaves similarly. Great, now let’s see which ARIA attributes we need to add according to the combobox description.

From guidelines we can see that we need to add the following roles:

  1. role="combobox" to the root element of the component
  2. role="listbox" to the list of items in the popup
  3. role="option" to each item of the dropdown list

The aria state attributes to be added:

  1. aria-expanded="true/false" to the root element to indicate whether the component is opened or closed
  2. aria-selected="true/false" to each item of the dropdown list to indicate selected state

The roles combobox and listbox can be added directly to the markup of the component:

<div class="multiselect" role="combobox">
  <div class="multiselect-field"></div>
  <div class="multiselect-popup">
    <ul class="multiselect-list" role="listbox">
      <content select="li"></content>
    </ul>
  </div>
</div>

To add the role option to each item of the list we loop over items in the refreshItems method. This new method is called when the component is rendered:

multiselectPrototype.render = function() {
  this.attachHandlers();
  this.refreshField();
  this.refreshItems();
};

multiselectPrototype.refreshItems = function() {
  var itemElements = this.itemElements();

  for(var i = 0; i < itemElements.length; i++) {
    var itemElement = itemElements[i];

    // set role and aria-selected property of an item
    itemElement.setAttribute("role", "option");
    itemElement.setAttribute("aria-selected", itemElement.hasAttribute("selected"));
  }
};

multiselectPrototype.itemElements = function() {
  return this.querySelectorAll('li');
};

The aria-expanded attribute can be added to the control in the togglePopup method which (as the name suggests) is responsible for showing and hiding the popup:

multiselectPrototype.togglePopup = function(show) {
  this._isOpened = show;
  this._popup.style.display = show ? 'block' : 'none';

  // set aria-expanded property
  this._control.setAttribute("aria-expanded", show);
};

We also initialize the aria-selected property of items depending on their selected attribute. The aria-selected property should be maintained to reflect the current item’s selected state. We can do that in selectItem and unselectItem methods:

multiselectPrototype.selectItem = function(item) {
  if(!item.hasAttribute('selected')) {
    // set aria-selected property of selected item
    item.setAttribute('aria-selected', true);

    item.setAttribute('selected', 'selected');
    this.fireChangeEvent();
    this.refreshField();
  }

  this.close();
};

multiselectPrototype.unselectItem = function(item) {
  // set aria-selected property of unselected item
  item.setAttribute('aria-selected', false);

  item.removeAttribute('selected');
  this.fireChangeEvent();
  this.refreshField();
};

And that’s it, the ARIA properties have been added. The next step is keyboard support.

Adding Keyboard Support

Let’s open the specification and look at the Keyboard Interactions section to see which interactions we need to support.

Here is the basic set of keys to be able to use the multiselect with the keyboard only:

  1. Alt + Up/Down Arrow – open/close the multiselect
  2. Esc – close the multiselect
  3. Up/Down Arrow – navigate through items
  4. Enter – select an item when multiselect is opened
  5. Backspace – unselect the last selected item

Making It Focusable

The very first step towards adding keyboard support is to make a component focusable. To do that we need to set the tabindex attribute, whose behavior differs depending on the tabindex value:

  • positive integer—defines the order of the element in keyboard focus navigation
  • 0 – the order of the element in keyboard focus navigation is defined by the browser
  • -1 – the element cannot be reached with keyboard focus navigation, but can now receive focus programatically using JavaScript’s focus() method.

In the case of custom components, the tabindex should be either -1 or 0, because we cannot know the order of the element on the target page. Thus we set tabindex on multiselect field to 0 directly in the markup:

<div class="multiselect-field" tabindex="0" aria-readonly="true"></div>

The next step is to handle the keydown event on the multiselect:

multiselectPrototype.attachHandlers = function() {
  this._control.addEventListener('keydown', this.keyDownHandler.bind(this));
  ...
};

The keyDownHandler method calls the particular key handler depending on the event.which property value:

multiselectPrototype.keyDownHandler = function(event) {
  switch(event.which) {
    case 8:  // Backspace
      this.handleBackspaceKey();
      break;
    case 13: // Enter
      this.handleEnterKey();
      break;
    case 27: // Escape
      this.handleEscapeKey();
      break;
    case 38: // Up Arrow
      event.altKey ? this.handleAltArrowUpKey() : this.handleArrowUpKey();
      break;
    case 40: // Down Arrow
      event.altKey ? this.handleAltArrowDownKey() : this.handleArrowDownKey();
      break;
    default:
      return;
  }

  // prevent native browser key handling
  event.preventDefault();
};

Once the key press is handled, we prevent the browser from carrying out its standard action by calling event.preventDefault().

Open/Close with Keyboard

The Alt + Down Arrow key combo should open the multiselect, while Alt + Up Arrow and Esc keys should close it:

multiselectPrototype.handleAltArrowDownKey = function() {
  this.open();
};

multiselectPrototype.handleAltArrowUpKey = function() {
  this.close();
};

multiselectPrototype.handleEscapeKey = function() {
  this.close();
};

The open and close methods just call the togglePopup method:

multiselectPrototype.open = function() {
  this.togglePopup(true);
};

multiselectPrototype.close = function() {
  this.togglePopup(false);
};

Firstly, we set the tabindex of each multiselect item to -1, so they become focusable:

multiselectPrototype.refreshItems = function() {
  var itemElements = this.itemElements();

  for(var i = 0; i < itemElements.length; i++) {
    var itemElement = itemElements[i];
    ...
    // set item tabindex attribute
    itemElement.setAttribute("tabindex", -1);
  }

  // initialize focused item index
  this._focusedItemIndex = 0;
};

The _focusedItemIndex property stores the index of the focused item.

Up Arrow and Down Arrow keys allow the user to navigate over the items in the list maintaining the current focused item index:

multiselectPrototype.handleArrowDownKey = function() {
  this._focusedItemIndex = (this._focusedItemIndex < this.itemElements().length - 1)
      ? this._focusedItemIndex + 1   // go to the next item
      : 0;                           // go to the first item

  this.refreshFocusedItem();
};

If the Down Arrow is pressed at the end of the list, the focus goes to the first item.

If the Up Arrow is pressed on the first item of the list, the focus goes to the last list item:

multiselectPrototype.handleArrowUpKey = function() {
  this._focusedItemIndex = (this._focusedItemIndex > 0)
    ? this._focusedItemIndex - 1        // go to the previous item
    : this.itemElements().length - 1;   // go to the last item

  this.refreshFocusedItem();
};

The refreshFocusedItem method sets focus to the item with the index equal to _focusedItemIndex:

multiselectPrototype.refreshFocusedItem = function() {
  this.itemElements()[this._focusedItemIndex].focus();
};

Finally, we need to change the open and close methods so that, when the multiselect is opened the focus goes to the item with the index _focusedItemIndex, and when the control is closed the focus goes back to the multiselect field:

multiselectPrototype.open = function() {
  this.togglePopup(true);
  this.refreshFocusedItem();
};

multiselectPrototype.close = function() {
  this.togglePopup(false);
  this._field.focus();
};

Now we can add some CSS to make the focused item look visually more appealing:

::content li:focus {
  outline: dotted 1px #333;
  background: #efefef;
}

Select/Deselect Item with Keyboard

The Enter key allows the user to select the current focused item. If the multiselect is opened we get the focused item and select it with selectItem method:

multiselectPrototype.handleEnterKey = function() {
  if(this._isOpened) {
    var focusedItem = this.itemElements()[this._focusedItemIndex];
    this.selectItem(focusedItem);
  }
};

The Esc key should remove the last selected item. If any selected items are present, we take the last one and deselect it by calling the unselectItem method:

multiselectPrototype.handleBackspaceKey = function() {
  var selectedItemElements = this.querySelectorAll("li[selected]");

  if(selectedItemElements.length) {
    this.unselectItem(selectedItemElements[selectedItemElements.length - 1]);
  }
};

Now we have supported all necessary keyboard interactions, so the component can be used with the keyboard only.

Visual Accessibility

Component Size

The multiselect has relative sizes in em and its size depends on the font size of the container. Thus it’s scalable and can be easily increased if necessary:

multiselect different sizes

High Contrast Mode

People with low vision or other visual disabilities sometimes use high contrast mode. OS X allows users to enable high contrast in the settings, whereas Windows provides special High Contrast Themes. There is also a popular Chrome extension called (surprisingly) High Contrast that allows users to browse the web with high contrast color filters.

Let’s see how our component looks in high contrast mode:

multiselect in high contrast mode

It looks ok, but there is an issue: the selected items are hardly distinguishable from the non-selected ones. A little change of color for selected and focused items fixes the issue:

multiselect in high contrast mode - fixed

Without Colors

Color accessibility is another important aspect of visual accessibility. There are many color-blind users exploring the web. This means that color should not be the only way to convey important information to the user. Here we can check how our component looks in grayscale mode (it’s usually provided by OS or special applications). Saying this, our component only consists of gray colors, so this is a check that we can skip.

Demo

Having implemented all of the above, this is the final product:

See the Pen Multiselect Web Component by SitePoint (@SitePoint) on CodePen.

Conclusion

To make a web component more accessible we need to be sure that:

  • The markup is semantic, so that assistive technologies such as screen readers can help users when they interact with the component. To do that try to use native HTML controls or, in the case of custom controls, add meaningful ARIA attributes.
  • The component can be used with the keyboard only. To achieve that make your component focusable with tabindex. Implement keyboard interactions following the ARIA practical guidelines.
  • The component can be used in high contrast mode and without colors.

Thanks for reading. Feel free to share your experience in making web components accessible in comments.

  • In the demo, the legend should say ↓ to open, and ↑ to close (not the other way around), right?

    • James Hibbard

      Oops. Fixed. Thanks :)

  • Why item selected by Enter? In select tag we use space key.

    • As mentioned, we support combobox shortcuts. Enter key is used to select an item, as specified in WIA ARIA practices (https://www.w3.org/TR/wai-aria-practices-1.1/#combobox). It’s done for the sake of simplicity.
      I agree, it’s good to support Space shortcut also, maybe with a bit different behavior (it should toggle selected state of an item). But again only basic keyboard support is implemented to show main principles.

  • JoshuaMuheim

    Thanks for this article. Good information here, especially the links to the ARIA specs. But did you really test your component in screenreaders? NVDA+FF only announces “Blank” in browse mode, and when choosing or deleting an option, no feedback is given. In IE11, NVDA completely rejects working with your codepen (or even reading it). JAWS itself doesn’t announce the component in FF at all, and in IE11 it also doesn’t read anything (like NVDA). I’m using the newest versions of both NVDA, JAWS, IE and FF.

    I’m a frontend developer for 10 years and an accessibility consultant for over two years now, and I’ve become convinced that ARIA, while being a good idea, has absolutely not gained maturity status yet in typical screenreaders and browsers. In fact, using ARIA roles in HTML leads to unpredictable behaviours in some screenreaders (e.g. JAWS jumping into and out from focus mode randomly), making it nearly impossible to get the custom components behaving in an uniform way across screenreader and browser combinations (let alone the different versions of them).

    When working with clients, at the time being, I strongly recommend them to avoid ARIA roles and stick to traditional standard HTML elements. And you would be astonished how many fancy ARIA components in fact can be implemented with basic HTML: for example your multiselect combobox, which can be divided into an edit input for filtering available options, and a list of checkboxes whose visibility is toggled by the filter edit input. Very easy to implement, usable for everybody, even with legacy browsers and screenreaders. Or tabs, which can be implemented using radiobuttons that toggle the visibility of the subsequent content. Or an accordion, implemented with checkboxes that toggle the visiblity of the subsequent content.

    I’ve seen many projects investing a lot of money into creating fancy ARIA roles and in the end still failing to become fully accessible. That’s a shame. But it doesn’t require a lot to create accessible traditional websites. So as long as screenreaders and browsers fail to interpret ARIA roles reliably, you better leave your hands off them and try to implement your requirements using standard HTML.

    • Hello Joshua, thank you for the feedback.

      Of course, I tested the component in the mentioned in the article screenreaders and modern browsers. Indeed, there are some issues with announcing the component once it’s focused.
      But I had no mentioned issues in IE11 neither with NVDA nor JAWS. Moreover, in IE11 it worked even better than in FF. Have you tried to open the result of Codepen in a separate tab?

      I completely agree that we should always try to go for a native HTML elements with built-in accessibility. It’s mentioned in the article as “the accessibility rule of thumb”.

      • JoshuaMuheim

        Yes, I tried it in the debug view of CodePen. I will take a look at it again later.

      • JoshuaMuheim

        I tested again. In NVDA+FF, here’s the speech viewer’s output when browsing the element (using the down arrow):

        heading level 1
        Multiselect Web Component
        Multiselect Web Component in VanillaJS
        heading level 3
        Demo
        blank
        heading level 3
        Keys

        So for the component, simply “blank” is announced. In focus mode, the same is the case. In IE, there’s no announcement by NVDA at all for any element on the page.

        I have opened the component in debug view. I’m propagating this on to my blind co-workers, let’s see what they think about it.

Recommended
Sponsors
Get the latest in JavaScript, once a week, for free.