Build an off-canvas menu with <dialog> and web components
Tags: html, webdev
Published: 2024-02-07 18:26:38

**Written by [Mark Conroy
](https://blog.logrocket.com/author/markconroy/)✏️**

An off-canvas menu is a common pattern in web design. You see it often on mobile websites where you click on a hamburger menu button and the menu slides in from the side of the screen, usually covering the content that's behind it. 

Building on the overview of off-canvas menus, this article will explore the steps of creating an accessible off-canvas menu using web components and the `<dialog>` element. We will explore how to integrate this menu into your website, ensuring it not only enhances user experience but also adheres to accessibility standards.

## Accessibility considerations for off-canvas menus

While creating an off-canvas menu is a fairly easy task to complete superficially, it can become tricky when you consider accessibility. You must consider things such as:

*   An ARIA attribute to announce that the item is `expanded`
*   A focus trap inside the off-canvas element so users don’t accidentally tab outside to links they can’t see
*   A button to close the off-canvas element (and after that, reset any attributes back to their defaults)
*   The focus state coming back to the item that triggered the opening of the off-canvas element

When it comes to accessibility, the first rule is to use the browser’s native APIs. In this case, we’ll use the [`<dialog>` element](https://blog.logrocket.com/using-the-dialog-element/). 

This will allow us to show the contents of the off-canvas element when we click a button, provide a focus trap for free, and announce whatever needs to be announced to assistive technologies. It also means that we don’t have to maintain or update any of these features ourselves. The next section will offer a short primer on how to create a dialog box.

## HTML for a `<dialog>` element

For the HTML, it’s as simple as adding a `<dialog>` element to the page, adding a button that we can use to open the dialog box, and finally adding a button inside the box that we can use to close it:

```html
<html>
  <head></head>
  <body>
    All your HTML here
    <button class="open-dialog">Open Dialog</dialog>
    <dialog>
      <button class="close-dialog">Close Dialog</dialog>
    </dialog>
  </body>
</html> 
```

_N.B.,_ By default, you can hit the `ESC` key to close the dialog if there is no close button present.

## JavaScript for a `<dialog>` element

The JavaScript is also quite simple. We create variables for the dialog box, the open button, and the close button. Then, we add an event listener to each of the buttons to call the `showModal()` or `close()` methods on the dialog:

```javascript
const dialog = document.querySelector('dialog');
const buttonOpen = document.querySelector('.open-dialog')
const buttonClose = document.querySelector('.close-dialog')

buttonOpen.addEventListener('click', function() {
  dialog.showModal()
});
buttonClose.addEventListener('click', function() {
  dialog.close()
});
```

You can see a demo of this [basic dialog element on CodePen](https://codepen.io/markconroy/pen/VwgRawy).

Now that our `<dialog>` element has been created, let's click the button and see what happens. Right now, the dialog is shown in the center of the screen. This is expected behavior, as when we usually think of a dialog, we think of a modal pop-up feature that shows a larger version of an image. 

However, for our menu, we want to show it animating in from the side of the screen. So let's add some CSS to position it off-screen.

## Adding CSS to position the dialog off-canvas

```css
.dialog-menu,
.dialog-menu[open] {
  position: fixed;
  width: 400px;
  max-width: 80%;
  min-height: 100vh;
  margin: 0;
  margin-left: auto;
  transform: translateX(100%);
  transition: .3s;
}
```

There are a couple of interesting things to note here about the CSS. By default, when visible, a dialog is centered horizontally and vertically just like it would be if you used `display: flex` on the container and `margin: auto` on the item itself. 

To stop it from being centered in the screen, we set `margin: 0` for all sides, and then `margin-left: auto`. This ensures that the dialog is positioned as far to the right of the screen as possible. We then use `position: fixed` and `transform: translateX(100%)` to move the dialog off the screen by the same value as its width (100% of its x/width). 

We need the `position: fixed` here or else we will have a horizontal scroll of the same value as the dialog’s width. (_N__.B.,_ We could use `overflow-x: hidden` on the body element, but that is a bit too drastic and can have unintended consequences). 

The width is set to 400px, but constrained by the max width being 80%. This ensures that when the dialog is visible, it won’t cover our entire screen, so users are not wondering where the content has gone! 

Next, for the height of the item, we set it to a minimum of 100% of the height of the screen, so if there are many menu items in it, we will be able to scroll to see them, but if there are only a few, the dialog will still look good. 

For a nice UX, we set the `transition: 0.3s` to create an animated effect for the dialog coming onto and off the screen. However, it has no effect yet, because dialogs being shown or hidden change state from `display: none` to `display: block` and you can’t transition from these two states. But don’t worry, we have a solution coming for you!

### CSS for when the dialog is on the screen

```css
.dialog-menu[open] {
  display: flex;
  margin: 0;
  margin-left: auto;
  flex-direction: column;
  transform: translateX(0);
  transition: .3s;
} 

.close-dialog {
  margin-left: auto;
}
```

When a dialog box is visible, it automatically gets an attribute of `open`. We’ll use this attribute for our styling when the modal is visible. 

We need to add the `margin: 0` and `margin-left: auto` here again, as it gets reset when the `dialog` is `open`. We set our display to `display: flex` rather than the `display: block` that the browser sets to it. I’m using `flex-direction: column` next, and then setting the `.close-dialog` button to `margin-left: auto` so that it is positioned on the right side of the dialog (you might prefer to use grid or some other mechanism). 

You’ll remember we set our `transform: translateX` to -100% to position it off-screen. Now, to have it on screen, we reset that back to `transform: translateX(0)` and add our transition time back in so it will animate out when closing. Yes, I know, the animation is still not working — we’ll get to that next:

See the CodePen for a [basic off-canvas dialog](https://codepen.io/markconroy/pen/WNPmwKQ).

## Animating an off-canvas dialog

We can’t animate something from `display: none` to `display: block` and vice versa. To get around this, we will take the following steps when we click on the button to open the `dialog`:

1.  Set `<dialog>` to `display: flex`, so now we can animate it
2.  Use a `setTimeout` for a tiny amount of time, and then call the `showModal()` method, which adds the `[open]` attribute. Now, because the dialog’s display property is at `display: flex` before we start the animation, our transition will work
3.  Apply the CSS that is scoped to the `[open]` attribute to animate the dialog into view

```javascript
const dialogMenu = document.querySelector(".dialog-menu");
    const menuToggle = document.querySelector(".menu-toggle");

    menuToggle.addEventListener("click", () => {
      dialogMenu.style.display = "flex";
      setTimeout(() => {
        dialogMenu.showModal();
      }, 100);
    });
```

When we want to close the off-canvas menu, we take a similar approach. 

First, we add a class of `.dialog-menu--closing` to the dialog. We can then use this class to run the animation of sending the dialog back offscreen. We can just add this class to the same CSS we have for the default `.dialog-menu` above. 

Next, we use another `setTimeout` with a very short amount of time, and use that to:

*   Close the dialog via the `.close()` menu
*   Set the dialog back to `display: none`, and
*   Finally, remove the `dialog-menu--closing` class that we added because the animation has now ended

```javascript
const dialogMenu = document.querySelector(".dialog-menu");
const dialogMenuCloseButton = document.querySelector(".dialog__menu-close");
const menuToggle = document.querySelector(".menu-toggle");

dialogMenuCloseButton.addEventListener('click', () => {
  dialogMenu.classList.add("dialog-menu--closing");
  setTimeout(() => {
    dialogMenu.close();
    dialogMenu.style.display = "none";
    dialogMenu.classList.remove("dialog-menu--closing");
  }, 100);
}
```

```css
.dialog-menu,
.dialog-menu[open].dialog-menu--closing {
  /* Same CSS as for .dialog-menu above */
}
```

We now have a nice-looking off-canvas dialog:

See the CodePen for an [animated off-canvas dialog](https://codepen.io/markconroy/pen/bGzJQrE).

## Creating a menu as a web component

In this section, we are going to use a web component to create a menu because web components are ideal for creating reusable HTML elements. Importantly, it allows for a single update to the menu to automatically circulate and apply throughout the entire site so that we only have to update our menu once. We could do the same for a reusable header and footer. 

Our web component is a simple component that creates a `nav` element and puts an unordered list inside it with the links for our menu. We attach this to the shadow DOM, and add some CSS:

```css
class Menu extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        ul {
          list-style: none;
          margin: 0;
          padding: 0;
        }
        li {
          margin-top: var(--spacing);
        }
        li + li {
          border-top: 1px solid white;
        }
        a {
          display: block;
          padding-block: 1rem; 
          text-decoration: none;
          color: white;
        }
      </style>
      <nav>
        <ul>
          <li><a href="https://example.com/about">About Us</a></li>
          <li><a href="https://example.com/services">Our Services</a></li>
          <li><a href="https://example.com/testimonials">Testimonials</a></li>
          <li><a href="https://example.com/location">Directions</a></li>
          <li><a href="https://example.com/contact">Contact Us</a></li>
        </ul>
      </nav>
    `;
  }
}

customElements.define("custom-menu", Menu); 
```

Now that we have our web component created and our dialog created and working, we just need to place our web component inside our `<dialog>` element:

```html
<dialog class="dialog-menu">
  <button class="close-dialog">Close Dialog</button>
  <custom-menu></custom-menu>
</dialog>
```

Here’s the full demo:

See the CodePen for an [animated off-canvas menu created with a web component](https://codepen.io/markconroy/pen/mdvgQay).

## What have we learned?

In this post, we learned how to use native HTML tags and web APIs to ensure future-proofing in our website. We also learned that the `<dialog>` element is not just for creating modals, but that we can animate it from anywhere and to anywhere on our site if we’re creative with our approach. Finally, we covered creating web components to be able to create a component once and use it everywhere across our webpage.

---

##Get set up with LogRocket's modern error tracking in minutes:

1. Visit https://logrocket.com/signup/ to get an app ID.
2. Install LogRocket via NPM or script tag. `LogRocket.init()` must be called client-side, not server-side.

NPM:
```bash
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script Tag:
```javascript
Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
```

3.(Optional) Install plugins for deeper integrations with your stack:
*   Redux middleware
*   ngrx middleware
*   Vuex plugin

[Get started now](https://lp.logrocket.com/blg/signup)