🛠️ Modal: Screen reader accessibility

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Modal: Screen reader accessibility

Like before, the first thing we want to do is audit our component with a screen reader.

The modal button

When a screen reader lands on the modal button, it says “Click me”.

“Click me!” is the accessible name for the button. But “Click me” is not helpful for screen readers because they don’t know what te expect after clicking the button.

We want to give screen reader users some context. In this case, since the button opens a login modal, let’s say “Open login form”.

We can do this by changing the text content.

<button class="button jsModalButton">Open login form</button>

This is much better!

But let’s say we want to continue showing sighted users “Click me!”. And we want to screen reader to read “Open login form”.

We can use aria-label to do this.

<button class="button jsModalButton" aria-label="Open login form">
  Click me!
</button>

Opening the modal

Sighted users can see a modal open when they click the “Click me” button. But blind users won’t be able to see this.

We want to let them know something will expand. To do this, we’ll add aria-expanded to the click me button.

<button
  class="button jsModalButton"
  aria-label="Open login form"
  aria-expanded="false"
>
  Click me!
</button>

When we open the modal, we need to change aria-expanded to true.

const openModal = _ => {
  // ...
  modalButton.setAttribute('aria-expanded', true)
}

When we close the modal, we need to change aria-expanded back to false

const closeModal = _ => {
  // ...
  modalButton.setAttribute('aria-expanded', false)
}

Naming the modal

When the user opens the modal, we bring their focus to the first input field. If you use Voiceover, you should hear “Email, edit text, [email protected]”.

  • “Email” comes from the label.
  • “Edit text” because the user focused on an <input> element.
  • “[email protected]” is the placeholder.

This is good.

But why did the user get redirected into an input field?

Sighted users get it because they can see the modal. But screen readers can’t see the modal. They’ll get confused.

We can tell screen reader users they’re inside a modal by setting the role of the modal to dialogue. (Remember, a modal is a dialogue).

<div class="modal" role="dialog">...</div>

Each dialogue role should come with an accessible name. In this case, we’ll use “Login form” as the accessible name.

We’ll can set the accessible name with aria-label since the words “Login form” is not found on the page.

<div class="modal" role="dialog" aria-label="Login form">...</div>

Voiceover users will now hear “Login form” (from the aria-label) and “web dialogue” (from the dialogue role).

NVDA says “dialogue” instead of “web dialogue”. The rest of the experience is similar.

Note: Notice Voiceover says “and 5 more items” while NVDA doesn’t. Voiceover has a tendency to announce the number of items inside a role.

Tabbing around the modal

Tab into the password field. On Voiceover, you should hear this:

  • “8 characters” and “bullet bullet bullet” comes from the placeholder
  • “Secure edit text” tells users they’re on a password field
  • “Password and one more item” comes from the label

The “Password and one more item” part is confusing. What’s the “one more item”?

Turns out, the “one more item” is caused by setting <span> to display: flex. (Voiceover has issues like these).

<label>
  <span>
    <!-- Setting display: flex caused the bug -->
    <svg>...</svg>
    Password
  </span>
  <input type="password" name="password" placeholder="••••••••" />
</label>

The SVG is the “one more item” Voiceover mentioned. Unfortunately, setting aria-hidden="true" on the SVG doesn’t remove “and one more item” from Voiceover.

I had to change the HTML and CSS to fix the password field.

The CSS is given to you in the starter file for this lesson. Here’s the HTML:

<div class="input-group">
  <label for="password">
    <svg width="1em" height="1em" viewBox="0 0 20 20">...</svg>
    <span>Password</span>
  </label>
  <input type="password" id="password" name="password" placeholder="••••••••" />
</div>

What I did was to use a label the <input> with a for attribute instead of wrapping the <input> with a <label>.

Note: Please change the HTML for the email field too.

With this change, Voiceover reads “8 characters, Password, secure edit text, bullet bullet bullet.” (No more of that "and one more item nonsense).

Note: NVDA doesn’t have a problem with display: flex on <span>. This section here is purely a Voiceover fix.

The close button

Continue tabbing in the modal. When you reach the close button, you’ll notice it doesn’t have an accessible name. Screen readers simply say “button”.

We need to give the button an accessible name. In this case, let’s use “Close login form”.

We’ll set the accessible name with aria-label since the words “Close login form” is not on the page.

<button class="modal__close-button jsModalClose" aria-label="Close login form">
  ...
</button>

Preventing users from moving out of the modal

Users should not be able interact with things outside of a modal when the modal is opened.

We have already prevent normal keyboard users from moving out by trapping focus in the modal. Screen reader users, however, can still move out of the modal with their shortcuts.

For example, if you use Voiceover, you can:

  • use VO + Shift + ↑ exit the modal
  • then VO + ← to go <main>
  • then VO + Shift + ↓ to go to the Open login form" button.

To fix this issue, we can add aria-modal="true" to the modal.

<div class="modal" role="dialog" aria-modal="true" aria-label="Login form">
  ...
</div>

Now Voiceover users can’t move out of the modal.

Hear the tng sound? That’s Voiceover’s way of say “no can do”.

Note: I noticed NVDA users can’t move out of the modal when we added the dialogue role. This seems to be a bug because there are non-modal dialogues out there too.

Fallback for aria-modal

We need to add a fallback for screen readers that don’t support aria-modal.

We can do this by adding aria-hidden to other elements when the modal is opened. In this case, <main> is the only other element.

const main = document.querySelector('main')

const openModal = _ => {
  // ...
  main.setAttribute('aria-hidden', 'true')
}

We also have to remove aria-hidden when we close the modal.

const closeModal = _ => {
  // ...
  main.removeAttribute('aria-hidden')
}

That’s it!