Accessible Dropdown Menu Code with Example

Accessible Dropdown Menu Code with Example
Project: Accessible Dropdown Menus
Author: Kevin Colonjard
Edit Online: View on CodePen
License: MIT

This code project implements an accessible dropdown menu using HTML, CSS, and JavaScript. The dropdown menu consists of multiple options that can be expanded and collapsed. Each option is represented by an icon and a corresponding label. The menu supports keyboard navigation and focus management for improved accessibility.

The code includes CSS styles for the dropdown layout and animations. It provides three variations of the dropdown menu: Slide, Scale, and Zoom, each with its own unique animation effect. Moreover, the dropdown menu is useful for creating interactive and user-friendly navigation menus or selection interfaces on web pages.

How to Create an Accessible Dropdown Menu

1. First, load the Google Fonts and Font Awesome by adding the following CDN links to the head tag of your webpage.

<link rel='stylesheet' href='https://fonts.googleapis.com/css2?family=Heebo:wght@400;500&amp;display=swap'>
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css'>

2. After that, create the HTML structure for the dropdown menu as follows.

<div class="dropdown" data-open="false">
  <span class="dropdown-label">Slide</span>
  <button class="dropdown-button" aria-haspopup="true" aria-expanded="false">
    <span>Options</span>
    <i class="dropdown-button-arrow fas fa-angle-down"></i>
  </button>

  <ul class="dropdown-menu slide" aria-role="menu" aria-hidden="true">
    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-pen"></i>
      <span>Edit</span>
    </li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-clone"></i>
      <span>Duplicate</span>
    </li>

    <li class="dropdown-menu-separator" role="separator"></li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-archive"></i>
      <span>Archive</span>
    </li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-arrows-alt"></i>
      <span>Move</span>
    </li>

    <li class="dropdown-menu-separator" role="separator"></li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-trash"></i>
      <span>Delete</span>
    </li>
  </ul>
</div>

<div class="dropdown" data-open="false">
  <span class="dropdown-label">Scale</span>
  <button class="dropdown-button" aria-haspopup="true" aria-expanded="false">
    <span>Options</span>
    <i class="dropdown-button-arrow fas fa-angle-down"></i>
  </button>

  <ul class="dropdown-menu scale" aria-role="menu" aria-hidden="true">
    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-pen"></i>
      <span>Edit</span>
    </li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-clone"></i>
      <span>Duplicate</span>
    </li>

    <li class="dropdown-menu-separator" role="separator"></li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-archive"></i>
      <span>Archive</span>
    </li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-arrows-alt"></i>
      <span>Move</span>
    </li>

    <li class="dropdown-menu-separator" role="separator"></li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-trash"></i>
      <span>Delete</span>
    </li>
  </ul>
</div>

<div class="dropdown" data-open="false">
  <span class="dropdown-label">Zoom</span>
  <button class="dropdown-button" aria-haspopup="true" aria-expanded="false">
    <span>Options</span>
    <i class="dropdown-button-arrow fas fa-angle-down"></i>
  </button>

  <ul class="dropdown-menu zoom" aria-role="menu" aria-hidden="true">
    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-pen"></i>
      <span>Edit</span>
    </li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-clone"></i>
      <span>Duplicate</span>
    </li>

    <li class="dropdown-menu-separator" role="separator"></li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-archive"></i>
      <span>Archive</span>
    </li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-arrows-alt"></i>
      <span>Move</span>
    </li>

    <li class="dropdown-menu-separator" role="separator"></li>

    <li class="dropdown-menu-item" tabindex="-1" aria-role="menuitem">
      <i class="dropdown-item-icon fas fa-trash"></i>
      <span>Delete</span>
    </li>
  </ul>
</div>

You just need to modify the HTML structure within each <ul class="dropdown-menu"> element to define your own options. Each option is represented by an <li class="dropdown-menu-item"> element. Within this element, you can customize the icon by changing the class of the <i> element, and the label text by modifying the content within the <span> element.

3. Now, use the following CSS code to style the dropdowns:

:root {
  --ff-body: "Heebo", sans-serif;
  --fs-sm: 0.875rem;
  --fs-md: 0.9375rem;
  --clr-light: 0 0% 100%;
  --clr-dark: 0 0% 10%;
  --clr-primary: 217 91% 60%;
}
.cd__main{
display: block ! important;
}
*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

*:focus {
  outline: none;
}

