Focus Trap Inert

Recipe 6.4 Keep focus contained

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>

<div class="page-wrapper"> 
  <header>
    <a href="#">Home</a>
  </header>

  <main>
    <button class="open">Login</button>
  </main>
</div>

<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 pageWrapper = document.querySelector('.page-wrapper')
const dialogOpen = document.querySelector(".open");
const dialogClose = document.querySelector(".close");
const dialog = document.querySelector('[role="dialog"]');
const heading = dialog.querySelectorAll('[tabindex="-1"]')[0];
let trigger;

dialogOpen.addEventListener("click", open);
dialogClose.addEventListener("click", close);

function open() {
  trigger = document.activeElement;
  pageWrapper.setAttribute('inert', 'inert') 
  dialogToggle(); 
}

function close() {
  pageWrapper.removeAttribute('inert')
  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