Building an Accessible Recursive Menu System
Tags: a11y, javascript, vue
Published: 2022-01-03 21:22:30

Hey hey! It's me again. The guy that rambles like he knows what he's talking about but is really flying by the seat of his pants!

Today we're going to be building an accessible menu system in Nuxt using recursion! You'll be able to use this pattern in a variety of ways: navigation drop-downs, nested sidebar navigations, and plenty of others.

We'll be building it in the context of a sidebar navigation. Think "multiple sub-level navigation menus" akin to what you would expect to see in the sidebar of a documentation site.

Those can get nested and messy very quickly, but we're going to build _two components_ to handle the whole thing!

_*Note: The accessible menu portion of this article is based on [this article](https://www.smashingmagazine.com/2017/11/building-accessible-menu-systems/) by none other than [Heydon Pickering](https://twitter.com/heydonworks)*_

View the repo [here](https://github.com/drewclem/nuxt-ally-components)

Let's jump right in!

## Setting up your project
We're going to be working in [Nuxt](https://nuxtjs.org), so let's get started with spinning up a new project.

I'm actually working from an existing Nuxt project, but here's the command you'll run.

``` bash
npx create-nuxt-app ally-tuts
```

It's going to ask you some questions for initial project setup. My answers are below, but choose as you wish.

- Programming Language: Javascript
- Package Manager: Npm
- UI Framework: None (I know, crazy. Right?)
- Nuxt.js Modules: Axios
- Linting Tools: ESLint
- Testing Framework: None
- Rendering Mode: Universal (SSR / SSG)
- Deployment Target: Static (Static/Jamstack hosting)
- Development Tools: jsconfig.json

Now that we have that complete, let's set up a simple scaffold for our app.

### A Quick HTML Scaffold
First thing is to delete the Tutorial.vue and NuxtLogo.vue files in the components/ folder. Next, we'll add a `SidebarNav.vue` in our components folder.

From there, we'll create a layouts folder in the root of our project and add a default.vue component. In that file, we're going to import our `SidebarNav` component and put it in the template.

Generally, this is where you would set up your header and footer-- and any other global layout level stuff-- but that's out of scope for us so we'll keep it nice and simple.

```html
<!-- ~/layouts/default.vue -->

<template>
  <main>
    <SidebarNav />
    <nuxt />
  </main>
</template>
```
_*One cool thing to note here, is that we don't have to import our SidebarNav component! Nuxt just makes it available.*_

And with that, we can move forward!

## Building the Top Level
Again, we're building this in the context of a sidebar navigation. With that in mind, our next step is to create `SidebarNav.vue` in our components/ folder.

Within that, we'll make our root element an `nav` and we'll go ahead and give it an `id` of `Sidebar Navigation`- which we'll be using later. And then we want to create a `ul` element inside of our nav, and that will ultimately be where our different menu options render!

```html
<!-- ~/components/SidebarNav.vue -->

<template>
  <nav id="Sidebar Navigation">
    <ul>

    </ul>
  </nav>
</template>
```
_*Your markup should look like this.*_

From there, we're going to move into our `script` tag in our `SidebarNav.vue`-- and what we're doing here is just dropping in some static data that we'll use to pass to our components that will then build out our navigation menu.

Copy & paste the code below in your `SidebarNav.vue`

```javascript
// ~/components/SidebarNav.vue

<script>
export default {
  data() {
    return {
      navigation: [
        {
          title: "Menu 1",
          link: "/",
        },
        {
          title: "Menu 2",
          submenus: [
            {
              title: "Submenu 1",
              link: "/",
            },
            {
              title: "Submenu 2",
              link: "/",
            },
            {
              title: "Submenu 3",
              submenus: [
                {
                  title: "Subsubmenu 1",
                  link: "/",
                },
                {
                  title: "Subsubmenu 2",
                  link: "/",
                },
              ],
            },
          ],
        },
      ],
    };
  }
};
</script>
```

Next, we're going to place a component (that doesn't exist yet, we'll build that next) inside of the `ul`, let's call it `BaseMenu`.

What we'll do here is `v-for` over the items in the `navigation` data we just created and we're going to pass each item it loops over into `BaseMenu` as a prop.

We're also going to pass in a prop of `depth` and we'll set it at zero for this base level. Now, we're not actually going to do anything with the `depth` prop- but I've found it makes it tremendously easier to track which component is at which level once you get into the recursion side of things.

It's been super helpful in debugging too. You know there's an issue somewhere you see something with a depth of 1 or higher at your root level.

So, let's add our `BaseMenu` in.

```html
// ~/components/SidebarNav.vue

<template>
  <nav id="Sidebar Navigation">
    <ul>
      <BaseMenu
        v-for="(menu, index) in navigation"
        :menu="menu"
        :depth="0"
        :key="index"
      />
    </ul>
  </nav>
</template>
```

## Building the First Recursive Level
The piece we're building next is going to be two things.

First, it's going to be the `li` within our `ul` that we just built in our `SidebarNav.vue`. And secondly, it's going to be the layer that determines whether to render another recursive menu system or just spit out a link.

So, lets' create a `BaseMenu.vue` component in our components folder, and lets scaffold out our vue file with the root element being an `li`.

Let's also declare the props we know this component will be expecting, based on the work we just did in the `SidebarNav`.

We know there are two props coming in, `menu` and `depth`. `menu` is a type of object and we want it to be required. `depth` is a number, and we want it to be required as well.

```javascript
// ~/components/BaseMenu.vue

<template>
  <li>
  </li>
</template>

<script>
export default {
  props: {
    menu: {
      type: Object,
      required: true,
    },
    depth: {
      type: Number,
      required: true,
    },
  },
};
</script>
```

Let's take a step back for a second and look at what we need this to do next.

We know part two of this is that it needs to decide whether to render another menu system or a link. Knowing that, we know we can use a `v-if`.

If we take a look at the data we added in our `SidebarNav` component, you can see that there is only ever a `submenus` array or a `link`- which is a just a string- but there is never both a single `menu` object.

We can use that to determine which element to render. `If there is a submenus array = give us another menu level, if not = give us a link`.

That could look something like this.

```html
<!-- ~/components/BaseMenu.vue -->

<template>
  <li>
    <template v-if="menu.submenus">
    </template>

    <nuxt-link v-else>
    </nuxt-link>
  </li>
</template>
```
Looking back at our data again, we can see that _if_ a menu object is a link, then it has two keys: title, and link.

Let's use that to finish building out the link part of our `BaseMenu`

```html
<!-- ~/components/BaseMenu.vue -->

<template>
  <li>
    <template v-if="menu.submenus">
    </template>

    <nuxt-link
      v-else
      :to="menu.link"
      :id="menu.title.toLowerCase().replace(' ', '-')"
    >
      {{ menu.title }
    </nuxt-link>
  </li>
</template>
```
You'll notice I did a little javascript-ing on the ID, it's just lowercasing and replacing spaces with hyphens- this step is completely optional. It's just the pattern _I_ prefer for id's.

Now all that's left is to add a bit that will soon become our actual submenu that get's rendered when necessary.

Let's add a component `BaseMenuItem` in our `v-if` statement, and we'll pass it the same props that our `BaseMenu` component uses- which will be menu (and that's an object) and depth (which is a number).

Your `BaseMenu` component should be looking something like this.

```javascript
// ~/components/BaseMenu.vue

<template>
  <li>
    <template v-if="menu.submenus">
      <BaseMenuItem
        :menu="menu"
        :depth="depth + 1"
      />
    </template>
    <nuxt-link
      v-else
      :id="menu.title.toLowerCase().replace(' ', '-')"
      :to="menu.link"
    >
      {{ menu.title }}
    </nuxt-link>
  </li>
</template>

<script>
export default {
  props: {
    menu: {
      type: Object,
      required: true,
    },
    depth: {
      type: Number,
      required: true,
    },
  },
};
</script>
```

Now we're ready to build out the `BaseMenuItem` component we just added to project.

## Building the accessible menu
This is the part of the project that was built based on [this tutorial](https://www.smashingmagazine.com/2017/11/building-accessible-menu-systems/) by Heydon Pickering for Smashing Magazine. The write-up originally appeared in his book ["Inclusive Components".](https://inclusive-components.design/menus-menu-buttons/)

Let's outline some things this component needs before we jump into the code.

**The Basics**
- We need a `button` to show/hide a menu's submenu (we know this because we're building a nested menu system)
- We need a `ul` that shows/hides when it's parent button is clicked.
- We need a method (or function) to handle the click of parent button

**Accessibility needs**
Again, if you want a detailed breakdown of everything a11y about this system, I highly suggest reading through [Heydon's write-up](https://www.smashingmagazine.com/2017/11/building-accessible-menu-systems/)

- We need the `aria-haspopup` attribute on our parent button. This allows assistive technologies to inform the user that clicking this button will reveal more content.
- We need the `aria-expanded` attribute on our parent button. This allows assistive technologies to inform the user whether or not the menu is currently open.
- We need the `aria-controls` attribute on our parent button. The intention of `aria-controls` is to help screen reader users navigate from a controlling element to a controlled element. It's only available in JAWS screen readers, but some users may expect it.
- Pressing the `esc` key should close the currently focused menu
- Opening a menu should focus the first element within it.

This may read as if it's a lot, but it really isn't _that_ much work.

### The structure
We can start by laying out the basic structure of our component, and we'll incrementally add functionality and accessibility as we go.

So, we'll start with a basic Vue component that has a `button` and a `ul` in it. We can also declare the props we know are going to be passed in here- remember that's going to be menu and number, same as our previous component.

We'll also want to set the key of `isOpen` in our `data`, so we'll have a something to toggle with out button click and we can also use that value to determine when to show our submenu.

At this point, we can deduce that the text in our button will be the title of the menu that's passed into it. Knowing that, we can go ahead and set that up as well.

```javascript
// ~/components/BaseMenuItem.vue

<template>
  <div>
    <button>
      {{ menu.title }}
    </button>

    <ul>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isOpen: false,
    };
  },
  props: {
    depth: {
      type: Number,
      required: true,
    },
    menu: {
      type: Object,
      required: true,
    },
  },
};
</script>
```

Next we can get started making this menu do stuff. Let's add a click event to our button that calls a `toggleMenu` function.

```javascript
// ~/components/BaseMenuItem.vue

...
<button @click.prevent="toggleMenu">
  {{ menu.title }}
</buttton>
...
```
And in our `methods`, we'll create out `toggleMenu` function. All it's going to do for now is toggle or `isOpen` key to it's opposite value

```javascript
// ~/components/BaseMenuItem.vue

...
<script>
export default {
  ...
  methods: {
    toggleMenu() {
      this.isOpen = !this.isOpen;
    }
  }
}
</script>
...
```

Now that that is in place, we can add a `v-show` to our `ul` and dynamically render it based on the button click.

Another thing we'll do is create a computed property that is just going to sanitize an ID we can use for the parent button and submenus.

Drop the text `hello` into your `ul` and fire the app up with `yarn dev` or `npm run dev` and you should find two parent items, one of which is a button that reveals `hello` when you click it!

So far it's working!

```javascript
// ~/components/BaseMenuItem.vue

<template>
  <div>
    <button
      :id="menuId"
      @click.prevent="toggleMenu(menu)"
    >
      {{ menu.title }}
    </button>

    <ul
      v-show="isOpen"
      :id="submenuId"
    >
     Hello
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isOpen: false,
    }
  },
  computed: {
    menuId() {
      return this.menu.title.toLowerCase().replace(' ', '-')
    },
    submenuId() {
      return `${this.menu.title.toLowerCase().replace(' ', '-')}-submenu`
    }
  },
  methods: {
    toggleMenu() {
      this.isOpen = !this.isOpen
    }
  }
}
</script>
```
_*Your `BaseMenuItem` component should be looking like this right now.*_

### Adding Aria Attributes
Revisiting our list from above, there's a few aria attributes we want to add to progressively enhance the experience for our assisted users.

- We need the `aria-haspopup` attribute on our parent button. This allows assistive technologies to inform the user that clicking this button will reveal more content.
- We need the `aria-expanded` attribute on our parent button. This allows assistive technologies to inform the user whether or not the menu is currently open.
- We need the `aria-controls` attribute on our parent button. The intention of `aria-controls` is to help screen reader users navigate from a controlling element to a controlled element. It's only available in JAWS screen readers, but some users may expect it.

On our button, let's add the `aria-haspopup="true"` attribute, and we'll also add `:aria-expanded="isOpen.toString()"` as well.

We're adding `aria-expanded` as a dynamic attribute and we're setting it to the value of our `isOpen` data point and converting it to a string. We're doing this because the attribute would be removed altogether when `isOpen` was false, and that's not what we want.

The last aria attribute we'll add to our button is `:aria-controls="submenuId"`. This is so any screen readers will know which menu this button controls.

```javascript
// ~/components/BaseMenuItem.vue

...
<button
 :id="menuId"
 @click.prevent="toggleMenu(menu)"
 aria-haspopup="true"
 :aria-expanded="isOpen.toString()"
 :aria-controls="submenuId"
>
 {{ menu.title }}
</button>
...
```

### Extending Accessibility
There's two more things we need to add to our menu item for it to be complete.

- Pressing the `esc` key should close the currently focused menu
- Opening a menu should focus the first element within it.

There's three steps to being able to close the currently focused menu. We need to (1) write a `closeMenu` method, (2) add a key listener to our `ul` that holds the menu, and (3) and a ref to our button.

So, let's add `ref="menuButtonRef"` to our button, and then let's create a `closeMenu` method that's going to set `this.isOpen = false` and we'll also focus our new button ref with `this.$refs.menuButtonRef.focus()`.

Lastly, let's add a key listener to our `ul` with `@keydown.esc.stop="closeMenu"`.

And that should have your currently focused menu closing! If you want to see something fun, remove the `.stop` and close a menu 😁.

```javascript
// ~/components/BaseMenuItem.vue

<template>
  <div>
    <button
      :id="menuId"
      ref="menuButtonRef"
      @click.prevent="toggleMenu(menu)"
      aria-haspopup="true"
      :aria-expanded="isOpen.toString()"
      :aria-controls="submenuId"
    >
      {{ menu.title }}
    </button>

    <ul
      v-show="isOpen"
      :id="submenuId"
      @keydown.esc.stop="closeMenu"
    >
     Hello
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isOpen: false,
    }
  },
  computed: {
    menuId() {
      return this.menu.title.toLowerCase().replace(' ', '-')
    },
    submenuId() {
      return `${this.menu.title.toLowerCase().replace(' ', '-')}-submenu`
    }
  },
  methods: {
    toggleMenu() {
      this.isOpen = !this.isOpen
    },
    closeMenu() {
      this.isOpen = false
      this.$refs.menuButtonRef?.focus()
  }
}
</script>
```

If it's not working, it may be because we haven't focused any menus when we're opening them. Let's do that now!

### Focusing first elements
By default, an accessible menu should focus the first element within it once it's opened.

To do this, we'll need to query for all clickable items within a menu from its ID and then focus the first one.

So, in our `toggleMenu` method we want to write an `if` statement to check if `isOpen` is true or not. If it is, then that's where we want to focus our first item.

One additional step we need to do, is utilize Vue's [nextTick](https://vuejs.org/v2/api/#Vue-nextTick)- which will allow us to ensure that we're checking the value of `isOpen` _after_ it's been updated.

Inside of our `nextTick` we'll get our submenu by its ID with `const subMenu = document.getElementById(this.submenuId);` and then narrow that down to the first one with `const firstItem = subMenu.querySelector("a, button");`.

After that, we just call `firstItem?.focus()` and now our menu will auto focus its first item when opened!

```javascript
// ~/components/BaseMenuItem.vue

...
methods: {
  toggleMenu() {
    this.isOpen = !this.isOpen

    if(this.isOpen) {
      this.$nextTick(() => {
        const submenu = document.getElementById(this.submenuId)
        const firstItem = submenu.querySelector("a, button")
        firstItem?.focus()
    })
  }
}
...
```

We also want to focus the initial trigger for the menu when it's closed. So we'll write a second `if` statement checking for `!this.isOpen` and add the same `this.$refs.menuButtonRef` that our `closeMenu` method has

```javascript
// ~/components/BaseMenuItem.vue

...
methods: {
  toggleMenu() {
    this.isOpen = !this.isOpen

    if(this.isOpen) {
      this.$nextTick(() => {
        const submenu = document.getElementById(this.submenuId)
        const firstItem = submenu.querySelector("a, button")
        firstItem?.focus()
      })
    }

    if(!this.isOpen) {
      this.$nextTick(() => {
        this.$refs.menuButtonRef?.focus()
      })
    }
  },
}
...
```

Our menu is fully functioning now!! We're not done just _yet_, but all our base functionality is now in place!

We're officially done with our `BaseMenuItem.vue` component.

## Arrow Key Navigation
The last step here is to allow users, assisted and non-assisted, to navigate up and down the menu tree with the arrow keys.

A lot of what we need is already in place, so all we're doing is writing a key event listener on the top level of our menu.

So, jumping back to our `SidebarNav.vue` component, let's add a `@keydown="handleKeyPress"` to our `nav` element.

```javascript
// ~/components/SidebarNav.vue

...
<nav id="Sidebar Navigation" @keydown="handleKeyPress">
  <ul>
    <BaseMenu
      v-for="(menu, index) in navigation"
      :menu="menu"
      :key="index"
      :depth="0"
    />
  </ul>
</nav>
...
```

Next, we'll write our `handleKeyPress` method.

Inside this method, we'll need to do a few things.

1. Get our nav element by ID `const navEl = document.getElementById("Sidebar Navigation");`
2. Get all focusable elements in our nav `const focusableElements = navEl.querySelectorAll(["a", "button"]);`
3. Convert the returned nodelist to an array `const focusableElementsArr = Array.from(focusableElements);`
4. Get the active element on the page `const activeEl = document.activeElement;`
5. Find the index of our active element
`const activeElIndex = focusableElementsArr.findIndex(
        (f) => f.id === activeEl.id
      );`
6. Find the last index of our focusable elements `const lastIdx = focusableElementsArr.length - 1;`

```javascript
// ~/components/SidebarNav.vue

methods: {
  handleKeyPress(e) {
    const navEl = document.getElementById("Sidebar Navigation");

    const focusableElements = navEl.querySelectorAll(["a", "button"]);

    const focusableElementsArr = Array.from(focusableElements);

    const activeEl = document.activeElement;

    const activeElIndex = focusableElementsArr.findIndex(
      (f) => f.id === activeEl.id
    );
    const lastIdx = focusableElementsArr.length - 1;
  },
},
```

Next, we'll write two `if` statements. One for `ArrowUp` and one for `ArrowDown`. If our user is on the first element and presses the up key, our first element will retain focus- but if they hit the down key, it will move them down one element.

And the inverse will happen for the last element.

```javascript
// ~/components/SidebarNav.vue

methods: {
  handleKeyPress(e) {
    const navEl = document.getElementById("Sidebar Navigation");

    const focusableElements = navEl.querySelectorAll(["a", "button"]);

    const focusableElementsArr = Array.from(focusableElements);

    const activeEl = document.activeElement;

    const activeElIndex = focusableElementsArr.findIndex(
      (f) => f.id === activeEl.id
    );
    const lastIdx = focusableElementsArr.length - 1;

    if (e.key === "ArrowUp") {
      activeElIndex <= 0
        ? focusableElementsArr[0].focus()
        : focusableElementsArr[activeElIndex - 1].focus();
    }
    if (e.key === "ArrowDown") {
      activeElIndex >= lastIdx
        ? focusableElementsArr[lastIdx].focus()
        : focusableElementsArr[activeElIndex + 1].focus();
    }
  },
},
```

Now jump over to your browser, open up some menus, and arrow key up and down!

## Summary
This walkthrough was bit long-winded, but- as you saw- there are a lot of moving parts to consider when building a system like this.

The good news? The system will work for an indefinite level of menus, provided the design and screen real-estate allow for it. The only limits aren't tied to the recursive system itself.

Another thing to note, the accessibility of it all _wasn't difficult or complex._ It took very little to take this from a "menu system" to an "accessible menu system", and a lot of base accessibility features are equally as simple to get in place.

Accessibility isn't an enhancement that should be place in the backlog. It's a core fundamental that should be accounted for in scoping, planning, and implementation.

Thank you for making it this far! These a11y write-ups have been huge learning experiences for me and I hope to bring more in 2022.

Disclaimer: This was built with happy path data structures. You may have to write some additional code to get your data structured how you want it. In learning this system, I had to write yet another recursive function that would scaffold a flat chunk of data into the nested levels needed.