A JavaScript class which attaches navigation menu behavior to markup.
- Fully accessible - Incorporates all essential practices for accessibility.
- Style-agnostic - Makes very few style assumptions and mandates.
- Highly flexible - Allows for infinitely-nested menus.
- Menu - An element which contains items.
- Parent Menu - A menu which has at least one child menu.
- Child Menu - A menu which has a parent menu (also known as a "submenu").
- Root Menu - A menu which has no parent menu.
- Item - An element within a menu which contains a button, and optionally, a child menu.
- Button - An element within an item which triggers an action such as navigating to a link or opening a child menu.
- Menu Button - A button which toggles the visibility of a root menu.
- The root menu must exist.
- The root menu must contain at least one items.
- A menu button may exist.
- An item must contain one button.
- An item may contain one child menu.
This pattern is infinitely nestable. See the example menu structure below:
menu button
menu
├───item
│ └───button
├───item
│ ├───button
│ └───menu
│ ├───item
│ │ └───button
│ └───item
│ └───button
├───item
│ ├───button
│ └───menu
│ ├───item
│ │ └───button
│ ├───item
│ │ ├───button
│ │ └───menu
│ │ ├───item
│ │ │ └───button
│ │ ├───item
│ │ │ └───button
│ │ └───item
│ │ └───button
│ └───item
│ └───button
├───item
│ └───button
└───item
└───button
id
is set to a randomly-generated string (if noid
is found).aria-label
is set toMenu Button
(if noaria-label
orinnerText
is found).role
is set tobutton
.aria-haspopup
is to totrue
.aria-controls
is set to its menu'sid
.aria-expanded
is set tofalse
.aria-expanded
is dynamically updated as part of aexpand/collapse
pattern.
aria-label
is set toMenu
(if noaria-label
is found).id
is set to a randomly-generated string (if noid
is found).role
is set tomenubar
(ormenu
if it is toggleable via a menu button).
- No attribute modifications.
id
is set to a randomly-generated string.tabindex
is set to-1
(or0
if it is the first button in the root menu).tabindex
is dynamically updated as part of aroving tabindex
pattern.role
is set tomenuitem
.- If the item has a menu...
aria-haspopup
is set totrue
.aria-controls
is set to its menu'sid
.aria-expanded
is set tofalse
.aria-expanded
is dynamically updated as part of aexpand/collapse
pattern.
- Clicking a button for an item without a menu will gain no special behavior.
- Clicking a button for an item with a menu will toggle the visibility of that menu.
- Clicking outside of an open menu (except the root menu) will cause the menu to close, along with any open child menus.
When focus is on a button:
- Space
- If the item has a menu...
- If the menu is closed...
- Opens the menu and moves focus to the first button in the menu.
- If the menu is open...
- Closes the menu and any open child menus.
- If the menu is closed...
- If the item has a menu...
- Enter
- If the item has a menu...
- If the menu is closed...
- Opens the menu and moves focus to the first button in the menu.
- If the menu is open...
- Closes the menu and any open child menus.
- If the menu is closed...
- If the item has a menu...
- Escape
- If the item is in a non-root menu...
- If the menu is open...
- Closes the menu and any open child menus. Moves focus to the button which opened it.
- If the menu is open...
- If the item is in a non-root menu...
- Tab
- Closes all menus. Moves focus to the next focusable element outside the menu system.
- Shift + Tab
- Closes all menus. Moves focus to the previous focusable element outside the menu system.
- Left Arrow - Moves focus to the previous button in the menu. If focus is on the first button, moves focus to the last button.
- Right Arrow - Moves focus to the next button in the menu. If focus is on the last button, moves focus to the first button.
- Up Arrow - Moves focus to the previous button in the menu. If focus is on the first button, moves focus to the last button.
- Down Arrow - Moves focus to the next button in the menu. If focus is on the last button, moves focus to the first button.
- Home - Moves focus to the first button in the menu.
- End - Moves focus to the last button in the menu.
- Page Up - Moves focus to the first button in the menu.
- Page Down - Moves focus to the last button in the menu.
- Character - Moves focus to next button in the menu that starts with the typed character (wrapping around to the beginning, if necessary). If none of the buttons start with the typed character, focus does not move.
The menu markup should be written such that menu elements have the class .menu
, item elements have the class .item
, and button elements have the class .button
.
To attach behavior to the menu, simply instantiate a new Menu
with the root menu as the only argument. The Menu
class will recursively construct instances for any child menus.
<div class="menu">
<div class="item">
<a class="button" href="#">Lorem Ipsum</a>
</div>
<div class="item">
<button class="button">Dolor Sit</button>
<div class="menu">
<div class="item">
<a class="button" href="#">Amet Consectetur</a>
</div>
<!-- More items... -->
</div>
</div>
<!-- More items... -->
</div>
<script>
new Menu(document.querySelector('.menu'));
</script>
The code above will yield a visually persistent menu. If instead the menu should be toggleable (e.g. a mobile menu toggled via a hamburger button), pass in an external button as the second argument.
<button class="menu-button"></button>
<div class="menu">
<!-- Items... -->
</div>
<script>
new Menu(document.querySelector('.menu'), document.querySelector('.menu-button'));
</script>
By default, menus will open and close instantly. However, transitions can be set using the data-transition
attribute on the menu element. There are two built-in transitions: fade
and slide
.
<div class="menu" data-transition="fade">
<!-- Items... -->
</div>
Each menu may have its own transition specified, independent from the other menus in the system.
Custom transitions are also supported. Simply define a new object in Menu.transitions
with an open()
and close()
function, and pass the key into the menu's data-transition
attribute. Both the open and close functions are required to run a provided callback function when the animation is complete (e.g. on a transitionend
event). Additionally, these functions must be configured such that the menu receives a display
value of none
for its closed state. Any other value is acceptable for its open state. Check the source code for example transitions.
Menu.transitions.myCustomTransition = {
open: (menu, callback) => {
// Transition code...
},
close: (menu, callback) => {
// Transition code...
},
};
<div class="menu" data-transition="myCustomTransition">
<!-- Items... -->
</div>
If you need to run certain functions according to menu behavior, you can listen for events on the menu element. When the menu begins the open transition, it dispatches menuopenstart
. When it finishes the open transition, it dispatches menuopenend
. When the menu begins the close transition, it dispatches menuclosestart
. When it finishes the close transition, it dispatches menucloseend
. See the example below:
const menu = document.querySelector('.menu');
new Menu(menu);
menu.addEventListener('menuopenstart', () => { console.log('Opening...'); });
menu.addEventListener('menuopenend', () => { console.log('Opened!'); });
menu.addEventListener('menuclosestart', () => { console.log('Closing...'); });
menu.addEventListener('menucloseend', () => { console.log('Closed!'); });