skip to Main Content

I am trying to create an accessible navigation menu and I want to trap the focus within each panel. When I click on the hamburger button, it will open the panel displaying a list of buttons or links. I want to be able to tab/up&down arrow through the options and have it loop again based on whether its the first or last focusable element.

If it’s just one level of navigation links, I know I can get all of the focusable elements and identify the first and last sibling and add an event listener keydown to loop, but my problem is that these navigation links have sub levels and I’m not really sure how to approach it.

In my codepen, I made a function where I get the id of the panel and get the array of the focusable elements and set it in a map. I’m not sure if that’s the right approach because my thought was to identify which panel is currently active then get the array of focusable elements from the map, find the first/last focusable element, then add the eventlistener to check what element is being focused until it finds the first or last and then loops. But when I was coding it, it just didn’t feel right at all.

const navigation = document.getElementsByClassName("n-navigation")[0];
const navigationMobile= navigation.getElementsByClassName("n-navigation__mobile")[0];
const navigationBlur= navigation.getElementsByClassName("n-navigation__blur")[0];

const hamburgerMenuBtn = navigation.firstElementChild;
hamburgerMenuBtn.addEventListener("click", openHamburgerMenu);

const mainContainer = navigationMobile.firstElementChild;
mainContainer.id = "main-container";
const closeMenuBtn = mainContainer.firstElementChild;
closeMenuBtn.addEventListener("click", closeMenu);
const mainList = mainContainer.getElementsByTagName("ul")[0];

const triggers = mainList.querySelectorAll("button.n-navigation__mobile__mainContainer__mainList__trigger, button.n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__trigger");
const previousBtns = mainList.querySelectorAll("button.n-navigation__mobile__mainContainer__mainList__panel__subContainer__previous, button.n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel__container__previous");

let activePanel;
let activeContainer;

let focusableMap = new Map();

for (let a=0; a < triggers.length; a++) {
  triggers[a].id = "trigger-id-" + a;
  triggers[a].addEventListener("click", nextTopic);
  
  let panel = triggers[a].nextElementSibling;
  panel.id = "panel-id-" + a;
  
  triggers[a].setAttribute("aria-controls", panel.id);
}
for (let a=0; a < previousBtns.length; a++) {
  previousBtns[a].addEventListener("click", previousTopic);
}

function openHamburgerMenu() {
  navigation.classList.add("n-navigation--active");
  navigationBlur.classList.remove("n-navigation__blur--hide");
  navigationBlur.classList.add("n-navigation__blur--show");
  this.setAttribute("aria-expanded", "true");
  
  if (!focusableMap.has(mainContainer.id)) {
    let container = mainContainer.getAttribute('class');
    addFocusableToMap(mainContainer, container);
  }
}

function closeMenu() {
  navigation.classList.remove("n-navigation--active");
  navigationBlur.classList.remove("n-navigation__blur--show");
  navigationBlur.classList.add("n-navigation__blur--hide");
  hamburgerMenuBtn.setAttribute("aria-expanded", "false");
}

function nextTopic() {
  console.log("next topic");
  
  let panel = this.nextElementSibling;
  let container = panel.firstElementChild.getAttribute('class');
  if (!focusableMap.has(panel.id)) addFocusableToMap(panel, container);
  
  if (this.classList.contains("n-navigation__mobile__mainContainer__mainList__trigger")) {
    mainList.parentElement.classList.add("n-navigation__mobile__mainContainer--right");
    panel.classList.add("n-navigation__mobile__mainContainer__mainList__panel--active");
  } else {
    activeContainer = activePanel;
    activePanel.classList.add("n-navigation__mobile__mainContainer__mainList__panel--right");
    panel.classList.add("n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel--active");
  }
  activePanel = panel;

}

function previousTopic() {
  console.log("previous topic");
  
  if (this.classList.contains("n-navigation__mobile__mainContainer__mainList__panel__subContainer__previous")) {
    mainList.parentElement.classList.remove("n-navigation__mobile__mainContainer--right");
    activePanel.classList.remove("n-navigation__mobile__mainContainer__mainList__panel--active");
    activePanel = null;  
  } else {
    activeContainer.classList.remove("n-navigation__mobile__mainContainer__mainList__panel--right");
    activePanel.classList.remove("n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel--active");
    activePanel = activeContainer;
    activeContainer = null;
  }
}

function addFocusableToMap(panel, container) {
  const focusable = panel.querySelectorAll("." + container + " > button, ." + container + " > ul > li > button, ." + container + " > ul > li > a");
  const newFocusable = [];
  
  for (let a=0; a < focusable.length; a++) {
    focusable[a].dataset.triggerid = panel.id;
    newFocusable.push(focusable[a]);
  }
  focusableMap.set(panel.id, focusable);

}
body {
  margin: 0;
}

