Dropdown
A dropdown is a menu that appears when a user clicks on a button or link. It is a common design pattern used in websites and applications to provide additional options or functionality to the user.
Design Hints
Accessiblity Hints
Dropdown Definition
The dropdown consists of two main parts:
- The dropdown button: button that triggers the dropdown menu to appear.
- The dropdown menu: A menu that appears when the dropdown button is clicked. The list of items are usually actionable, such as links or buttons. It can also contain submenus but this implementation is primarily focused on the dropdown menu without submenus.
Simple Dropdown
A simple dropdown with a list of options.
<div class="relative inline-flex text-left"> <button type="button" class="group peer inline-flex w-full items-center justify-center gap-x-1.5 rounded-md bg-white px-4 py-3 text-sm font-semibold text-gray-900 shadow-sm outline-offset-4 outline-black ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-4 lg:py-2.5 lg:text-base dark:bg-zinc-900 dark:text-gray-100 dark:shadow-none dark:outline-white dark:ring-zinc-800 dark:hover:bg-zinc-700" id="menu-button-ht" aria-expanded="false" aria-haspopup="true" > Options <svg class="-mr-1 h-5 w-5 text-gray-400 group-aria-expanded:-rotate-180 motion-safe:transition" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" ></path> </svg> </button>
<ul class="absolute right-0 top-10 z-10 mt-2 hidden min-w-max origin-top-right rounded-md bg-white text-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none peer-aria-expanded:block dark:bg-zinc-900 dark:text-gray-200" id="dropdown-menu-ht" role="menu" aria-orientation="vertical" aria-labelledby="menu-button-ht" tabindex="-1" > <li tabindex="-1" role="menuitem" id="nht-menu-item-0"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Account settings</a > </li> <li tabindex="-1" role="menuitem" id="nht-menu-item-1"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Support</a > </li> <li tabindex="-1" role="menuitem" id="nht-menu-item-2"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >License</a > </li> <li tabindex="-1" role="menuitem" id="nht-menu-item-3"> <form method="POST" id="nht-menu-item-form-3" action="#"> <button type="submit" tabindex="-1" class="my-1 block w-full px-4 py-3 text-left text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" > Sign out </button> </form> </li> </ul></div>
<script> function menuInitilization() { const button = document.getElementById("menu-button-ht"); const dropdown = document.getElementById("dropdown-menu-ht"); const dropdownItems = dropdown?.querySelectorAll("li"); button?.addEventListener("click", () => { event.stopPropagation(); if (button.getAttribute("aria-expanded") === "true") { closeMenu(); return; } button.setAttribute("aria-expanded", "true"); if (dropdown && dropdownItems) { document.documentElement.addEventListener("click", closeMenu); dropdownItems[0].focus();
let tabFocus = 0; /** * Handles keyboard navigation and interactions for a dropdown menu. * * When the dropdown is focused, the following keyboard interactions are supported: * - Arrow Up/Down: Moves the focus to the previous/next item in the dropdown * - Escape: Closes the dropdown * - Enter: Clicks the currently focused dropdown item * - Tab: Moves focus back to the button */ dropdown.addEventListener("keydown", (keyboardEvent) => { const e = keyboardEvent;
if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); dropdownItems[tabFocus].setAttribute("tabindex", "-1"); if (e.key === "ArrowDown") { tabFocus++; // If we're at the end, go to the start if (tabFocus >= dropdownItems.length) { tabFocus = 0; } // Move left } else if (e.key === "ArrowUp") { tabFocus--; // If we're at the start, move to the end if (tabFocus < 0) { tabFocus = dropdownItems.length - 1; } } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Escape") { closeMenu(); return; } else if (e.key === "Enter") { dropdownItems[tabFocus].children[0]?.click(); closeMenu(); return; } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault();
if (e.key === "ArrowRight") { tabFocus = dropdownItems.length - 1; } else if (e.key === "ArrowLeft") { tabFocus = 0; } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Tab" || e.key === "Shift+Tab") { e.preventDefault(); button.focus(); } }); } }); } function closeMenu() { const button = document.getElementById("menu-button-ht");
button?.setAttribute("aria-expanded", "false"); button?.focus(); document.documentElement.removeEventListener("click", closeMenu); } menuInitilization();</script>
---
---
<div class="relative inline-flex text-left"> <button type="button" class="group peer inline-flex w-full items-center justify-center gap-x-1.5 rounded-md bg-white px-4 py-3 text-sm font-semibold text-gray-900 shadow-sm outline-offset-4 outline-black ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-4 lg:py-2.5 lg:text-base dark:bg-zinc-900 dark:text-gray-100 dark:shadow-none dark:outline-white dark:ring-zinc-800 dark:hover:bg-zinc-700" id="menu-button" aria-expanded="false" aria-haspopup="true" > Options <svg class="-mr-1 h-5 w-5 text-gray-400 group-aria-expanded:-rotate-180 motion-safe:transition" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"></path> </svg> </button>
<ul class="absolute right-0 top-10 z-10 mt-2 hidden min-w-max origin-top-right rounded-md bg-white text-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none peer-aria-expanded:block dark:bg-zinc-900 dark:text-gray-200" id="dropdown-menu" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1" > <li tabindex="-1" role="menuitem" id="n-menu-item-0"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Account settings</a > </li> <li tabindex="-1" role="menuitem" id="n-menu-item-1"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Support</a > </li> <li tabindex="-1" role="menuitem" id="n-menu-item-2"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >License</a > </li> <li tabindex="-1" role="menuitem" id="n-menu-item-3"> <form method="POST" id="n-menu-item-form-3" action="#"> <button type="submit" tabindex="-1" class="my-1 block w-full px-4 py-3 text-left text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" > Sign out </button> </form> </li> </ul></div>
<script> /** * Initializes the dropdown menu functionality, including event listeners for button clicks, keyboard navigation, and closing the menu. * * This function sets up the event listeners and behavior for a dropdown menu, including: * - Toggling the "aria-expanded" attribute on the menu button when the menu is opened or closed * - Focusing the first dropdown item when the menu is opened * - Handling keyboard navigation (arrow keys, Escape, Enter) to move focus between dropdown items * - Closing the menu when the user clicks outside the menu or presses Escape * * This function assumes the following HTML structure: * - A button element with the ID "menu-button" * - A UL element with the ID "dropdown-menu" containing the dropdown items as LI elements */ function menuInitilization() { const button = document.getElementById("menu-button") as HTMLButtonElement; const dropdown = document.getElementById( "dropdown-menu", ) as HTMLUListElement; const dropdownItems = dropdown?.querySelectorAll( "li", ) as NodeListOf<HTMLLIElement>; button?.addEventListener("click", (event) => { event.stopPropagation(); if (button.getAttribute("aria-expanded") === "true") { closeMenu(); return; } button.setAttribute("aria-expanded", "true");
if (dropdown && dropdownItems) { document.documentElement.addEventListener("click", closeMenu); dropdownItems[0].focus();
let tabFocus = 0; /** * Handles keyboard navigation and interactions for a dropdown menu. * * When the dropdown is focused, the following keyboard interactions are supported: * - Arrow Up/Down: Moves the focus to the previous/next item in the dropdown * - Escape: Closes the dropdown * - Enter: Clicks the currently focused dropdown item * - Tab: Moves focus back to the button */ dropdown.addEventListener("keydown", (keyboardEvent) => { const e = keyboardEvent as KeyboardEvent;
if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); if (e.key === "ArrowDown") { tabFocus++; // If we're at the end, go to the start if (tabFocus >= dropdownItems.length) { tabFocus = 0; } // Move left } else if (e.key === "ArrowUp") { tabFocus--; // If we're at the start, move to the end if (tabFocus < 0) { tabFocus = dropdownItems.length - 1; } } tabFocus = tabFocus % dropdownItems.length; (dropdownItems[tabFocus] as HTMLLIElement).focus(); } else if (e.key === "Escape") { closeMenu(); return; } else if (e.key === "Enter") { (dropdownItems[tabFocus].children[0] as HTMLElement)?.click(); closeMenu(); return; } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault();
if (e.key === "ArrowRight") { tabFocus = dropdownItems.length - 1; } else if (e.key === "ArrowLeft") { tabFocus = 0; } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Tab" || e.key === "Shift+Tab") { e.preventDefault(); button.focus(); } }); } }); }
/** * Closes the menu by setting the `aria-expanded` attribute of the menu button to `false` and removing the click event listener from the document. */ function closeMenu() { const button = document.getElementById("menu-button") as HTMLButtonElement;
button?.setAttribute("aria-expanded", "false"); button?.focus(); document.documentElement.removeEventListener("click", closeMenu); }
document.addEventListener("astro:page-load", menuInitilization);</script>
Alternate Dropdown
A different style for a simple dropdown.
<div class="relative inline-flex text-left"> <button type="button" class="group peer inline-flex w-full items-center justify-center gap-x-1.5 rounded-md bg-indigo-700 px-4 py-3 text-sm font-semibold text-white shadow-sm outline-offset-4 outline-black ring-1 ring-inset ring-indigo-900 hover:bg-indigo-500 focus:outline-4 lg:py-3 lg:text-base dark:bg-indigo-950 dark:shadow-none dark:outline-white dark:hover:bg-indigo-700" id="menu-button-althtm" aria-expanded="false" aria-haspopup="true" > Options <svg class="-mr-1 h-5 w-5 text-gray-400 group-aria-expanded:-rotate-180 motion-safe:transition" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" ></path> </svg> </button>
<ul class="absolute right-0 top-11 z-10 mt-2 hidden min-w-max origin-top-right rounded-md bg-indigo-50 text-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none peer-aria-expanded:block dark:bg-indigo-950 dark:text-gray-200" id="dropdown-menu-althtm" role="menu" aria-orientation="vertical" aria-labelledby="menu-button-althtm" tabindex="-1" > <li tabindex="-1" role="menuitem" id="althtm-menu-item-0"> <a href="#" tabindex="-1" class="block border-b border-gray-300 px-4 py-2.5 text-sm hover:bg-indigo-200 dark:border-zinc-700 dark:hover:bg-indigo-700" >Account settings</a > </li> <li tabindex="-1" role="menuitem" id="althtm-menu-item-1"> <a href="#" tabindex="-1" class="block border-b border-gray-300 px-4 py-2.5 text-sm hover:bg-indigo-200 dark:border-zinc-700 dark:hover:bg-indigo-700" >Support</a > </li> <li tabindex="-1" role="menuitem" id="althtm-menu-item-2"> <a href="#" tabindex="-1" class="block border-b border-gray-300 px-4 py-2.5 text-sm hover:bg-indigo-200 dark:border-zinc-700 dark:hover:bg-indigo-700" >License</a > </li> <li tabindex="-1" role="menuitem" id="althtm-menu-item-3"> <form method="POST" id="althtm-menu-item-form-3" action="#"> <button type="submit" tabindex="-1" class="block w-full border-b border-gray-300 px-4 py-2.5 text-left text-sm hover:bg-indigo-200 dark:border-zinc-700 dark:hover:bg-indigo-700" > Sign out </button> </form> </li> </ul></div>
<script> function menuInitilizationalt() { const button = document.getElementById("menu-button-althtm"); const dropdown = document.getElementById("dropdown-menu-althtm"); const dropdownItems = dropdown?.querySelectorAll("li"); button?.addEventListener("click", () => { event.stopPropagation(); if (button.getAttribute("aria-expanded") === "true") { closeMenu(); return; } button.setAttribute("aria-expanded", "true"); if (dropdown && dropdownItems) { document.documentElement.addEventListener("click", closeMenu); dropdownItems[0].focus();
let tabFocus = 0; /** * Handles keyboard navigation and interactions for a dropdown menu. * * When the dropdown is focused, the following keyboard interactions are supported: * - Arrow Up/Down: Moves the focus to the previous/next item in the dropdown * - Escape: Closes the dropdown * - Enter: Clicks the currently focused dropdown item * - Tab: Moves focus back to the button */ dropdown.addEventListener("keydown", (keyboardEvent) => { const e = keyboardEvent;
if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); dropdownItems[tabFocus].setAttribute("tabindex", "-1"); if (e.key === "ArrowDown") { tabFocus++; // If we're at the end, go to the start if (tabFocus >= dropdownItems.length) { tabFocus = 0; } // Move left } else if (e.key === "ArrowUp") { tabFocus--; // If we're at the start, move to the end if (tabFocus < 0) { tabFocus = dropdownItems.length - 1; } } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Escape") { closeMenu(); return; } else if (e.key === "Enter") { dropdownItems[tabFocus].children[0]?.click(); closeMenu(); return; } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault();
if (e.key === "ArrowRight") { tabFocus = dropdownItems.length - 1; } else if (e.key === "ArrowLeft") { tabFocus = 0; } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Tab" || e.key === "Shift+Tab") { e.preventDefault(); button.focus(); } }); } }); } function closeMenu(event) { const button = document.getElementById("menu-button-althtm"); button?.setAttribute("aria-expanded", "false"); button?.focus(); document.documentElement.removeEventListener("click", closeMenu); } menuInitilizationalt();</script>
---
---
<div class="relative inline-flex text-left"> <button type="button" class="group peer inline-flex w-full items-center justify-center gap-x-1.5 rounded-md bg-indigo-700 px-4 py-3 text-sm font-semibold text-white shadow-sm outline-offset-4 outline-black ring-1 ring-inset ring-indigo-900 hover:bg-indigo-500 focus:outline-4 lg:py-3 lg:text-base dark:bg-indigo-950 dark:shadow-none dark:outline-white dark:hover:bg-indigo-700" id="menu-button-alt" aria-expanded="false" aria-haspopup="true" > Options <svg class="-mr-1 h-5 w-5 text-gray-400 group-aria-expanded:-rotate-180 motion-safe:transition" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"></path> </svg> </button>
<ul class="absolute right-0 top-11 z-10 mt-2 hidden min-w-max origin-top-right rounded-md bg-indigo-50 text-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none peer-aria-expanded:block dark:bg-indigo-950 dark:text-gray-200" id="dropdown-menu-alt" role="menu" aria-orientation="vertical" aria-labelledby="menu-button-alt" tabindex="-1" > <li tabindex="-1" role="menuitem" id="alt-menu-item-0"> <a href="#" tabindex="-1" class="my-1 block border-b border-gray-300 px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-indigo-200 focus:outline-4 dark:border-zinc-700 dark:outline-white dark:hover:bg-indigo-700" >Account settings</a > </li> <li tabindex="-1" role="menuitem" id="alt-menu-item-1"> <a href="#" tabindex="-1" class="my-1 block border-b border-gray-300 px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-indigo-200 focus:outline-4 dark:border-zinc-700 dark:outline-white dark:hover:bg-indigo-700" >Support</a > </li> <li tabindex="-1" role="menuitem" id="alt-menu-item-2"> <a href="#" tabindex="-1" class="my-1 block border-b border-gray-300 px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-indigo-200 focus:outline-4 dark:border-zinc-700 dark:outline-white dark:hover:bg-indigo-700" >License</a > </li> <li tabindex="-1" role="menuitem" id="alt-menu-item-3"> <form method="POST" id="alt-menu-item-form-3" action="#"> <button type="submit" tabindex="-1" class="my-1 block w-full px-4 py-3 text-left text-sm outline-offset-4 outline-black hover:bg-indigo-200 focus:outline-4 dark:outline-white dark:hover:bg-indigo-700" >Sign out</button > </form> </li> </ul></div>
<script> function menuInitilizationAlt() { const button = document.getElementById( "menu-button-alt", ) as HTMLButtonElement; const dropdown = document.getElementById( "dropdown-menu-alt", ) as HTMLUListElement; const dropdownItems = dropdown?.querySelectorAll( "li", ) as NodeListOf<HTMLLIElement>; button?.addEventListener("click", (event) => { event.stopPropagation(); if (button.getAttribute("aria-expanded") === "true") { closeMenu(); return; } button.setAttribute("aria-expanded", "true");
if (dropdown && dropdownItems) { document.documentElement.addEventListener("click", closeMenu); dropdownItems[0].focus();
let tabFocus = 0; /** * Handles keyboard navigation and interactions for a dropdown menu. * * When the dropdown is focused, the following keyboard interactions are supported: * - Arrow Up/Down: Moves the focus to the previous/next item in the dropdown * - Escape: Closes the dropdown * - Enter: Clicks the currently focused dropdown item * - Tab: Moves focus back to the button */ dropdown.addEventListener("keydown", (keyboardEvent) => { const e = keyboardEvent as KeyboardEvent;
if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); if (e.key === "ArrowDown") { tabFocus++; // If we're at the end, go to the start if (tabFocus >= dropdownItems.length) { tabFocus = 0; } // Move left } else if (e.key === "ArrowUp") { tabFocus--; // If we're at the start, move to the end if (tabFocus < 0) { tabFocus = dropdownItems.length - 1; } } tabFocus = tabFocus % dropdownItems.length; (dropdownItems[tabFocus] as HTMLLIElement).focus(); } else if (e.key === "Escape") { closeMenu(); return; } else if (e.key === "Enter") { (dropdownItems[tabFocus].children[0] as HTMLElement)?.click(); closeMenu(); return; } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault();
if (e.key === "ArrowRight") { tabFocus = dropdownItems.length - 1; } else if (e.key === "ArrowLeft") { tabFocus = 0; } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Tab" || e.key === "Shift+Tab") { e.preventDefault(); button.focus(); } }); } }); } /** * Closes the menu by setting the `aria-expanded` attribute of the menu button to `false` and removing the click event listener from the document. */ function closeMenu() { const button = document.getElementById( "menu-button-alt", ) as HTMLButtonElement;
button?.setAttribute("aria-expanded", "false"); button?.focus(); document.documentElement.removeEventListener("click", closeMenu); }
document.addEventListener("astro:page-load", menuInitilizationAlt);</script>
Animated Dropdown
The simple dropdown with animations.
---
---
<div class="group relative inline-flex text-left"> <button type="button" class="group/button peer inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-4 py-3 text-sm font-semibold text-gray-900 shadow-sm outline-offset-4 outline-black ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-4 lg:py-2.5 lg:text-base dark:bg-zinc-900 dark:text-gray-100 dark:shadow-none dark:outline-white dark:ring-zinc-800 dark:hover:bg-zinc-700" id="menu-button-an" aria-expanded="false" aria-haspopup="true" > Options <svg class="-mr-1 h-5 w-5 text-gray-400 group-aria-expanded/button:-rotate-180 motion-safe:transition motion-safe:duration-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"></path> </svg> </button>
<ul aria-orientation="vertical" aria-labelledby="menu-button-an" role="menu" class="absolute right-0 top-11 z-10 mt-2 max-h-0 min-w-max origin-top-right overflow-hidden rounded-md bg-white text-gray-800 opacity-0 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none peer-aria-expanded:max-h-96 peer-aria-expanded:opacity-100 motion-safe:transition-all motion-safe:duration-500 motion-safe:ease-in-out dark:bg-zinc-900 dark:text-gray-200" id="dropdown-menu-an" aria-hidden="true" tabindex="-1" > <li tabindex="-1" role="menuitem" id="an-menu-item-0"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Account settings</a > </li> <li tabindex="-1" role="menuitem" id="an-menu-item-1"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Support</a > </li> <li tabindex="-1" role="menuitem" id="an-menu-item-2"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >License</a > </li> <li tabindex="-1" role="menuitem" id="an-menu-item-3"> <form method="POST" id="an-menu-item-form-3" action="#"> <button type="submit" tabindex="-1" class="my-1 block w-full px-4 py-3 text-start text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Sign out</button > </form> </li> </ul></div>
<script> /** * Initializes the menu functionality for a dropdown menu. * This function sets up event listeners for the dropdown button and menu items, * allowing the user to open and navigate the dropdown menu using the keyboard. * The function also handles closing the menu when the user clicks outside of it. */ function menuInitilization() { const button = document.getElementById( "menu-button-an", ) as HTMLButtonElement; const dropdown = document.getElementById( "dropdown-menu-an", ) as HTMLUListElement; const dropdownItems = dropdown?.querySelectorAll( "li", ) as NodeListOf<HTMLLIElement>; button?.addEventListener("click", (event) => { event.stopPropagation(); if (button.getAttribute("aria-expanded") === "true") { closeMenu(); return; } button.setAttribute("aria-expanded", "true"); if (dropdown) { dropdown.setAttribute("aria-hidden", "false");
/** * Handles keyboard navigation and interactions for a dropdown menu. * * When the dropdown is focused, the following keyboard interactions are supported: * - Arrow Up/Down: Moves the focus to the previous/next item in the dropdown * - Escape: Closes the dropdown * - Enter: Clicks the currently focused dropdown item * - Tab: Moves focus back to the button */ if (dropdownItems) { document.documentElement.addEventListener("click", closeMenu); setTimeout(() => { dropdownItems[0].focus(); }, 200); // to prevent the focus setting from causing a stutter in the animation let tabFocus = 0;
dropdown.addEventListener("keydown", (keyboardEvent) => { const e = keyboardEvent as KeyboardEvent;
if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); dropdownItems[tabFocus].setAttribute("tabindex", "-1"); if (e.key === "ArrowDown") { tabFocus++; // If we're at the end, go to the start if (tabFocus >= dropdownItems.length) { tabFocus = 0; } // Move left } else if (e.key === "ArrowUp") { tabFocus--; // If we're at the start, move to the end if (tabFocus < 0) { tabFocus = dropdownItems.length - 1; } } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Escape") { closeMenu(); return; } else if (e.key === "Enter") { (dropdownItems[tabFocus].children[0] as HTMLElement)?.click(); closeMenu(); return; } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault();
if (e.key === "ArrowRight") { tabFocus = dropdownItems.length - 1; } else if (e.key === "ArrowLeft") { tabFocus = 0; } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Tab" || e.key === "Shift+Tab") { e.preventDefault(); button.focus(); } }); } } }); } function closeMenu() { const button = document.getElementById("menu-button-an"); const dropdown = document.getElementById("dropdown-menu-an");
button?.setAttribute("aria-expanded", "false"); dropdown?.setAttribute("aria-hidden", "true"); button?.focus(); document.documentElement.removeEventListener("click", closeMenu); } document.addEventListener("astro:page-load", menuInitilization);</script>
---
---
<div class="group relative inline-flex text-left"> <button type="button" class="group/button peer inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-4 py-3 text-sm font-semibold text-gray-900 shadow-sm outline-offset-4 outline-black ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-4 lg:py-2.5 lg:text-base dark:bg-zinc-900 dark:text-gray-100 dark:shadow-none dark:outline-white dark:ring-zinc-800 dark:hover:bg-zinc-700" id="menu-button-an" aria-expanded="false" aria-haspopup="true" > Options <svg class="-mr-1 h-5 w-5 text-gray-400 group-aria-expanded/button:-rotate-180 motion-safe:transition motion-safe:duration-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"></path> </svg> </button>
<ul aria-orientation="vertical" aria-labelledby="menu-button-an" role="menu" class="absolute right-0 top-11 z-10 mt-2 max-h-0 min-w-max origin-top-right overflow-hidden rounded-md bg-white text-gray-800 opacity-0 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none peer-aria-expanded:max-h-96 peer-aria-expanded:opacity-100 motion-safe:transition-all motion-safe:duration-500 motion-safe:ease-in-out dark:bg-zinc-900 dark:text-gray-200" id="dropdown-menu-an" aria-hidden="true" tabindex="-1" > <li tabindex="-1" role="menuitem" id="an-menu-item-0"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Account settings</a > </li> <li tabindex="-1" role="menuitem" id="an-menu-item-1"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Support</a > </li> <li tabindex="-1" role="menuitem" id="an-menu-item-2"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >License</a > </li> <li tabindex="-1" role="menuitem" id="an-menu-item-3"> <form method="POST" id="an-menu-item-form-3" action="#"> <button type="submit" tabindex="-1" class="my-1 block w-full px-4 py-3 text-start text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Sign out</button > </form> </li> </ul></div>
<script> /** * Initializes the menu functionality for a dropdown menu. * This function sets up event listeners for the dropdown button and menu items, * allowing the user to open and navigate the dropdown menu using the keyboard. * The function also handles closing the menu when the user clicks outside of it. */ function menuInitilization() { const button = document.getElementById( "menu-button-an", ) as HTMLButtonElement; const dropdown = document.getElementById( "dropdown-menu-an", ) as HTMLUListElement; const dropdownItems = dropdown?.querySelectorAll( "li", ) as NodeListOf<HTMLLIElement>; button?.addEventListener("click", (event) => { event.stopPropagation(); if (button.getAttribute("aria-expanded") === "true") { closeMenu(); return; } button.setAttribute("aria-expanded", "true"); if (dropdown) { dropdown.setAttribute("aria-hidden", "false");
/** * Handles keyboard navigation and interactions for a dropdown menu. * * When the dropdown is focused, the following keyboard interactions are supported: * - Arrow Up/Down: Moves the focus to the previous/next item in the dropdown * - Escape: Closes the dropdown * - Enter: Clicks the currently focused dropdown item * - Tab: Moves focus back to the button */ if (dropdownItems) { document.documentElement.addEventListener("click", closeMenu); setTimeout(() => { dropdownItems[0].focus(); }, 200); // to prevent the focus setting from causing a stutter in the animation let tabFocus = 0;
dropdown.addEventListener("keydown", (keyboardEvent) => { const e = keyboardEvent as KeyboardEvent;
if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); dropdownItems[tabFocus].setAttribute("tabindex", "-1"); if (e.key === "ArrowDown") { tabFocus++; // If we're at the end, go to the start if (tabFocus >= dropdownItems.length) { tabFocus = 0; } // Move left } else if (e.key === "ArrowUp") { tabFocus--; // If we're at the start, move to the end if (tabFocus < 0) { tabFocus = dropdownItems.length - 1; } } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Escape") { closeMenu(); return; } else if (e.key === "Enter") { (dropdownItems[tabFocus].children[0] as HTMLElement)?.click(); closeMenu(); return; } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault();
if (e.key === "ArrowRight") { tabFocus = dropdownItems.length - 1; } else if (e.key === "ArrowLeft") { tabFocus = 0; } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Tab" || e.key === "Shift+Tab") { e.preventDefault(); button.focus(); } }); } } }); } function closeMenu() { const button = document.getElementById("menu-button-an"); const dropdown = document.getElementById("dropdown-menu-an");
button?.setAttribute("aria-expanded", "false"); dropdown?.setAttribute("aria-hidden", "true"); button?.focus(); document.documentElement.removeEventListener("click", closeMenu); } document.addEventListener("astro:page-load", menuInitilization);</script>
Astrojs Web Component Dropdown
The animated dropdown is available as a web component in ravixUI. You can find the implementation and code below.
---
---
<div class="group relative inline-flex text-left"> <button type="button" class="group/button peer inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-4 py-3 text-sm font-semibold text-gray-900 shadow-sm outline-offset-4 outline-black ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-4 lg:py-2.5 lg:text-base dark:bg-zinc-900 dark:text-gray-100 dark:shadow-none dark:outline-white dark:ring-zinc-800 dark:hover:bg-zinc-700" id="menu-button-an" aria-expanded="false" aria-haspopup="true" > Options <svg class="-mr-1 h-5 w-5 text-gray-400 group-aria-expanded/button:-rotate-180 motion-safe:transition motion-safe:duration-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"></path> </svg> </button>
<ul aria-orientation="vertical" aria-labelledby="menu-button-an" role="menu" class="absolute right-0 top-11 z-10 mt-2 max-h-0 min-w-max origin-top-right overflow-hidden rounded-md bg-white text-gray-800 opacity-0 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none peer-aria-expanded:max-h-96 peer-aria-expanded:opacity-100 motion-safe:transition-all motion-safe:duration-500 motion-safe:ease-in-out dark:bg-zinc-900 dark:text-gray-200" id="dropdown-menu-an" aria-hidden="true" tabindex="-1" > <li tabindex="-1" role="menuitem" id="an-menu-item-0"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Account settings</a > </li> <li tabindex="-1" role="menuitem" id="an-menu-item-1"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Support</a > </li> <li tabindex="-1" role="menuitem" id="an-menu-item-2"> <a href="#" tabindex="-1" class="my-1 block px-4 py-3 text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >License</a > </li> <li tabindex="-1" role="menuitem" id="an-menu-item-3"> <form method="POST" id="an-menu-item-form-3" action="#"> <button type="submit" tabindex="-1" class="my-1 block w-full px-4 py-3 text-start text-sm outline-offset-4 outline-black hover:bg-gray-100 focus:outline-4 dark:outline-white dark:hover:bg-zinc-700" >Sign out</button > </form> </li> </ul></div>
<script> /** * Initializes the menu functionality for a dropdown menu. * This function sets up event listeners for the dropdown button and menu items, * allowing the user to open and navigate the dropdown menu using the keyboard. * The function also handles closing the menu when the user clicks outside of it. */ function menuInitilization() { const button = document.getElementById( "menu-button-an", ) as HTMLButtonElement; const dropdown = document.getElementById( "dropdown-menu-an", ) as HTMLUListElement; const dropdownItems = dropdown?.querySelectorAll( "li", ) as NodeListOf<HTMLLIElement>; button?.addEventListener("click", (event) => { event.stopPropagation(); if (button.getAttribute("aria-expanded") === "true") { closeMenu(); return; } button.setAttribute("aria-expanded", "true"); if (dropdown) { dropdown.setAttribute("aria-hidden", "false");
/** * Handles keyboard navigation and interactions for a dropdown menu. * * When the dropdown is focused, the following keyboard interactions are supported: * - Arrow Up/Down: Moves the focus to the previous/next item in the dropdown * - Escape: Closes the dropdown * - Enter: Clicks the currently focused dropdown item * - Tab: Moves focus back to the button */ if (dropdownItems) { document.documentElement.addEventListener("click", closeMenu); setTimeout(() => { dropdownItems[0].focus(); }, 200); // to prevent the focus setting from causing a stutter in the animation let tabFocus = 0;
dropdown.addEventListener("keydown", (keyboardEvent) => { const e = keyboardEvent as KeyboardEvent;
if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault(); dropdownItems[tabFocus].setAttribute("tabindex", "-1"); if (e.key === "ArrowDown") { tabFocus++; // If we're at the end, go to the start if (tabFocus >= dropdownItems.length) { tabFocus = 0; } // Move left } else if (e.key === "ArrowUp") { tabFocus--; // If we're at the start, move to the end if (tabFocus < 0) { tabFocus = dropdownItems.length - 1; } } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Escape") { closeMenu(); return; } else if (e.key === "Enter") { (dropdownItems[tabFocus].children[0] as HTMLElement)?.click(); closeMenu(); return; } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault();
if (e.key === "ArrowRight") { tabFocus = dropdownItems.length - 1; } else if (e.key === "ArrowLeft") { tabFocus = 0; } tabFocus = tabFocus % dropdownItems.length; dropdownItems[tabFocus].focus(); } else if (e.key === "Tab" || e.key === "Shift+Tab") { e.preventDefault(); button.focus(); } }); } } }); } function closeMenu() { const button = document.getElementById("menu-button-an"); const dropdown = document.getElementById("dropdown-menu-an");
button?.setAttribute("aria-expanded", "false"); dropdown?.setAttribute("aria-hidden", "true"); button?.focus(); document.documentElement.removeEventListener("click", closeMenu); } document.addEventListener("astro:page-load", menuInitilization);</script>
The Dropdown is implemented using the Web Components API, which creates a customizable and interactive dropdown menu. It allows you to display a button that, when clicked, reveals a list of menu items. Learn more about Web Components in the MDN Web Docs.
Usage
To use the Dropdown component in your Astro project, follow these steps:
- Copy the code into a component file (e.g., Dropdown.astro).
- Import the Dropdown component into your Astro page.
- Define the necessary props for the dropdown menu.
- Render the Dropdown component in your Astro page, passing the required props.
Example implementation:
---import Dropdown from "./Dropdown.astro";
const menuLabel = "Select an option";const menuItems = [ { label: "Option 1", type: "link", href: "/option1" }, { label: "Option 2", type: "button", action: "/handle-option2" }, // Add more menu items as needed];const xAlignment = "right";const yAlignment = "top";---
<Dropdown menuLabel={menuLabel} menuItems={menuItems} xAlignment={xAlignment} yAlignment={yAlignment}/>
Data Attributes
The data-dropdown attribute is a custom attribute used to identify and control the behavior of the dropdown component. It is applied to different elements within the dropdown structure, and its value specifies the role of the element. The following values are used:
-
data-dropdown=“button”: This value is applied to the button element that serves as the trigger for the dropdown menu. The button should have the aria-expanded attribute set to “false” initially, indicating that the dropdown menu is collapsed.
-
data-dropdown=“menu”: This value is applied to the
<ul>
element that represents the dropdown menu. It should have the aria-hidden attribute set to “true” initially, indicating that the menu is hidden.
Component Props
The component accepts the following props:
menuLabel
(required): The label text for the dropdown menu of type string.xAlignment
: The horizontal alignment of the dropdown menu. Can be eitherleft
orright
(The default value isright
).yAlignment
: The vertical alignment of the dropdown menu. Can be eithertop
orbottom
(The default value istop
)menuItems
(required): An array of menu item objects. Each object should have the following properties:
Property | Type | Description |
---|---|---|
label | string (required) | The text for the menu item. |
type | string (required) | The type of the menu item. Can be either link or button . |
href | string | The URL for the link menu item. Only used if type is link . |
action | string | The action to be performed when the button menu item is clicked. Only used if type is button . |
For customization of action
you will need to modify the form component controlling the button and change the action to the desired function.
JavaScript Functionality
The Dropdown component includes a JavaScript class (XDropdown) that handles the toggling of the dropdown menu. When the dropdown button is clicked, the menu is expanded or collapsed, and the aria-expanded attribute is updated accordingly. The menu can be closed by clicking outside the dropdown or by pressing the Escape key.
Accessibility
The Dropdown component follows accessibility best practices by using appropriate ARIA attributes:
- The dropdown button has aria-expanded and aria-haspopup attributes to indicate the state of the dropdown menu and also inform of the popup menu.
- The dropdown menu has aria-orientation, aria-labelledby, role, and aria-hidden attributes to provide semantic information to assistive technologies.
- Each menu item has the role menuitem.
The component is provided as-is and may require further testing and customization to fit your specific use case.
Accessibility Testing Status
Component | NVDA | Windows Narrator | WAVE | Axe | IBM Equal Access |
---|---|---|---|---|---|
Simple Dropdown | Yes | Yes | Yes | Yes | Yes |
Animated Dropdown | Yes | Yes | Yes | Yes | Yes |
Alternate Animated Dropdown | Yes | Yes | Yes | Yes | Yes |
Astro JS Web Component | Yes | Yes | Yes | Yes | Yes |
The dropdown components are implemented with the WAI-ARIA 1.2 design patterns. The components above implement all the required functionality mention in WAI-ARIA APG specification for menu button and WAI-ARIA APG specification for menu .
The components support keyboard navigation once the dropdown is opened:
Keyboard Navigation | Description |
---|---|
Arrow Up/Down | Moves the focus to the previous/next menu item. |
Escape | Closes the dropdown menu. |
Enter | Triggers the action associated with the focused menu item. |
Arrow Left/Right | Moves the focus to the first/last menu item. |
Tab or Shift + Tab | Moves the focus out of the menu and into the menu button. |