Web Accessibility

The goal of this article is understand how to test the accessibility of a web page and then to provide many patterns to enable accessibilty on most the web components.

How to test the accessibility of a web page ?

Enable a Screen Reader extansion (ChromeVox Classic Extension for chrome will do the job) and while using only the key board you can start to browse your web site. Once you can do whatever you want and once you will confident about the clarity of the information read by the screen reader, you can say you are done.

To recap: 1/ Everything can be done with the keyboard and 2/Info read by the screen reader ensure your site is usable.

Buttons

Button as button element + text

The regular button element with text in it is accessible because it's reachable from a keyboard, and a screen reader, and if it has text in it, it is fine.

Button as button element + text not clear enough

The regular button element with text in it is accessible because it's reachable from a keyboard, and a screen reader. But the info read by the screen reader is not clear enough to explain the purpose of the button (a '+' as the text for the button for example). In that case you can make use of aria-label attribute:

<button aria-label="Add book">+</button>

Button as button element + CSS Icon

Since it is a button element, it is reachable by the keyboard but nothing is read by the screen reader. First you need to add text in the button element while making sure it rendered only for screen reader. And second you need to tell the screen reader to ignore the icon.

To make sure the text in only visible by screen reader you can make use of the following css class:

.visuallyhidden {
	border: 0;
	clip: rect(0 0 0 0);
	height: 1px;
	margin: -1px;
	overflow: hidden;
	padding: 0;
	position: absolute;
	width: 1px;
}

To tell the screen reader that it must ignore the icon you can set aria-hidden="true" on i element which defines the icon.

<button>
	<span class="visuallyhidden">Help!</span>
	<i class="icon icon-help" aria-hidden="true"></i>
</button>

Button as a div element + inline svg

By default, it is not reachable by the keyboard and the screen reader can't tell us anything meaningfull about the button.

First you need to specify the div play the role of a button and second you must set the attribute tabindex on the dev element. That means that it will be reachable from a keyboard and a screen reader, but it will be kept in its original tab order. Finally to make sure the screen reader can read meaningfull information you can make use of the attribute aria-labelledby on the svg element:

<div class="button" role="button" tabindex="0">
   <svg aria-labelledby="svgtitle">
      <title id="svgtitle">A meaningfull message about the button</title>
   </svg>
</div>

Button events

You also need to make sure you can trigger the action of the button with the keyboard.

You need to use use key event listener in your js code and code the appropriate logic.

Forms

To make sure element are announced wrap input elements in a label element.

<label>
   Your name
   <input type="text">
/label>

If this can't be done you make use the label / id combinaison:

<div class="label-wrap">
	<label for="hometown">
	Hometown
	</label>
</div>
<div class="input-wrap">
	<input type="text" id="hometown">			</div>

To label a group of radio button:

<fieldset>
	<legend>The label of the group</legend>
	<div>
		<label>
			<input type="radio" name="animals">
			Dog
		</label>
	</div>
	<div>
             ....
       </div>
</fieldset>

How to group label element

First why would you want to group label element ? There are at least two obvious use cases. First because while testing your page with the screen reader you realize that some labels when read separately loose in clarity. You can do that with the aria-labelledby attribute.

<div aria-labelledby="id1 id2">
  <span id="id1">
    This text will be read
    <Link id="id2" to="/">
      And this one too.
    </Link>
  </span>
</div>

Second in form input validation when you want to ensure validation error are read by screen at the right place and at the right time. This time you can use the aria-describedby attribute:

<input
  id={id}
  aria-describedby="id1 id2"
/>

With id1 matching a container id whose purpose could be to display additionnal information about the input element and id2 matching a container id whose purpose could be to display validation error. At least now, the screen reader will read all information while the focus in on the input element.

When dynamic changes occur to content on a page they are usually visually apparent to users who can see the page but may not be made apparent to users of assistive technologies. A good example for this is form submission. ARIA live regions provide a way to make those dynamic content changes able to be announced by assistive technologies. And simple example of that can be:

<div
  id='errorId'
  aria-live="asertive"
>
Conditionnal error message
</div>

Landmark region accessibility

Since all page content must be contained by landmarks if you have no landmark, it is an issue. You can define landmark either by specifying the role attribute on html element or you can do it by using semantic html5 element.

The following are ARIA roles for landmark regions:

  • banner

  • complementary

  • contentinfo

  • form

  • main

  • navigation

  • search

  • application

The following are HTML5 elements for landmark regions:

  • article

  • aside

  • header

  • footer

  • main

  • nav

  • section

Most of the web pages have a header, some navigation, some content and a footer. To specify these landmarks region with aria-roles, the header will have a role of banner, the button whose purpose is navigation will have a role of navigation, the content div will have a role of main and finally the container which contains the footer will have a role of contentinfo

We could do the same with the html5 elements header, nav, main and footer.

Heading level accessibility

Obviously any page should contains on h1 heading. Pages should be structured in a hierarchical manner (h1, then h2, then h3, etc...) in other words do not skip heading levels - always follow consecutive order.

Do not use heading for formatting, it would create unnecessary noise for screen reader users who are browsing the headings of the page.

Alternative text for images

All <img> elements should have an alt attribute provided. When not provided many screen readers will fallback to reading the file name instead. The alt value should not include words like “image”, “picture” or “icon”. Screen readers are already announcing that the element is an image. Images that are decorative only and provide no information or function to the page should be hidden from Assistive Technologies by providing an empty alt attribute (alt=””).

Focus management

Each time you add/remove information a page (add item, remove item, open a modal, close a modal, etc...) you must ensure your focus is at the right place after the action occured.

Each time you navigate to a new page you must check what element got the focus and ask youself if this makes sense. This is really important if you don't want screen reader user to get lost.

Tips: to know what element of a web page has currently the focus, you can execute the following in the console:

document.activeElement

How to use the ARIA specification

You can be interested by the widgets roles, examples are button, checkbox, gridcell, link

And also be specifications about states and properties, examples are aria-current (state), aria-describedby, aria-details, aria-disabled (state)

All these attributes will only affect the accessibilty tree. The following custom element will be identical to a disabled button from the accessibilty perspective, but the rendering won't be identical.

 <custom-button role="button" tabindex="0" aria-label="Close" aria-disabled="true"></custom-button>

Hidding content

display:none will hide from everybody: browser and screen reader

hidden html attribute will hide from everybody too: browser and screen reader

opacity:0 will be rendered in the DOM and will invisible. Nevertheless the element can still be accessible by the keyboard. Adding a tabindex="-1" will enseure it is also invisible from the keyboard. The content of node, even if it is not visible on the screen is still reachable by screen reader.

visibility:hidden will be rendered in the DOM and will invisible. It won't be keyboard accessible. And it will won't be screen reader accessible. Same as display:none except space is reserved.

visuallyhidden custom css class, to hide from the screen of all devices but screen reader. Be carefull any focusable element are still reachable by keyboard.

aria-hidden=true, the accessibilty tree is impacted but not the DOM tree. Be carefull any focusable element are still reachable by keyboard but without any information for screen reader to read - so don't forget tabindex="-1"

To summurize, whatever techniques you use if you decide to hide some content you must check the keyboard accessibility and the screen reader accessibility.

When keyboard-only users interact with your site they use the tab key to jump from link to link. If you have a lot of links at the first of your page in your header or in a menu, they must tab through those every time they come to a new page just to get to the main content. Providing a skip to main content link allows them to easily bypass this.

<!DOCTYPE html>
<html lang="en">
<head>
	<title>Skip Links Demo</title>
	<link rel="stylesheet" type="text/css" href="css/demo.css">
</head>
<body>
	<ul class="skip-links">
		<li><a href="#global-nav">Skip to navigation</a></li>
		<li><a href="#main">Skip to content</a></li>
		<li><a href="#footer">Skip to footer</a></li>
	</ul>
	<header role="banner" id="global-nav" tabindex="-1">

	</header>
	<main role="main" id="main" tabindex="-1">
		<h1>Homepage</h1>
		<h2><a href="#">Article Title</a></h2>
	</main>
	<footer role="contentinfo" id="footer" tabindex="-1">

	</footer>
</body>
</html>

.skip-links {
	list-style: none;
	margin: 0;
	padding: 0;
	position: relative;
}
.skip-links li a {
	background-color: #fff;
	display: block;
	left: -600000px;
	padding: 0.5em;
	position: absolute;
}
.skip-links li a:focus {
	left: 0;
}
[tabindex="-1"]:focus {
	outline: none;
}

Accessible modal dialog

Chrome is the only browser to support the dialog element. Safari, like other non-Chrome browsers, does not provide the styles and behavior necessary for this element. To fix this you can use a polyfill. When using modal you can also use wicg-inert to ensure some part of the code remain hidden from assistive technologies

npm install --save dialog-polyfill wicg-inert

<!DOCTYPE html>
<html lang="en">
<head>
	<title>Accessible Modal Dialogs</title>
	<link rel="stylesheet" type="text/css" href="style.css">
	<link rel="stylesheet" type="text/css" href="node_modules/dialog-polyfill/dist/dialog-polyfill.js">
</head>
<body>
	<div class="wrapper">
		<main role="main" id="main" tabindex="-1">
			<h1>Homepage</h1>
			<h2><a href="#">Article Title</a></h2>
			<button id="dialogTrigger">Open dialog</button>
		</main>		
	</div>

	<dialog aria-labelledby="dialogHeading">
		<h2 id="dialogHeading">Dialog Title</h2>
		<button id="closeBtn">Close</button>
	<dialog>

	<script src="node_modules/dialog-polyfill/dist/dialog-polyfill.js"></script>
	<script src="node_modules/wicg-inert/dist/inert.js"></script>
	<script src="script.js"></script>
</body>
</html>

The logic to control the dialog is written in script.js:

document.addEventListener("DOMContentLoaded", pageLoaded);

function pageLoaded(event) {

    var dialogBtn = document.getElementById('dialogTrigger'),
        closeBtn = document.getElementById('closeBtn'),
        dialog = document.querySelector('dialog');
        wrapper = document.querySelector('.wrapper');

    dialogPolyfill.registerDialog(dialog);

    dialogBtn.addEventListener('click', dialogBtnHandler);
    closeBtn.addEventListener('click', dialogCloseBtnHandler);

    function dialogBtnHandler(event) {
      dialog.showModal();

      document.addEventListener('keydown', keydownHandler);
      wrapper.setAttribute('inert', '');
    }

    function dialogCloseBtnHandler(event) {
      closeDialog();
    }

    function closeDialog() {
      dialog.close();

      setTimeout(function() {
        dialogBtn.focus();
      });

      wrapper.removeAttribute('inert');
    }

    function keydownHandler(event) {
      if (event.keyCode === 27) {
        closeDialog()
      }
    }
}

When the user close the dialog the focus is sent back to open dialog button.

The attribute

aria-labelledby 
is used on the dialog element so that screen reader can read what the dialog is for.

There is an escape key listener so that when the user press escape an open dialog will be closed.

One way to create a blocking context is with the HTML attribute inert. The idea of inert is that you can apply it to sibling elements of something that you want to be modal, and it will prevent interaction in that part of the DOM tree. inert attribute will hide this content from assistive technologies.