Using JavaScript to trap focus in an element

category: code

For information on how to accessibly implement the components I’m working on, I often refer to WAI-ARIA Authoring Practices specification. One thing this spec sometimes recommends, is to trap focus in an element, for example in a modal dialog while it is open. In this post I will show how to implement this.

Some history

In the early days of the web, web pages used to be very simple: there was lots of text, and there were links to other pages. Easy to navigate with a mouse, easy to navigate with a keyboard.

The modern web is much more interactive. We now have ‘rich internet applications’. Elements appear and disappear on click, scroll or even form validation. Overlays, carousels, AJAX… most of these have ‘custom’ behaviour. Therefore we cannot always rely on the browser’s built-in interactions to ensure user experience. We go beyond default browser behaviour, so the duty to fix any gaps in user experience is on us.

One common method of ‘fixing’ the user’s experience, is by carefully shifting focus around with JavaScript. The browser does this for us in common situations, for example when tabbing between links, when clicking form labels or when following anchor links. In less browser-predictable situations, we will have to do it ourselves.

When to trap focus

Trapping focus is a behaviour we usually want when there is modality in a page. Components that could have been pages of their own, such as overlays and dialog boxes. When such components are active, the rest of the page is usually blurred and the user is only allowed to interact with our component.

Not all users can see the visual website, so we will also need to make it work non-visually. The idea is that if for part of the site we prevent clicks, we should also prevent focus.

Some examples of when to trap focus:

In these cases we would like to trap focus in the modal, alert or navigation menu, until they are closed (at which point we want to undo the trapping and return focus to the element that instantiated the modal).


We need these two things to be the case during our trap:


In order to implement the above behaviour on a given element, we need to get a list of the focusable elements within it, and save the first and last one in variables.

In the following, I assume the element we trap focus in is stored in a variable called element.

Get focusable elements

In JavaScript we can figure out if elements are focusable, for example by checking if they either are interactive elements or have tabindex.

This gives a list of common elements that are focusable:

var focusableEls = element.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])');

This is an example list of common elements; there are many more focusable elements. Note that it is useful to exclude disabled elements here.

Save first and last focusable element

This is a way to get the first and last focusable elements within an element:

var firstFocusableEl = focusableEls[0];  
var lastFocusableEl = focusableEls[focusableEls.length - 1];

We can later compare these to document.activeElement, which contains the element in our page that currently has focus.

Listen to keydown

Next, we can listen to keydown events happening within the element , check whether they were TAB or SHIFT TAB and then apply logic if the first or last focusable element had focus.

var KEYCODE_TAB = 9;