button {
  border: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

ul {
  list-style: none;
}

body {
  display: grid;
  grid-template-columns: auto;
  gap: 4em;
  justify-content: center;
  align-content: center;
  min-height: 100vh;
  padding: 2em;
  background-color: hsl(var(--clr-primary)/10%);
  color: hsl(var(--clr-dark));
  font-family: var(--ff-body);
  font-size: var(--fs-md);
}
@media (min-width: 60em) {
  body {
    grid-template-columns: repeat(3, auto);
  }
}

.dropdown {
  position: relative;
  width: 14em;
}

.dropdown-label {
  display: inline-block;
  margin-bottom: 0.5em;
  font-size: var(--fs-sm);
  font-weight: 500;
}

.dropdown-button {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
  padding: 1em 1.5em;
  border-radius: 0.5em;
  background-color: hsl(var(--clr-primary));
  color: hsl(var(--clr-light));
  font-weight: 500;
}
.dropdown-button::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0px 0px 0px 3px hsl(var(--clr-primary)/40%);
  opacity: 0;
  transition: opacity 0.2s;
}
.dropdown-button:hover::after, .dropdown-button:focus::after, .dropdown:focus-within > .dropdown-button::after {
  opacity: 1;
}

.dropdown-button-arrow {
  margin-left: auto;
  transition: transform 0.2s;
}
.dropdown[data-open=true] .dropdown-button-arrow {
  transform: rotate(180deg);
}

.dropdown-menu {
  position: absolute;
  right: 0;
  z-index: 1000;
  width: 100%;
  margin-top: 0.5em;
  padding: 0.35em 0;
  border-radius: 0.5em;
  background-color: hsl(var(--clr-light));
  box-shadow: 0px 6px 20px 0px hsl(var(--clr-dark)/10%);
  visibility: hidden;
  opacity: 0;
  transition: all 0.3s;
}
.dropdown[data-open=true] .dropdown-menu {
  visibility: visible;
  opacity: 1;
}

.dropdown-menu-separator {
  margin: 0.35em 0;
  border-bottom: 1px solid hsl(var(--clr-dark)/10%);
}

.dropdown-menu-item {
  display: flex;
  align-items: center;
  margin: 0 0.35em;
  padding: 0.65em 1.15em;
  border-radius: 0.25em;
  cursor: pointer;
  transition: background-color 0.2s;
}
.dropdown-menu-item:focus {
  background-color: hsl(var(--clr-primary)/10%);
}

.dropdown-item-icon {
  margin-right: 1.25em;
  color: hsl(var(--clr-primary)/75%);
}

.dropdown-menu.slide {
  transform: translateY(1rem);
}
.dropdown[data-open=true] .dropdown-menu.slide {
  transform: translateY(0);
}

.dropdown-menu.scale {
  transform: scaleY(0);
  transform-origin: top;
}
.dropdown[data-open=true] .dropdown-menu.scale {
  transform: scaleY(1);
}

.dropdown-menu.zoom {
  transform: translate(-2em, -2.5em) scale(0);
  transform-origin: top right;
}
.dropdown[data-open=true] .dropdown-menu.zoom {
  transform: translate(0) scale(1);
}

4. Finally, add the following JavaScript code just before closing the body element to activate accessible dropdowns.

class Dropdown {
  constructor(container) {
    this.isOpen = false;
    this.activeIndex = undefined;

    this.container = container;
    this.button = container.querySelector(".dropdown-button");
    this.menu = container.querySelector(".dropdown-menu");
    this.items = container.querySelectorAll(".dropdown-menu-item");
  }

  initEvents() {
    this.button.addEventListener("click", this.toggle.bind(this));
    document.addEventListener("click", this.onClickOutside.bind(this));
    document.addEventListener("keydown", this.onKeyEvent.bind(this));
  }

  toggle() {
    this.isOpen = !this.isOpen;
    this.button.setAttribute("aria-expanded", this.isOpen);
    this.menu.setAttribute("aria-hidden", !this.isOpen);
    this.container.dataset.open = this.isOpen;
  }

  onClickOutside(e) {
    if (!this.isOpen) return;

    let targetElement = e.target;

    do {
      if (targetElement == this.container) return;

      targetElement = targetElement.parentNode;
    } while (targetElement);

    this.toggle();
  }

  onKeyEvent(e) {
    if (!this.isOpen) return;

    if (e.key === "Tab") {
      this.toggle();
    }

    if (e.key === "Escape") {
      this.toggle();
      this.button.focus();
    }

    if (e.key === "ArrowDown") {
      this.activeIndex =
        this.activeIndex < this.items.length - 1 ? this.activeIndex + 1 : 0;
      this.items[this.activeIndex].focus();
    }

    if (e.key === "ArrowUp") {
      this.activeIndex =
        this.activeIndex > 0 ? this.activeIndex - 1 : this.items.length - 1;
      this.items[this.activeIndex].focus();
    }
  }
}

const dropdowns = document.querySelectorAll(".dropdown");
dropdowns.forEach((dropdown) => new Dropdown(dropdown).initEvents());

That’s it! hopefully, you have successfully created an accessible dropdown menu. If you have any questions or suggestions, feel free to comment below.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *