Search code examples
htmlcssbuttonwcag

How to properly account for WCAG name, state, value, and role when using an icon as a toggle button


Frequently I'm faced with the task of using material icons and turning them into interactive buttons. For instance, there might be an icon that when clicked reveals text and rotates. When it's clicked again it rotates back to the original position and the text disappears.

<div onClick={toggleButton()}>
  <i
    role="toggle button"
    aria-pressed="true"
    alt="Toggle text"
    class="material-icons"
  >
    toggle_off
  </i>
  Random text...
</div>

<div onClick={toggleButton()}>
  <i
    role="toggle button"
    aria-pressed="true"
    alt="Toggle text"
    class="material-icons"
  >
    toggle_on
  </i>
</div>

-an if conditional would render either of these divs based on pressed or not pressed 

Typically, I handle this button adding a role, aria-state, and alt text to the material icon and change the aria-state when clicked. However, I always feel that I might not be doing this as effectively as I should.

What is the proper way to make something like an icon used as a toggle button accessible?

(I know that best practice for WCAG (web accessibility) is to use a button component, but because of the unique nature of material icons that's not possible.)


Solution

  • You'll need to change a few things:

    1. role="toggle button" should be role="button"
    2. Beginning with aria-pressed="true" is fine if your default button state is pressed. Otherwise, its initial value should be false
    3. Get rid of alt, as it doesn't belong here. If you're including text in your icon (as in your example code), you don't need to replace anything. Otherwise, use aria-label and put your button text there
    4. Add tabindex="0" so that your icon can be reached by keyboard

    Event handling

    To make your icon behave like a real button, you'll also need to listen for keys like the space-bar and enter key. These keystrokes should be treated as clicks, as a real button would.

    const el = document.querySelector('my-icon');
    
    el.addEventListener('keypress', handleKeyPress);
    el.addEventListener('click', handleClick);
    
    function handleClick() { 
      // Update aria-pressed
    }
    
    function handleKeyPress(e) {
      // Handle space-bar and enter key
    }
    

    So, in the end, your toggle icon button might look something like this:

    <i
      role="button"
      aria-pressed="false"
      class="material-icons"
    >
      Button text
    </i>
    

    or (sans visible button text):

    <i
      role="button"
      aria-pressed="false"
      aria-label="Button text"
      class="material-icons"
    ></i>