I recently read this article by Keith Grant which introduced the newly arrived
and inside of it is an input element. If you want to style that
when the contained input has focus, you can do it like so:
div:focus-within {
border: 2px solid red;
}
CodePen Embed Fallback
The CSS trick to focus trapping
Let’s exploit :focus-within and CSS transitions to implement a basic focus trap inside of a element.
To summarise, here is how the trick works. When the focus is not within the dialog (and the dialog is open), we:
trigger a CSS transitiondetect that transition completion in JavaScriptfocus the first element in the dialog
But first, let’s get set up. Here is the basic dialog and opening functionality:
button.onclick = () => {
modal.showModal();
}
There we go. Clicking on the button should open our dialog. Just this much code required to make a basic working modal using the new
CodePen Embed Fallback
Note: As in the above example demo of dialog, you’ll notice some extra polyfill code to make work in browsers where it isn’t supported.
If you opened the dialog in the example above and started tabbing several times, you may have already noticed the problem: the focus starts with elements in the dialog, but then leaves once the last element in the dialog has been passed.
This is the core of our trick. We somehow need to send the lost focus detected with :focus-within over to JavaScript so that we can send the focus back to the dialog. This is where CSS transitions come into play. A CSS transition is something that happens through CSS, but emits events in JavaScript too. In our case, we can trigger a transition on any property with a negligible (because it doesn’t matter in our case) visual difference and listen for the transition completion in JavaScript.
Note that we need to trigger this transition when the dialog is open but doesn’t have focus inside it.
dialog {
background-color: rgb(255, 255, 255);
}
dialog[open]:not(:focus-within) {
background-color: rgb(255, 255, 254);
transition: background-color 0.01s;
}
Let’s see what that CSS is doing.
We put a background-color of our choice on the dialog. This isn’t necessary, but ensures we have the same background color across browsers.The dialog[open]:not(:focus-within) selector applies when the dialog is open but doesn’t have focus on or inside it. This works because native element puts an open attribute when it’s open.Inside this rule, we change the background-color by a minimum amount. This is the least change required to trigger a CSS animation and at the same time not causing any visual difference for the user (remember this is a dummy transition). Also, we set the transition property with a very small duration because we want it to finish as soon as possible and get detected in JavaScript.
A touch of JavaScript
Now, all we need to do is detect the end of our triggered CSS transition and focus back the first element inside the modal, like so:
modal.addEventListener(transitionend, (e) => {
modal.querySelector(input).focus();
});
We attach a transitionend listener on the modal and inside the callback, we focus the first input inside the modal. Done!
CodePen Embed Fallback
Limitations
This is a quick experiment I did to create a working proof-of-concept of focus trapping with the :focus-within pseudo-class. It has several limitations compared to dedicated JavaScript solutions to achieve this. Nevertheless, something is better than nothing!
Here are a couple of things this implementation lacks:
According to W3C guidelines, the focus should cycle on the focusable element. But we are always focusing on the first input element. This is because without writing more JavaScript, we cannot know whether the focus was lost from the first or last element.We are always focusing back to the first input element. But there are many more focusable HTML elements that might be present before input or maybe there is not input element at all inside the modal. Again, full-fledged JavaScript solutions detect and maintain a list of all focusable elements and focus the right one.
Better (JavaScript) implementations of focus trapping
As mentioned earlier, one working example is available inside the W3C documentation itself.Here is another implementation by Rodney Rehm that also listens for tab.Greg Kraus has a library that achieves this. His implementation maintains a list of selectors for all valid focusable elements.One more lightweight library to create accessible modals.
That is all for this experiment. If you liked this trick, you can follow me on Twitter where I share more articles and side projects of mine.
div:focus-within {
border: 2px solid red;
}
CodePen Embed Fallback
The CSS trick to focus trapping
Let’s exploit :focus-within and CSS transitions to implement a basic focus trap inside of a
To summarise, here is how the trick works. When the focus is not within the dialog (and the dialog is open), we:
trigger a CSS transitiondetect that transition completion in JavaScriptfocus the first element in the dialog
But first, let’s get set up. Here is the basic dialog and opening functionality:
button.onclick = () => {
modal.showModal();
}
There we go. Clicking on the button should open our dialog. Just this much code required to make a basic working modal using the new
CodePen Embed Fallback
Note: As in the above example demo of dialog, you’ll notice some extra polyfill code to make
If you opened the dialog in the example above and started tabbing several times, you may have already noticed the problem: the focus starts with elements in the dialog, but then leaves once the last element in the dialog has been passed.
This is the core of our trick. We somehow need to send the lost focus detected with :focus-within over to JavaScript so that we can send the focus back to the dialog. This is where CSS transitions come into play. A CSS transition is something that happens through CSS, but emits events in JavaScript too. In our case, we can trigger a transition on any property with a negligible (because it doesn’t matter in our case) visual difference and listen for the transition completion in JavaScript.
Note that we need to trigger this transition when the dialog is open but doesn’t have focus inside it.
dialog {
background-color: rgb(255, 255, 255);
}
dialog[open]:not(:focus-within) {
background-color: rgb(255, 255, 254);
transition: background-color 0.01s;
}
Let’s see what that CSS is doing.
We put a background-color of our choice on the dialog. This isn’t necessary, but ensures we have the same background color across browsers.The dialog[open]:not(:focus-within) selector applies when the dialog is open but doesn’t have focus on or inside it. This works because native element puts an open attribute when it’s open.Inside this rule, we change the background-color by a minimum amount. This is the least change required to trigger a CSS animation and at the same time not causing any visual difference for the user (remember this is a dummy transition). Also, we set the transition property with a very small duration because we want it to finish as soon as possible and get detected in JavaScript.
A touch of JavaScript
Now, all we need to do is detect the end of our triggered CSS transition and focus back the first element inside the modal, like so:
modal.addEventListener(transitionend, (e) => {
modal.querySelector(input).focus();
});
We attach a transitionend listener on the modal and inside the callback, we focus the first input inside the modal. Done!
CodePen Embed Fallback
Limitations
This is a quick experiment I did to create a working proof-of-concept of focus trapping with the :focus-within pseudo-class. It has several limitations compared to dedicated JavaScript solutions to achieve this. Nevertheless, something is better than nothing!
Here are a couple of things this implementation lacks:
According to W3C guidelines, the focus should cycle on the focusable element. But we are always focusing on the first input element. This is because without writing more JavaScript, we cannot know whether the focus was lost from the first or last element.We are always focusing back to the first input element. But there are many more focusable HTML elements that might be present before input or maybe there is not input element at all inside the modal. Again, full-fledged JavaScript solutions detect and maintain a list of all focusable elements and focus the right one.
Better (JavaScript) implementations of focus trapping
As mentioned earlier, one working example is available inside the W3C documentation itself.Here is another implementation by Rodney Rehm that also listens for tab.Greg Kraus has a library that achieves this. His implementation maintains a list of selectors for all valid focusable elements.One more lightweight library to create accessible modals.
That is all for this experiment. If you liked this trick, you can follow me on Twitter where I share more articles and side projects of mine.