DEV Community

Cover image for Build an off-canvas menu with <dialog> and web components
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Build an off-canvas menu with <dialog> and web components

Written by Mark Conroy
✏️

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.

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>
  <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> 
Enter fullscreen mode Exit fullscreen mode

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:

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()
});
Enter fullscreen mode Exit fullscreen mode

You can see a demo of this basic dialog element on CodePen.

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

.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;
}
Enter fullscreen mode Exit fullscreen mode

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

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

.close-dialog {
  margin-left: auto;
}
Enter fullscreen mode Exit fullscreen mode

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.

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
const dialogMenu = document.querySelector(".dialog-menu");
    const menuToggle = document.querySelector(".menu-toggle");

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

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
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);
}
Enter fullscreen mode Exit fullscreen mode
.dialog-menu,
.dialog-menu[open].dialog-menu--closing {
  /* Same CSS as for .dialog-menu above */
}
Enter fullscreen mode Exit fullscreen mode

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

See the CodePen for an animated off-canvas dialog.

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:

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); 
Enter fullscreen mode Exit fullscreen mode

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:

<dialog class="dialog-menu">
  <button class="close-dialog">Close Dialog</button>
  <custom-menu></custom-menu>
</dialog>
Enter fullscreen mode Exit fullscreen mode

Here’s the full demo:

See the CodePen for an animated off-canvas menu created with a web component.

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:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

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

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (2)

Collapse
 
edydeyemi profile image
Edydeyemi

Thanks for this

Collapse
 
jangelodev profile image
João Angelo

Hi Megan Lee,
Excellent content, very useful.
Thanks for sharing.