Modals are a really common component seen on many websites and apps. However, making an accessible modal is actually a very tricky process. In this post, I'll take a look at some common modal libraries and see how well each one handles these accessibility challenges.

"[modals] are the boss battle at the end of accessibility" - Rob Dodson

What is a modal #

Before we start, let's clear up some terminology. In user interface design, a modal is 'a graphical control element subordinate to an application's main window. It creates a mode that disables the main window but keeps it visible, with the modal window as a child window in front of it.' (see modal on Wikipedia).

On the web, modals are often used to display an alert or confirm a user action (usually called a modal dialog) but they are also used for other purposes such as showing images in a lightbox or displaying a form.

Wait! isn't there already a native modal? #

Yes it's true, HTML does have its own <dialog> element (see dialog on MDN) but as of the time of writing it still has many outstanding issues which make it not suitable for production.

To get a mix of popular libraries to test for accessibility, I did a search for 'modal library' and took a look at the top results. I filtered out any library which didn't have its own demo (for testing purposes) along with any which were clearly broken or severely outdated.

The Tests #

I came up with 10 tests which each library should pass to meet accessibility standards (see Making Modal Windows Better for Everyone and a11y dialog for more information). It's worth stating that passing these tests does not necessarily make the library fully accessible (this would need a great deal of user testing), rather that passing these tests meants the libary has met basic accessibility requirements.

  • Modal hidden when closed. Modal content should be hid from assistive technology when it is closed (for example by using display: none).
  • Accessible modal trigger. The button which opens the modal should be coded as a <button> element and should have an accessible name (either by its contents or aria-label or similar).
  • Focus managed on open. When a modal is triggered, focus should be sent to either the modal container (by using tabindex="-1" and calling .focus() on it) or the first focusable item in the modal.
  • Focus trapped when open. When a modal is open, a user tabbing with the keyboard should only allow be able to access the modal content and not any page content underneath. *Address bar
  • Accessible modal container. The modal container should have role="dialog" and should have an accessible name (either by using aria-label or aria-labelledby).
  • Clear focus styles. When tabbing through the modal with a keyboard, there should be clear focus styles for focusable elements.
  • Accessible close button. There should be a <button> to close the modal and this should have an accessible name.
  • Escape to close. Hitting the <esc> key when the modal is open should close it.
  • Backdrop to close. Clicking/tapping the backdrop area (usually a semi-transparent layer behind the modal) should close the modal.
  • Focus managed on close. Closing the modal should return focus back to the trigger which opened it (in most use cases).

Note: Sometimes it's difficult to be sure which element currently has focus (especially when focus styles are removed), so I used this snippet from Marcy Sutton:

/* Paste in the console to see which element currently has focus */
document.body.addEventListener("focusin", () => {
console.log(document.activeElement);
});

Micromodal.js #

This is a really well put together little library. The documentation is clear and not overwhelming and it passes all the tests.

View Micromodal.js docs

Testing Micromodal.js
Criteria Pass / Fail
Modal hidden when closed Pass
Accessible modal trigger Pass
Focus managed on open Pass
Focus trapped when open Pass
Accessible modal container *Pass
Clear focus styles Pass
Accessible close button Pass
Escape to close Pass
Backdrop to close Pass
Focus managed on close Pass

Bootbox.js #

This could be a good modal library with a few simple fixes.

Firstly the focus outline is barely visible which makes navigating with the keyboard almost impossible.

Secondly, the close button is hidden from assistive technology but is still focusable via the keyboard. It should really be available to everyone and have an accessible name.

Finally, focus should also be returned to the trigger button when the modal is closed. It would also be better if you could click the backdrop to close the modal.

View Bootbox.js docs

Testing Bootbox.js
Criteria Pass / Fail
Modal hidden when closed Pass
Accessible modal trigger Pass
Focus managed on open Pass
Focus trapped when open *Pass
Accessible modal container Pass
Clear focus styles Fail
Accessible close button Fail
Escape to close Pass
Backdrop to close Fail
Focus managed on close Fail

jQuery Modal #

Unfortunately, this modal library has some serious accessibility issues. Most importantly, focus is not sent to the modal when opened, not trapped inside or sent back to the trigger when closed. This makes it impossible to use with the keyboard.

The trigger and close buttons are links rather than buttons. I was hopeful that this was a progressive enhancement which would allow the modal to work without JS, but this isn't the case.

The modal also does not have the correct aria role or an accessible name.

View jQuery Modal docs

