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.
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:
expanded
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.
<dialog>
elementFor 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>
N.B., By default, you can hit the ESC
key to close the dialog if there is no close button present.
<dialog>
elementThe 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() });
You can see a demo of this basic dialog element on CodePen:
See the Pen
Basic Dialog Element by Mark Conroy (@markconroy)
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.
.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!
.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 Pen
Basic Off-canvas Dialog by Mark Conroy (@markconroy)
on CodePen.
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
:
<dialog>
to display: flex
, so now we can animate itsetTimeout
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[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); });
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()
menudisplay: none
, anddialog-menu--closing
class that we added because the animation has now endedconst 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); }
.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 Pen
Animated Off Canvas Dialog by Mark Conroy (@markconroy)
on CodePen.
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);
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>
Here’s the full demo:
See the Pen
Animated off canvas menu created with web component by Mark Conroy (@markconroy)
on CodePen.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
One Reply to "Build an off-canvas menu with <dialog> and web components"
Because this method of animating the dialog’s closing process relies on clicking a specific button, it doesn’t work when the dialog is closed by other means, such as pressing the Esc key.
Unfortunately, there doesn’t seem to be a `beforeclose` event for dialogs, only `close`, which seems to trigger AFTER the dialog has closed.