javascripthtmlreactjsaccessibilitywai-aria

"aria-hidden elements do not contain focusable elements" issue when modal is shown


I'm using React Modal in my application and when it is open running the aXe accessibility tool gives the following error:

aria-hidden elements do not contain focusable elements

This is because the React modal adds a aria-hidden="true" to the root element of the application (the div all my apps components are rendered under, but not the modal), but it does not update the tab index or disable every focusable element.

However the React modal traps the keyboard focus, so the user can't tab out of the modal and clicking the background closes the modal.

So my question is:

Is this actually an issue I need to fix? Or is this a false positive as the tool doesn't have knowledge of the modal trapping focus?

If this does need to be fixed, is my only option to manually update the tab index or disable every focusable element?

The HTML when the modal is open looks kinda like this:

<div data-react-modal-body-trap="" tabindex="0" style="position: absolute; opacity: 0;"></div>
<div id="root" aria-hidden="true">application content</div>
<div class="ReactModalPortal">
    <div class="ReactModal__Overlay ReactModal__Overlay--after-open modal-overlay-6fODnA">
        <div tabindex="-1" role="dialog">modal content</div>
    </div>
</div>

Solution

  • Short Answer

    Adding aria-modal to your modal will remove this warning.

    Long Answer

    It took me a while to realise why our modals do not have this warning but yours would as we employ similar markup. We use the aria-modal property on our modals.

    Axe has been updated to expect the aria-modal property on a modal. aria-modal has average support at the moment but it is a good practice as it counters developer mistakes (as screen reader / browser combos that do respect it will automatically trap focus for you!).

    Hiding items outside a modal

    The only way to truly hide everything is to add tabindex="-1" to every single interactive item.

    However in reality that is far more likely to cause a catastrophic accessibility issue if your JS function you use to add tabindex="-1" to every interactive element encounters an issue and doesn't successfully revert the tabindex or remove it. This would mean you leave parts of the page completely inaccessible!

    Obviously you would then fail WCAG on the "Robust" part of POUR. Please don't do this.

    The best compromise is to use aria-hidden on the <main> and <aside> containers (any top level containers). Then use aria-modal on your modal as this will trap focus in some browser / screen reader combos. That combination of aria will provide the highest coverage for browser support.

    Finally you should manage focus for people using the tab key. This is our backup in case the above methods fail and for people not using a screen reader (i.e. people with dexterity or accuracy issues who can't use a mouse.)

    If you need information on how to trap the tab focus within a modal I will provide a code sample but it is pretty straight forward.

    Managing tab key focus will not stop screen reader users or misbehaving plugins from getting outside your modal (if the other methods fail) but believe me, if they have a problem with your site after you implement the above they will have bigger problems on other sites.

    inert - a further string to your bow?

    Finally as another backup we add inert to the items outside of our modal. Support isn't great, but every little helps!

    You can polyfill it if you want but I don't think it has moved outside of the draft spec yet so we just use it as is.

    It is purely there as yet another add in and (hopefully) to future proof our legacy applications as inert is a much needed and easy to understand attribute. It blocks screen readers access to items without changing visual design (basically aria-hidden but as a standard attribute, with the advantage that it effectively removes all children from the accessibility tree.)

    Example

    Try removing the aria-modal="true" from the following example and running Axe, the warning will return.

    <main aria-hidden="true" inert><a href="https://google.com">test</a></main>
    <div class="modal" aria-hidden="false" aria-modal="true">
        <label for="iTest">input test</label>
        <input id="iTest"/>
    </div>