Testing jQuery Modal
Criteria Pass / Fail
Modal hidden when closed Pass
Accessible modal trigger Partial
Focus managed on open Fail
Focus trapped when open Fail
Accessible modal container Fail
Clear focus styles Partial
Accessible close button Partial
Escape to close Pass
Backdrop to close Pass
Focus managed on close Fail

Bootstrap modal #

This is a tried and tested modal which passes all the tests and has some great documentation and examples.

There were only two minor issues which I found. First, the focus outlines are a bit subtle which can make it difficult to see where focus was. Secondly, the modal container seems to be in the focus trap of the modal which it shouldn't.

View Bootstrap modal docs

Testing Bootstrap Modal
Criteria Pass / Fail
Modal hidden when closed Pass
Accessible modal trigger Pass
Focus managed on open Pass
Focus trapped when open Pass
Accessible modal container Pass
Clear focus styles Pass
Accessible close button Pass
Escape to close Pass
Backdrop to close Pass
Focus managed on close Pass

Vex #

There are some serious accessibility issues with Vex modal. Firstly the trigger button is a link instead of a button. Since it has no href it's not actually keyboard focusable, so there's no way to trigger the modal unless you are using a mouse.

Once in the modal, focus is not trapped and closing the modal does not return focus to the trigger since it's not a focusable element.

Focus states are also very subtle which it difficult to see where you are when tabbing with the keyboard.

The dialog itself is missing the correct aria role and accessible name.

View Vex

Testing Vex
Criteria Pass / Fail
Modal hidden when closed Pass
Accessible modal trigger Fail
Focus managed on open Pass
Focus trapped when open Fail
Accessible modal container Fail
Clear focus styles Partial
Accessible close button Fail
Escape to close Pass
Backdrop to close Pass
Focus managed on close Fail

Popeye #

Popeye describes itself as 'A simple modal library for AngularJS applications'. Unfortunately this library has some serious issues.

The trigger button is a link with an empty href rather than a button.

Focus is not sent to the modal when it is opened and is not trapped inside. Focus is also not sent back to the trigger when the modal is closed.

The dialog container has no aria role or accessible name.

The close button is a link with an empty href. It also has no accessible name due to how it's usin a pseudo element to show the 'x' character.

View Popeye)

Testing Vex
Criteria Pass / Fail
Modal hidden when closed Pass
Accessible modal trigger Partial
Focus managed on open Fail
Focus trapped when open Fail
Accessible modal container Fail
Clear focus styles Pass
Accessible close button Fail
Escape to close Pass
Backdrop to close Pass
Focus managed on close Fail

LdCover #

This modal library has a number of serious issues.

Both the modal trigger and close buttons are <div>s which mean they are not focusable and do not have an accessible name.

Focus is not sent to the modal when it's opened or trapped inside. Focus is not returned to the trigger when it's closed.

The dialog does not have the correct aria role or an accessible name.

View LdCover

Testing LdCover
Criteria Pass / Fail
Modal hidden when closed Pass
Accessible modal trigger Fail
Focus managed on open Fail
Focus trapped when open Fail
Accessible modal container Fail
Clear focus styles Fail
Accessible close button Fail
Escape to close Pass
Backdrop to close Pass
Focus managed on close Fail

Modaal #

Modaal describes itself as 'An accessible dialog window plugin for all humans.'. On the whole this is a good modal although it does have a couple of minor issues.

Firstly the modal trigger is coded as a link instead of a button. This doesn't seem to be a progressive enhancement as the modal doesn't work without JS enabled.

The close button could have better focus styles as it's not clear when this element has focus.

View Modaal

Testing Modaal
Criteria Pass / Fail
Modal hidden when closed Pass
Accessible modal trigger Partial
Focus managed on open Pass
Focus trapped when open Pass*
Accessible modal container Pass
Clear focus styles Pass
Accessible close button Pass
Escape to close Pass
Backdrop to close Pass
Focus managed on close Pass

Summary #

All the libraries tested seemed to get a few things right. They all hid the modal content correctly when the modal was closed and they all let the user hit the escape key to close the modal.

A lot of libraries failed at focus management. They did not move focus when the modal was open, trap focus inside the modal or pass focus back to the trigger when closed.

Some libraries did not correctly code the trigger as close buttons as button elements and in some cases this meant these controls were not accessible by using the keyboard.

Some libraries did not add the correct role="dialog" to the modal container and did not add an accessible name (either through aria-label or aria-labelledby).

Only two libraries managed to fully pass the tests: MicroModal.js and Bootstrap. Modaal came a close third but has a couple of minor issues which need addressing.