Task

Ensure modals are keyboard navigable

Category: 
Keyboard
Where: 
Custom code
Check as complete

When keyboard users tabs into modals (like a popup or dialog), focus should remain in that window. This helps prevent users from accidentally moving focus outside the modal without closing it. Users should be able to use the ESC key to close the modal.

Tab through your page to make sure modals are keyboard navigable

You want to make sure that as you tab through your page, focus doesn’t jump outside the modal window to the main page where it will be hidden.

  1. Tap enter on the button that opens the modal
  2. Tab into the elements within the modal

When the modal is open, try tapping the ESC key to make sure it closes the modal. If it doesn’t, follow the steps below.

Accessible modal components

A common way to build a modal in Webflow consists of a button that is a direct sibling to the main modal element. An interaction is set on the button to show the sibling content element when clicked. We should also add some specific attributes on the button, modal and children elements along with some custom code to trap the user’s focus when the modal is open and allow them to close it with the ESC key.

Reference the cloneable project below that uses the proper structure, attributes and code outlined in this task.

Structure & class names

<checks-richtext_class>modal-open_btn<checks-richtext_class>

<checks-richtext_class>modal-wrapper<checks-richtext_class>

<checks-richtext_class>modal-container<checks-richtext_class>

<checks-richtext_class>modal-close_btn<checks-richtext_class>

<checks-richtext_class>modal-close_area<checks-richtext_class>

Webflow Designer canvas with modal button selected and navigator open showing the structure of elements

Custom attributes

How to add custom attributes: https://university.webflow.com/lesson/custom-attributes

<checks-richtext_class>modal-open_btn<checks-richtext_class>

  • role= "button"

<checks-richtext_class>modal-wrapper<checks-richtext_class>

  • role="dialog"
  • aria-modal="true"
  • aria-labelledby="Popup Modal"

<checks-richtext_class>modal-close_btn<checks-richtext_class>

  • role=”button”

<checks-richtext_class>modal-close_area<checks-richtext_class>

  • tabindex="-1"
  • aria-hidden="true"

Webflow Designer with modal button selected and element settings panel open
Webflow Designer with modal wrapper selected and element settings panel open
Webflow Designer with modal close area selected and element settings panel open
Webflow Designer with modal close button selected and element settings panel open

Custom code

Add the custom code below to the before </body> section of the page or project settings.

Note: the script uses the class of <checks-richtext_class>modal-open_btn<checks-richtext_class> on the trigger element, <checks-richtext_class>modal-wrapper<checks-richtext_class> on the content element, <checks-richtext_class>modal-close_btn<checks-richtext_class> and <checks-richtext_class>modal-close_area<checks-richtext_class> for the close triggers. To use different classes, update the class names in the script as well.



<script>
    $(document).ready(function() {
        var buttonThatOpenedModal;
        var findModal = function(elem) {
            var tabbable = elem.find('select, input, textarea, button, a').filter(':visible');

            var firstTabbable = tabbable.first();
            var lastTabbable = tabbable.last();
            /*set focus on first input*/
            firstTabbable.focus();

            /*redirect last tab to first input*/
            lastTabbable.on('keydown', function(e) {
                if ((e.which === 9 && !e.shiftKey)) {
                    e.preventDefault();
                    firstTabbable.focus();
                }
            });

            /*redirect first shift+tab to last input*/
            firstTabbable.on('keydown', function(e) {
                if ((e.which === 9 && e.shiftKey)) {
                    e.preventDefault();
                    lastTabbable.focus();
                }
            });

            /* allow escape key to close insiders div */
            elem.on('keydown', function(e) {
                if (e.keyCode === 27) {
                    $(elem).find('.modal-close_btn').click();
                };
            });
        };

        var modalOpenButton = $('.modal-open_btn');
        modalOpenButton.on('keydown', function(e) {
            // Only activate on spacebar and enter
            if (e.which !== 13 && e.which !== 32) {
                return;
            }

            e.preventDefault();

            // Simulate a mouseclick to trigger Webflow's IX2/Interactions
            var evt = document.createEvent("MouseEvents");
            evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
            $(this).get(0).dispatchEvent(evt);
        });
        modalOpenButton.on('click', function() {
            $(this).next().show();
            findModal($(this).next());
            buttonThatOpenedModal = $(this);
        });

        var modalCloseButton = $('.modal-close_btn, .modal-close_area');
        modalCloseButton.on('keydown', function(e) {
            // Only activate on spacebar and enter
            if (e.which !== 13 && e.which !== 32) {
                return;
            }

            e.preventDefault();

            // Simulate a mouseclick to trigger Webflow's IX2/Interactions
            var evt = document.createEvent("MouseEvents");
            evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
            $(this).get(0).dispatchEvent(evt);
        });
        modalCloseButton.on('click', function() {
            $(this).closest('.modal-wrapper').hide();
            if (buttonThatOpenedModal) {
                buttonThatOpenedModal.focus();
                buttonThatOpenedModal = null;
            }
        });
    });

</script>
Copy codeCopied!
Back to checklist

Total progress

Congratulations on making the web a more accessible place. Celebrate your work on Twitter.
Celebration horn and streamer emoji
0 / 0
Hide progress
Show progress