element.addEventListener('keydown', function(e) {
    if (e.key === 'Tab' || e.keyCode === KEYCODE_TAB) {
        if ( e.shiftKey ) /* shift + tab */ {
            if (document.activeElement === firstFocusableEl) {
        } else /* tab */ {
            if (document.activeElement === lastFocusableEl) {

Alternatively, you can add the event listener to the first and last items. I like the above approach, as with one listener, there is only one to undo later.

Putting it all together

With some minor changes, this is my final trapFocus() function:

function trapFocus(element) {
    var focusableEls = element.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),
        firstFocusableEl = focusableEls[0];  
        lastFocusableEl = focusableEls[focusableEls.length - 1];
        KEYCODE_TAB = 9;

    element.addEventListener('keydown', function(e) {
        var isTabPressed = (e.key === 'Tab' || e.keyCode === KEYCODE_TAB);

        if (!isTabPressed) { 

        if ( e.shiftKey ) /* shift + tab */ {
            if (document.activeElement === firstFocusableEl) {
        } else /* tab */ {
            if (document.activeElement === lastFocusableEl) {


In this function, we have moved the check for tab to its own variable (thanks Job), so that we can stop function execution right there.

Further reading

See also Trapping focus inside the dialog by allyjs, Dialog (non-modal) in WAI-ARIA Authoring Practices and Can a modal dialog be made to work properly for screen-reader users on the web? by Everett Zufelt.

Edit 26-09-2019: added :not([disabled]) to selector

Thanks Job, Rodney, Matijs and Michiel for your suggestions!

Update 13 August 2018: examples now use vanilla JavaScript instead of jQuery.

Comments & mentions (20)

Roel Van Gils 30 Jan 2017 13:57:48

Perhaps you should mention in the article that this technique only works reliably for (sighted) users who rely on keyboard navigation.

Screenreader users typically use a virtual focus to get around on a page. (⌃ + ⌘ + Arrow keys when using Voiceover, for example).

Johan Ronsse 01 Feb 2017 12:50:23

Very interesting.

My theory is that if what is “behind” the { modal/popover/interactive element that has its own focus cycle} can be put in a separate container, and we temporarily remove it from the DOM flow, we can work around this whole problem.

A screenreader wouldn’t even see it.

But that’s just an idea. I have some more reading to do.

Ari Picker replied: I implemented my first focus trap. It worked but had a bit of jank.
Luckily I came across Hidde de Vries; his article is great.…
Danielle Fuechtmann replied: Designing ethically is essential; here are some great ways to apply ethics to front-end web development:…
Lena C. replied: How to trap focus in a modal dialog… #frontend #a11y
Bobby 14 Nov 2018 02:22:10

Great example. namespace is not used.

Matt T 11 Feb 2019 16:02:00

This example is nice and concise but actually seems ineffective. You can’t actually tab to the first and last item directly because the focus is then skipped to the next wrapped around item. You would have to detect when the focus it’s outside the element and then trigger this trap, because the tab event handling occurs after the browser has already moved the focus itself.

Hidde de Vries 26 Mar 2019 07:11:52

Hi Matt T: I’m not sure what you mean. What we’re handling is a keyboard event and we’re preventing its default behaviour (moving focus to the next focusable element), so the browser would not move focus. Which part doesn’t work for you?

Ale Narvaja ???? replied:
Using JavaScript to trap focus in an element ????… #webdevelopment #javascript #CodeNewbie #100DaysOfCode
Tore Aurstad replied: Using JavaScript to trap focus in an element…
Stefan Houtzager 18 Nov 2019 12:52:00

Thanks for writing this. I have used some of your ideas, added others and created

Yavy 21 Nov 2019 20:24:00

but where do you call trapFocus() function? I have a callout window that I am creating dynamically

Hidde 22 Nov 2019 13:03:39

@Yavy: Whenever trapping is needed, so if you’ve just created a modal and the user opens it, that’s the time you’d usually want to enforce focus trap, removing it whenever the user triggers the modal to close.

Srishti 02 Mar 2020 20:18:50

Thank you for this! But how would you provide focus trap in a dialog with multiple links? Your code seems to work with two links(first & last) but I am not sure how to get it working for multiple links. Please advise.

Carson Powers 22 Apr 2020 20:34:00

What would the aria-role for such a component be? I believe it should be a role=“application” because you are taking over native browser functionality (tabbing) and overloading it with your own logic (tab trapping).

Hidde de Vries 23 Apr 2020 11:39:47

@Srishti It should work with any number of interactive elements. The script only needs to act when first or last are encountered, as or any others, TAB works as expected.

@Carson Powers: it depends on the component what a suitable ARIA role would be. In general, ARIA roles are best avoided unless they provide something useful.

Carson Powers 10 May 2020 05:57:00

I arrived on an answer: The role should be a dialog.

Hidde de Vries 19 May 2020 12:03:54

@Carson Powers: nah, not always! You can trap focus in an element with a role of dialog, but you can also do it in an element that does not have that role, the role is not necessary and depends on what you are trying to convey.

Kim 28 Jul 2020 22:16:00

@Bobby The namespace seems to be a leftover from when the examples were written for jQuery, where the `keydown` event listener had a jQuery namespace attached to it so that the listener could be removed by a separate trap release function. That related function no longer exists in the vanilla JS version.

My assumption is that the intended use is to set this once (such as on a modal container) and then use some other method of moving the focus in and out of the modal when it’s shown and hidden—thus no release function is necessary.

Hidde 05 Aug 2020 15:34:21

@Kim: thanks for this, you are absolutely right, I have just removed “namespace”!

Leave a comment
Posted a response to this?

This website uses Webmentions. You can manually notify me if you have posted a response, by entering the URL below.