Use accessible design patterns

When using custom widgets and controls to construct a web page or application, the first requirement is that they are appropriate for the task they are intended to support, such as: data entry, controlling what content is displayed on screen, or navigation.

  • Make sure widgets and controls are appropriate for the task at hand. Accessibility problems can emerge when users find it difficult to understand what it is that they’re supposed to do and how they should operate it.
  • Make sure widgets and controls follow recognized design patterns for accessibility. In almost all cases, this will require the use of WAI-ARIA markup plus JavaScript to manage keyboard interaction, and CSS to provide suitable visual styling to content. Design patterns should mirror the behavior of the component’s equivalent desktop version.
  • Check accessibility support within third-party UI libraries. If you’re using a third party UI library, such as Bootstrap, jQuery, or Angular, read the documentation to understand as much as possible about its out-of-the-box accessibility support and the areas where you’ll have to add accessibility support that’s missing.


For each custom widget and its controls, check:

  • Is the widget appropriate for the task it is intended to support?
  • Does the behavior of the widget and its controls match its relevant design pattern?


✎ Technique: Autocomplete input controls

Autocomplete widgets can be helpful for accessibility because they can make it easier to enter text by providing suggestions based on the characters initially typed. This particularly helps people who find typing more difficult and people who may be susceptible to spelling mistakes.

Creating an accessible integrated autocomplete widget is a complex process. You need to ensure that screen-reader users are notified when the list of suggestions appears and that they can enter the list and select an option using the keyboard.

One solution is to use an ARIA live region to announce when autocomplete options are available, and to create a listbox containing the options that appear as the user types, which is controlled with the up and down arrow keys.


For this example, we’re manually implementing autocomplete for a “choose your pet” input. To begin with, the markup should look like this:

  <label for="pets">Choose a pet</label>
  <div class="suggestions-container">
    <div class="suggestions-help" role="status" aria-live="polite"></div>
    <input id="pets" data-suggest type="text" autocomplete="off" aria-autocomplete="list">
    <div class="suggestions"></div>

Note the .suggestions-help live region and .suggestions, which will need to be populated with the listbox (where applicable) on the input event. Also note autocomplete="off", which suppresses native autocomplete functionality that could get in the way of our custom UI. The ARIA property of aria-autocomplete="list" replaces the semantics so that “autocomplete” is announced on focus.

$('[data-suggest]').on('input', function() {
  // handle suggestions here

In the code editor example you’ll see an array of pet types. As the user types, the input event listens for changes and compares the entered value to each item in the pets array. If there are matches, the first thing the script does is alert the user using the live region:

if (!_.isEmpty(suggestions)) {
      $('.suggestions-help', scope).text(
      'There are '+suggestions.length+' suggestions. Use the up and down arrows to browse.');

This tells the user how many suggestions are available (suggestions.length) and—importantly—how to choose from those suggestions. The newly populated listbox is made focusable using tabindex="0", and it is focused if the user presses the down arrow key. In this state, list’s markup looks like this:

<div role="listbox" tabindex="0" aria-activedescendant="pets-0">
  <div id="pets-0" role="option" tabindex="-1">hamster</div>
  <div id="pets-1" role="option" tabindex="-1">hermit crab</div>
  <div id="pets-2" role="option" tabindex="-1">horse</div>

Note the listbox role and that each child of the listbox has a role of option. This is the only way that a listbox will provide the correct information to screen readers.

Also note that the listbox has a property called aria-activedescendant. This keeps track of the “active” option as the user searches through the options with the up and down keys. It uses the current option’s id as its value, and that value is updated with JavaScript. That way, whenever a new option is made active, screen readers should announce the option’s text, such as “cat,” and the index of the option, such as “two of three.”

To choose the selected list option, the user can hit enter, which clears the list of options from the listbox and refocuses the input field:

if (e.keyCode == 13) {

Code editor

See this custom autocomplete implementation in a code editor. Try operating it with just your keyboard, with a screen reader running at the same time.

✎ Technique: Accessible modal dialogs

Modal dialogs can enhance usability by focusing attention on a specific message that requires a user action to continue.

An accessible modal dialog is one where keyboard focus is managed properly, and the correct information is exposed to screen readers. HTML and WAI-ARIA can be used to provide the necessary semantic information, CSS the appearance and Javascript the behavior.


In this modal dialog example, we’ll look at the HTML, CSS and JavaScript separately.


The dialog itself must be constructed from a combination of HTML and WAI-ARIA attributes, as in this example:

<div id="dialog" role="dialog" aria-labelledby="title" aria-describedby="description">
  <h1 id="title">Title of the dialog</h1>
  <p id="description">Information provided by the dialog.</p>
  <button id="close" aria-label="close">×</button>

Note the dialog role, which tells assistive technologies that the element is a dialog. The aria-labelledby and aria-describedby attributes are “relationship” attributes that connect the dialog to its title and description explicitly. So when focus is moved to the dialog or inside it, the text within those two elements will be read in succession. The close button has an aria-label attribute that overrides the element’s text character of “times” to read “close” when screen readers interact with it.


As well as some CSS for color and positioning, the dialog is set to display:none by default. When the custom attribute open is added to the dialog with JavaScript, the dialog is revealed.

[role="dialog"][data-open] {
  display: block;

Distinctive focus styles are added for the dialog’s the opening and closing buttons so that it’s clear to keyboard users which element is focused. This style is paired with the hover style so that keyboard and mouse operation look consistent.

button:focus, button:hover  {
  outline: 3px solid #d4aa00;


When the “trigger” button is pressed, the script runs the openDialog() function:

function openDialog() {
  dialog.setAttribute('data-open', '');
  close.addEventListener('keydown', function(e) {
    if (e.keyCode == 9) {
  document.getElementById('cover').style.display = 'block';
  document.addEventListener('keydown', addESC);

First, dialog.setAttribute('data-open', '') adds the open attribute to the dialog, which sets the dialog’s CSS display value to block. Next, focus is moved to the dialog’s close button. This does two things: It triggers the announcement of the dialog's title and description in screen readers, and it makes the dialog easy to close at the press of a key.

The next block confines focus to the close button, making sure the user does not accidentally leave the dialog until it has been dismissed. That is, if the TAB key is pressed—which would move focus away from the dialog—the default behavior is suppressed with e.preventDefault();.

Note the line with "addESC" at the end. That adds a listener for the ESC key so that when it is pressed, closeDialog() is run. It is conventional to be able to close a dialog with the ESC key.

var addESC = function(e) {
  if (e.keyCode == 27) {

Clicking or pressing ENTER on the close button will also fire closeDialog():

function closeDialog() {
  document.getElementById('cover').style.display = 'none';
  document.removeEventListener('keydown', addESC);

Note the trigger.focus() line that moves focus back to whichever element opened the dialog. This is important: Keyboard users should always be returned to where they were before they opened the dialog. If you don't do that, when the close button is hidden (and no longer focusable) the <body> element will be focused by default. That will force keyboard users to step through the page manually to find the spot where they left off.

Code editor

Try operating the dialog with only your keyboard in the code editor provided (external link). See how the keyboard operation is affected when you remove the JavaScript lines that manage focus!