Go to main contentGo to footer

Building an accessible product, part 2

In this post, we dive into how to make the most common interactive elements of a web product accessible, starting with navigation controls such as a menu toggler. We'll recommend a few JS libraries that are key to making a page accessible, and then share how we used the prefers-reduced-motion media query.

Marco ZampettiTech Lead, Web Accessibility, UX & Information Architecture

ARIA attributes: what they are and what they're for

ARIA, also called WAI-ARIA, stands for "Web Accessibility Initiative-Accessible Rich Internet Applications". This initiative aims to give developers a set of patterns and attributes to make their creations more accessible.

ARIA attributes on HTML tags are used for various purposes and in specific contexts, which we'll look at more closely later, and can be summed up as follows:

  • interacting with the accessibility tree;
  • providing additional information;
  • communicating the state of interactive elements;
  • removing elements from the accessibility tree.

aria-hidden is useful for hiding content that is repeated several times in the DOM but only needs to appear once in the accessibility tree. For example, a menu with social links that appears in both the header and the footer.

Another use case for this attribute is icons, for example when they're handled with SVG images. To keep the screen reader from trying to read the contents of the SVG code, we can hide it and provide a readable element as an alternative.

<a href="https://www.twitter.com/cantierecreativo" title="Twitter">
  <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 16">...</svg>
  <span class="sr-only">Twitter</span>
</a>

Making a link accessible requires it to have content that a screen reader can read, and that content must be understandable to the user. If a link contains only a graphic element such as an icon, it needs a title or aria-label attribute.

<a href="https://www.cantierecreativo.net/servizi" aria-label="Servizi">
  <svg>...</svg>
</a>

If it's a link that opens in a new page, we recommend making this behavior explicit in the element's text description.

<a
  target="_blank"
  rel="noopener"
  href="https://www.twitter.com/cantierecreativo"
  title="Twitter"
  aria-label="Vai al profilo su Twitter (apre in una nuova finestra)">
   ...
</a>

Even a "simple" external link isn't a trivial matter when it comes to accessibility; this article takes a deeper look at the topic.

Tabs, accordions and menu togglers

Here we have a part of the UI that becomes available after the user activates it with an action. For example, mobile navigation menus revealed by clicking/tapping the classic "hamburger" button, but also accordion menus and tabbed content navigation.

Making these elements accessible via keyboard and screen reader takes a few precautions to ensure the voice interface is clear and functional. So we need to:

  • give an ID to the toggler elements and to the controlled element;
  • use aria attributes to reference the ID of the toggler and of the controlled element;
  • use aria attributes to express whether the controlled element is visible/open or not.

Here's an HTML example of the tag structure:

<button
  id="toggler_id" 
  aria-controls="nav_id" 
  aria-expanded="false" 
  aria-label="Apri/chiudi il menù di navigazione principale"
>
  <i class="icon" data-icon="hamburger" aria-hidden="true"></i>
</button>

<nav
  id="nav_id"
  aria-labelledby="toggler_id"
>
  <ul>
    <li><a href="#">Home</a></li>
    <li><a href="#">Prodotti</a></li>
    <li><a href="#">Contatti</a></li>
  </ul>
</nav>

And the JS code to control this element and its attributes.

const menu = document.querySelector("#nav_id");
const toggler = document.querySelector("#toggler_id");

toggler.addEventListener("click", (e) => {
  e.preventDefault();
  menu.classList.toggle("is-open"); // classe CSS per gestire la visibilità del menù
  if (toggler.getAttribute("aria-expanded") === "false") {
    toggler.setAttribute("aria-expanded", "true");
  } else {
  toggler.setAttribute("aria-expanded", "false");
});

Form basics: input labels and buttons

Forms are a central part of web product accessibility, since they contain interactive elements, the inputs, which must be made usable with specific controls.

Before getting into the details of inputs, let's look at a few guidelines about their context:

  1. Make sure every input has an associated label.
  2. Inputs must have validation and show understandable errors; we recommend specifying the type and using HTML validation where possible.
  3. Forms must have a submit button, and its type attribute must be specified, because different browsers may have different default types.

Here's an example:

<form>
  <label for="field_id">Email</label>
  <input type="email" id="field_id" required="" placeholder="Inserisci qua la tua email" />
  <button type="submit">Invia</button>
</form>

Other inputs, select, select multiple

Almost every input type has its own potential accessibility issues, because each browser and operating system may interpret it differently, so how it translates into an accessible interface can vary. There's a lot of research online, based on user testing, that helps you evaluate the best possible implementation for your case.

So research and in-depth study are the starting points we recommend. For example, this article explains the issues and the solutions adopted for an input type number. But as we'll see shortly, there are often accessible plugins ready to solve the problem and spare you headaches.

A very complex and interesting case is that of select and select multiple, which Sarah Higley covers brilliantly in this series of articles in two parts.

If you need to create a custom input, don't be scared: what matters is always working with accessibility as your reference point. This excellent article shows the example of a numeric quantity input, like the form for adding a product to a cart in an e-commerce context:

<form action="">
  <fieldset>
    <legend>Adjust Quantity</legend>
    <div>
      <label for="qty-element">Current Quantity</label>
      <input type="text" role="alert" aria-live="assertive" value="0" id="qty-element" />
      <button type="button" aria-label='Add to Quantity' aria-controls="qty-element">+</button>
      <button type="button" aria-label='Subtract from Quantity' title="subtract 10" aria-controls="qty-element">=</button>
    </div>
  </fieldset>
</form>

Accessible libraries we're using

Here are some libraries and plugins we're using to create accessible components:

SwiperJS - Slider
https://swiperjs.com/

Tabpanel Widget - Tabs & Accordions
https://tabpanelwidget.com/

For a complete collection of accessible components, see this extremely thorough article by Vitaly Friedman, updated in March 2021.

Reduced motion: an ad hoc solution

The prefers-reduced-motion media query tells our product something very important: the user has set their operating system to reduce motion animations. That's because people with vestibular disorders or labyrinthitis feel dizzy when they see excessive motion.

On web pages, this can happen with:

  • motion transitions on carousels;
  • page scrolling with entrance transitions;
  • smooth scrolling;
  • zooming and magnification.

Since this media query has 92% browser support, we tried using it to make a project we recently published even more accessible.

In this case, it let us give a card component a special transition based on a fade instead of a zoom:

.card {
  &:hover {
    .card__image {
      transform: scale(1.1);
      @media (prefers-reduced-motion) {
        transform: none;
        opacity: 0.8;
      }
    }
  }
}

With this code, we set up smooth scrolling globally but disabled it when this preference is set:

html {
  scroll-behaviour: smooth;
  @media (prefers-reduced-motion) {
    scroll-behaviour: auto;
  }
}

Finally, in our SwiperJS setup we changed the transition of the graphic slides, again replacing motion with a fade:

const isMotionReduced = () => {
  if ('matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches) {
    return true;
  }
  return false;
}
const bigSlider = (container) => {
  if (isMotionReduced()) {
    effectType = 'fade';
  }
  const bigGallery = new Swiper(container, {
  preloadImages: false,
  lazy: true,
  effect: effectType,
  }
}

As we've seen, implementing accessibility isn't trivial and requires in-depth study and research. That's also why we want to share some reference websites and blogs we recommend following to stay up to date on the topic.

Sarah Higley, dev @Microsoft
https://sarahmhigley.com/

Kitty Giraudel, dev @Gorillas
https://kittygiraudel.com/

This wraps up our first series of posts on accessibility. We hope they've helped you understand the needs of these users and find practical solutions to meet them in the products we build.

footer