.n-navigation {
  display: grid;
  height: 4.375em;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  padding: 1em;
  background-color: #f7f9fa;
}
@media screen and (min-width: 50em) {
  .n-navigation__hamburgerMenu {
    display: none;
  }
}
.n-navigation__logo {
  text-align: center;
}
@media screen and (min-width: 50em) {
  .n-navigation__logo {
    text-align: left;
  }
}
.n-navigation__mobile {
  height: 100vh;
  width: 20em;
  background-color: white;
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  transform: translateX(-20em);
  visibility: hidden;
  z-index: 0;
  transition: transform 350ms, z-index 0s, visibility 0s;
  transition-delay: 0s, 350ms, 350ms;
}
@media screen and (min-width: 50em) {
  .n-navigation__mobile {
    display: none;
  }
}
.n-navigation__mobile__mainContainer {
  padding: 1.375em 1.5em;
  transform: translateX(0);
  transition: transform 350ms;
  visibility: visible;
}
.n-navigation__mobile__mainContainer ul {
  list-style-type: none;
  padding-left: 0;
}
.n-navigation__mobile__mainContainer__close {
  display: block;
  margin-left: auto;
}
.n-navigation__mobile__mainContainer__mainList {
  margin-top: 2.25em;
  margin-bottom: 3em;
}
.n-navigation__mobile__mainContainer__mainList__trigger {
  display: flex;
  width: 100%;
  background-color: transparent;
  color: #1d1d1f;
  text-decoration: none;
  border: none;
  padding: 0.5em 0;
  justify-content: space-between;
  align-items: center;
}
.n-navigation__mobile__mainContainer__mainList__trigger:hover, .n-navigation__mobile__mainContainer__mainList__trigger:focus {
  cursor: pointer;
}
.n-navigation__mobile__mainContainer__mainList__trigger:hover .n-navigation__mobile__mainContainer__mainList__trigger__icon, .n-navigation__mobile__mainContainer__mainList__trigger:focus .n-navigation__mobile__mainContainer__mainList__trigger__icon {
  color: #c41320;
}
.n-navigation__mobile__mainContainer__mainList__trigger__copy {
  font-family: tahoma;
  font-size: 1.625rem;
}
.n-navigation__mobile__mainContainer__mainList__trigger__icon {
  font-size: 1.5em;
  transition: color 350ms;
}
.n-navigation__mobile__mainContainer__mainList__panel {
  width: 100%;
  position: absolute;
  top: 0;
  left: 0;
  transform: translateX(-20em);
  transition: transform 350ms;
  visibility: hidden;
  transition: transform 350ms, visibility 0s;
  transition-delay: 0s, 350ms;
}
.n-navigation__mobile__mainContainer__mainList__panel__subContainer {
  padding: 3em 1.5em;
}
.n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel {
  width: 100%;
  position: absolute;
  top: 0;
  left: 0;
  transform: translateX(-20em);
  visibility: hidden;
  transition: transform 350ms, visibility 0s;
  transition-delay: 0s, 350ms;
}
.n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel__container {
  padding: 3em 1.5em;
}
.n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel--active {
  visibility: visible;
  transition: transform 350ms, visibility 0s;
  transition-delay: 0s, 0s;
}
.n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel--right {
  transform: translateX(20em);
  visibility: hidden;
  transition: transform 350ms, visibility 0s;
  transition-delay: 0s, 350ms;
}
.n-navigation__mobile__mainContainer__mainList__panel--right {
  transform: translateX(0);
}
.n-navigation__mobile__mainContainer__mainList__panel--active {
  visibility: visible;
  transition: transform 350ms, visibility 0s;
  transition-delay: 0s, 0s;
}
.n-navigation__mobile__mainContainer--right {
  transform: translateX(20em);
  visibility: hidden;
  transition: transform 350ms, visibility 0s;
  transition-delay: 0s, 350ms;
}
.n-navigation__desktop {
  display: none;
}
@media screen and (min-width: 50em) {
  .n-navigation__desktop {
    display: block;
  }
}
.n-navigation__blur {
  display: block;
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: rgba(18, 18, 18, 0.36);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.n-navigation__blur--hide {
  opacity: 0;
  visibility: hidden;
  z-index: -1;
  transition: opacity 450ms, visibility 0s, z-index 0s;
  transition-delay: 0s, 450ms, 450ms;
}
.n-navigation__blur--show {
  opacity: 1;
  visibility: visible;
  z-index: 0;
}
.n-navigation--active .n-navigation__mobile {
  transform: translateX(0);
  visibility: visible;
  z-index: 2;
  transition: transform 450ms, visibility 0s;
  transition-delay: 0s, 0s;
}
<nav class="n-navigation" role="navigation">
  <button class="n-navigation__hamburgerMenu" aria-label="Menu" aria-expanded="false">
     <svg aria-hidden="true" viewBox="0 0 24 24" role="img" width="24px" height="24px" fill="none">
       <path stroke="currentColor" stroke-width="1.5" d="M21 5.25H3M21 12H3m18 6.75H3"></path>
     </svg>
  </button>
  <div class="n-navigation__logo">
  <a href="#">Site Name</a>
  </div>
  
  <div class="n-navigation__mobile">
    <div class="n-navigation__mobile__mainContainer">
      <button class="n-navigation__mobile__mainContainer__close">X</button>
      <ul class="n-navigation__mobile__mainContainer__mainList">
        <li>
          <button class="n-navigation__mobile__mainContainer__mainList__trigger">
            <span class="n-navigation__mobile__mainContainer__mainList__trigger__copy">Menu Item 1</span>
            <span class="n-navigation__mobile__mainContainer__mainList__trigger__icon"> > </span>
          </button>
          <div class="n-navigation__mobile__mainContainer__mainList__panel">
            <div class="n-navigation__mobile__mainContainer__mainList__panel__subContainer">
              <button class="n-navigation__mobile__mainContainer__mainList__panel__subContainer__previous">All</button>
              <h2>Menu Item 1</h2>
              <ul class="n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList">
                <li>
                  <a href="#" class="n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__trigger">
                    <span>Menu Item 1's</span>
                  </a>
                </li>
                <li>
                  <a href="#" class="n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__trigger">
                    <span>Menu Item 1's</span>
                  </a> 
                </li>
                <li>
                  <button class="n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__trigger">
                    <span>Menu Item 1's</span>
                    <span> > </span>
                  </button>
                  <div class="n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel">
                    <div class="n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel__container">
                      <button class="n-navigation__mobile__mainContainer__mainList__panel__subContainer__subList__panel__container__previous">Menu Item 1's</button>
                      <h3>Menu Item 1's</h3>
                      <ul>
                        <li>
                          <a href="#">HiM.\</a>
                        </li>
                        <li>
                          <a href="#">Herrr</a>
                        </li>
                      </ul>
                    </div>
                  </div>
                </li>
              </ul>
            </div>
          </div>
        </li>
        <li>
          <button class="n-navigation__mobile__mainContainer__mainList__trigger">
            <span class="n-navigation__mobile__mainContainer__mainList__trigger__copy">Menu Item 2</span>
            <span class="n-navigation__mobile__mainContainer__mainList__trigger__icon"> > </s>
          </button>
          <div class="n-navigation__mobile__mainContainer__mainList__panel">
            <div class="n-navigation__mobile__mainContainer__mainList__panel__subContainer">
              <button class="n-navigation__mobile__mainContainer__mainList__panel__subContainer__previous">All</button>
              <h2>Menu Item 2</h2>
              <ul>
                <li>Menu Item 2's</li>
              </ul>
            </div>
          </div>
        </li>
        <li>
          <a href="#" class="n-navigation__mobile__mainContainer__mainList__trigger">
            <span class="n-navigation__mobile__mainContainer__mainList__trigger__copy">Menu Item 3</span>
          </a>
        </li>
      </ul>
    </div>
  </div>

  <div class="n-navigation__desktop"></div>
  <div class="n-navigation__iconMenu">
    search and cart
  </div>
  <div class="n-navigation__blur n-navigation__blur--hide"></div>
</nav>

2

Answers


  1. you could try this

    add document.querySelector('.n-navigation__mobile__mainContainer__mainList__trigger').focus(); to your openHamburgerMenu() function

    by doing this, you can tab to the menu item options, you could modify this to include handling up and down arrows

    function openHamburgerMenu() {
      navigation.classList.add("n-navigation--active");
      navigationBlur.classList.remove("n-navigation__blur--hide");
      navigationBlur.classList.add("n-navigation__blur--show");
      this.setAttribute("aria-expanded", "true");
      
      if (!focusableMap.has(mainContainer.id)) {
        let container = mainContainer.getAttribute('class');
        addFocusableToMap(mainContainer, container);
      }
      
      document.querySelector('.n-navigation__mobile__mainContainer__mainList__trigger').focus();
    }
    
    Login or Signup to reply.
  2. Check out the accessible dialog pattern from the W3C ARIA Authoring Practices Guide (APG). It contains working code that uses plain javascript.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search