Code:
<style>
[role="dialog"] {
align-items: center;
background: rgb( 0 0 0 / 0.2);
inset: 0;
justify-content: center;
margin: auto;
position: fixed;
}
[role="dialog"]:not([hidden]) {
display: flex;
}
[role="dialog"] > div {
background: rgb(255 255 255);
box-sizing: border-box;
max-width: 40rem;
padding: 2rem;
width: 90vw;
}
:focus-visible {
outline: 4px solid #123456;
outline-offset: 4px;
}
[role="dialog"]:focus-visible {
outline-offset: -8px;
}
</style>
<header>
<a href="#">Home</a>
</header>
<main>
<button class="open">Login</button>
</main>
<div role="dialog" aria-modal="true" hidden aria-labelledby="heading">
<div>
<button class="close">Close</button>
<h1 id="heading" tabindex="-1">Login</h1>
<a href="#">A link</a>
</div>
</div>
<script>
const dialogOpen = document.querySelector(".open");
const dialogClose = document.querySelector(".close");
const dialog = document.querySelector('[role="dialog"]');
const heading = dialog.querySelectorAll('[tabindex="-1"]')[0];
const focusableElements = dialog.querySelectorAll("button, a");
let trigger;
dialogOpen.addEventListener("click", open);
dialogClose.addEventListener("click", close);
dialog.addEventListener("keydown", (e) => {
if (e.code !== "Tab") return;
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
const active = document.activeElement;
if (e.shiftKey) {
if (first === active) {
e.preventDefault();
last.focus();
}
} else if (last === active) {
e.preventDefault();
first.focus();
}
});
function open() {
trigger = document.activeElement;
dialogToggle();
}
function close() {
dialogToggle();
}
function dialogToggle() {
const isOpen = !dialog.hasAttribute("hidden");
if (!isOpen) {
dialog.removeAttribute("hidden");
heading.focus();
} else {
dialog.setAttribute("hidden", "hidden");
trigger.focus();
}
}
</script>
Live Demo on CodePen