Navigation menus
When and how to use this component
The horizontal menu is used to display the first 2 levels of pages within a website’s architecture.
For levels 3 and below the menu should be complemented by using navigation blocks to drill down to sub-pages.
Guidelines
Amount of Items
Due to limited horizontal space, especially on low resolution screens, the number of items on the first level of the menu should be 3-5 items.
Labeling
Labels on the first level of the menu should be simple and composed of 1 to 3 words (for the English language version of the menu).
Using 3 words should be an exception when you want to refer to two related topics (e.g. News and events / Contracts and funding).
When to use this component
- if you need an alternative to the listing navigation blocks
- only for improved sites
- only with a basic page header
- below the basic page header
- on every page of the website
Do not use this component
- with a default page header
Resources
- https://www.w3.org/WAI/GL/wiki/Using_ARIA_menus
- https://www.w3.org/WAI/tutorials/menus/
- https://dequeuniversity.com/library/aria/menus/megamenu
- https://github.com/adobe-accessibility/Accessible-Mega-Menu
- https://ux.stackexchange.com/questions/5124/what-are-good-rules-for-naming-menu-items/5126
- https://thrivehive.com/navigation-menu-best-practices
{#
Parameters:
- "menu_label" (integer): max number of items to display (0 = display all) (default: 5)
- "menu_aria_label" (string): label displayed before the list (default: '')
- "links" (array): [{
"href" (string): target of the link
"label" (string): label of the link
"is_active" (boolean): does the menu contains the current page? (default: false)
"children_links" (array): [{
"href" (string): target of the link
"label" (string): label of the link
"is_active" (boolean): is it the current page? (default: false)
}]
}]
- "extra_classes" (string): extra CSS classes to be added
- "extra_attributes" (array): extra attributes classes (optional, format: [{ 'name': 'name_of_the_attribute', 'value': 'value_of_the_attribute'}])
Blocks:
- "navigation": instead of providing an array of links, you can also embed the template and fill the "navigation" block directly
#}
{% set menu_label = menu_label|default('') %}
{% set menu_aria_label = menu_aria_label|default('') %}
{# Internal properties #}
{% set _css_class = 'ecl-navigation-menu' %}
{% set _extra_attributes = '' %}
{# Internal logic - Process properties #}
{% if extra_classes is defined %}
{% set _css_class = _css_class ~ ' ' ~ extra_classes %}
{% endif %}
{% if extra_attributes is defined %}
{% for attr in extra_attributes %}
{% set _extra_attributes = _extra_attributes ~ ' ' ~ attr.name ~ '="' ~ attr.value ~'"' %}
{% endfor %}
{% endif %}
{# Print the result #}
<nav class="{{ _css_class }}" aria-label="{{ menu_aria_label }}"{{ _extra_attributes|raw }}>
<div class="ecl-container">
<button class="ecl-navigation-menu__toggle ecl-navigation-menu__hamburger ecl-navigation-menu__hamburger--squeeze" aria-controls="nav-menu-expandable-root" aria-expanded="false">
<span class="ecl-navigation-menu__hamburger-box">
<span class="ecl-navigation-menu__hamburger-inner"></span>
</span>
<span class="ecl-navigation-menu__hamburger-label">{{ menu_label }}</span>
</button>
<ul class="ecl-navigation-menu__root" id="nav-menu-expandable-root" aria-hidden="true">
{% block navigation %}
{% for _link in links %}
{% set _link_class_name = 'ecl-navigation-menu__item' ~ (_link.is_active ? ' ecl-navigation-menu__item--active' : '') %}
<li class="{{ _link_class_name }}">
{% if _link.children_links is not defined or _link.children_links is empty %}
<a href="{{ _link.href }}" class="ecl-navigation-menu__link">{{ _link.label }}</a>
{% else %}
<a href="{{ _link.href }}" class="ecl-navigation-menu__link" aria-controls="nav-menu-expandable-group-{{ loop.index }}" aria-expanded="false" aria-haspopup="true">{{ _link.label }}</a>
<ul class="ecl-navigation-menu__group" id="nav-menu-expandable-group-{{ loop.index }}">
{% for _child_link in _link.children_links %}
{% set _child_link_class_name = 'ecl-navigation-menu__link' ~ (_child_link.is_active ? ' ecl-navigation-menu__link--active' : '') %}
<li class="ecl-navigation-menu__item"><a href="{{ _child_link.href }}" class="{{ _child_link_class_name }}">{{ _child_link.label }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
{% endblock %}
</ul>
</div>
</nav>
{
"menu_label": "Menu",
"menu_aria_label": "Main Navigation",
"links": [
{
"label": "Home",
"href": "#home"
},
{
"label": "Item 1",
"href": "#item1",
"is_active": true,
"children_links": [
{
"label": "Item 1.1",
"href": "#item1-1"
},
{
"label": "Item 1.2",
"href": "#item1-2"
},
{
"label": "Item 1.3",
"href": "#item1-3"
},
{
"label": "Item 1.4",
"href": "#item1-4",
"is_active": true
},
{
"label": "Item 1.5",
"href": "#item1-5"
},
{
"label": "Item 1.6",
"href": "#item1-6"
}
]
},
{
"label": "Item 2",
"href": "#item2",
"children_links": [
{
"label": "Item 2.1",
"href": "#item2-1"
},
{
"label": "Item 2.2",
"href": "#item2-2"
},
{
"label": "Item 2.3",
"href": "#item2-3"
},
{
"label": "Item 2.4",
"href": "#item2-4"
},
{
"label": "Item 2.5",
"href": "#item2-5"
},
{
"label": "Item 2.6",
"href": "#item2-6"
}
]
},
{
"label": "Item 3",
"href": "#item3"
}
],
"_demo": {
"scripts": "document.addEventListener('DOMContentLoaded', function () {\n ECL.megamenu();\n });"
}
}
<nav class="ecl-navigation-menu" aria-label="Main Navigation">
<div class="ecl-container">
<button class="ecl-navigation-menu__toggle ecl-navigation-menu__hamburger ecl-navigation-menu__hamburger--squeeze" aria-controls="nav-menu-expandable-root" aria-expanded="false">
<span class="ecl-navigation-menu__hamburger-box">
<span class="ecl-navigation-menu__hamburger-inner"></span>
</span>
<span class="ecl-navigation-menu__hamburger-label">Menu</span>
</button>
<ul class="ecl-navigation-menu__root" id="nav-menu-expandable-root" aria-hidden="true">
<li class="ecl-navigation-menu__item">
<a href="#home" class="ecl-navigation-menu__link">Home</a>
</li>
<li class="ecl-navigation-menu__item ecl-navigation-menu__item--active">
<a href="#item1" class="ecl-navigation-menu__link" aria-controls="nav-menu-expandable-group-2" aria-expanded="false" aria-haspopup="true">Item 1</a>
<ul class="ecl-navigation-menu__group" id="nav-menu-expandable-group-2">
<li class="ecl-navigation-menu__item"><a href="#item1-1" class="ecl-navigation-menu__link">Item 1.1</a></li>
<li class="ecl-navigation-menu__item"><a href="#item1-2" class="ecl-navigation-menu__link">Item 1.2</a></li>
<li class="ecl-navigation-menu__item"><a href="#item1-3" class="ecl-navigation-menu__link">Item 1.3</a></li>
<li class="ecl-navigation-menu__item"><a href="#item1-4" class="ecl-navigation-menu__link ecl-navigation-menu__link--active">Item 1.4</a></li>
<li class="ecl-navigation-menu__item"><a href="#item1-5" class="ecl-navigation-menu__link">Item 1.5</a></li>
<li class="ecl-navigation-menu__item"><a href="#item1-6" class="ecl-navigation-menu__link">Item 1.6</a></li>
</ul>
</li>
<li class="ecl-navigation-menu__item">
<a href="#item2" class="ecl-navigation-menu__link" aria-controls="nav-menu-expandable-group-3" aria-expanded="false" aria-haspopup="true">Item 2</a>
<ul class="ecl-navigation-menu__group" id="nav-menu-expandable-group-3">
<li class="ecl-navigation-menu__item"><a href="#item2-1" class="ecl-navigation-menu__link">Item 2.1</a></li>
<li class="ecl-navigation-menu__item"><a href="#item2-2" class="ecl-navigation-menu__link">Item 2.2</a></li>
<li class="ecl-navigation-menu__item"><a href="#item2-3" class="ecl-navigation-menu__link">Item 2.3</a></li>
<li class="ecl-navigation-menu__item"><a href="#item2-4" class="ecl-navigation-menu__link">Item 2.4</a></li>
<li class="ecl-navigation-menu__item"><a href="#item2-5" class="ecl-navigation-menu__link">Item 2.5</a></li>
<li class="ecl-navigation-menu__item"><a href="#item2-6" class="ecl-navigation-menu__link">Item 2.6</a></li>
</ul>
</li>
<li class="ecl-navigation-menu__item">
<a href="#item3" class="ecl-navigation-menu__link">Item 3</a>
</li>
</ul>
</div>
</nav>
-
Content:
/** * ECL Horizontal menus * @define navigation-menu */ .ecl-navigation-menu { background-color: map-get($ecl-colors, 'blue-75'); margin: 0; } .ecl-navigation-menu__root { background-color: map-get($ecl-colors, 'blue-25'); display: flex; flex-direction: column; flex-grow: 1; list-style: none; margin: 0; padding: 0; position: relative; } .ecl-navigation-menu__root[aria-hidden='true'] { display: none; // Force display if no JS .no-js & { display: flex; } } .ecl-navigation-menu__root::before { background-color: map-get($ecl-colors, 'blue-25'); content: ''; height: 100%; left: 50%; position: absolute; top: 0; transform: translateX(-50%); width: 100vw; } .ecl-navigation-menu__toggle { background-color: map-get($ecl-colors, 'blue-75'); border-width: 0; color: #fff; font-size: map-get($ecl-font-size, 'm'); font-weight: bold; padding: map-get($ecl-spacing, 'm') 0; position: relative; text-align: left; text-decoration: none; width: 100%; } .ecl-navigation-menu__group { background-color: map-get($ecl-colors, 'grey-5'); display: none; list-style: none; margin: 0; padding-left: 0; position: relative; } .ecl-navigation-menu__group::before { background-color: map-get($ecl-colors, 'grey-5'); content: ''; height: 100%; left: 50%; position: absolute; top: 0; transform: translate(-50%, 0); width: 100vw; } .ecl-navigation-menu__link { align-items: center; background-color: transparent; border-bottom: 1px solid map-get($ecl-colors, 'blue-50'); border-left-width: 0; border-right-width: 0; border-top: 0; color: map-get($ecl-colors, 'blue-120'); display: flex; font-size: map-get($ecl-font-size, 's'); font-weight: bold; justify-content: space-between; padding: map-get($ecl-spacing, 's') 0; position: relative; text-decoration: none; width: 100%; } .ecl-navigation-menu__item--active > .ecl-navigation-menu__link { color: #fff; } .ecl-navigation-menu__group .ecl-navigation-menu__link { &:hover, &:focus { text-decoration: underline; } } .ecl-navigation-menu__item:last-child .ecl-navigation-menu__link { border-bottom-width: 0; } /* Icon display */ .ecl-navigation-menu__link[aria-expanded]::after { padding-left: map-get($ecl-spacing, 'xxxs'); .no-js & { display: none; } } .ecl-navigation-menu__link[aria-expanded='false'] { &::after { @extend %ecl-icon--after; @include ecl-icon('down'); } } .ecl-navigation-menu__link[aria-expanded='true'] { border-bottom-width: 0; &::after { @extend %ecl-icon--after; @include ecl-icon('up'); } } .ecl-navigation-menu__link ~ .ecl-navigation-menu__group .ecl-navigation-menu__link { border-bottom-width: 0; font-weight: normal; &::after { display: none; } &--active { font-weight: bold; } } /* Collapsible block display */ .ecl-navigation-menu__link[aria-expanded='true'] ~ .ecl-navigation-menu__group { display: block; } .ecl-navigation-menu__item--active { background-color: map-get($ecl-colors, 'blue-75'); position: relative; } .ecl-navigation-menu__item--active::before { background-color: map-get($ecl-colors, 'blue-75'); //#f5f5f5; content: ''; height: 100%; left: 50%; position: absolute; top: 0; transform: translateX(-50%); width: 100vw; } .ecl-navigation-menu__item--active > .ecl-navigation-menu__link:not([aria-expanded='true']) { border-bottom-width: 0; } /* Hamburger button */ // Based on https://github.com/jonsuh/hamburgers .ecl-navigation-menu__hamburger-box { left: 0; position: absolute; top: 50%; } .ecl-navigation-menu__hamburger-label { padding-left: map-get($ecl-spacing, 'm') + map-get($ecl-spacing, 's'); } .ecl-navigation-menu__hamburger-inner { display: block; margin-top: -0.1em; } .ecl-navigation-menu__hamburger-inner, .ecl-navigation-menu__hamburger-inner::before, .ecl-navigation-menu__hamburger-inner::after { background-color: #fff; border-radius: 0.1em; height: 0.2em; position: absolute; width: map-get($ecl-spacing, 'm'); } .ecl-navigation-menu__hamburger-inner::before, .ecl-navigation-menu__hamburger-inner::after { content: ''; display: block; } .ecl-navigation-menu__hamburger-inner::before { top: -0.5em; } .ecl-navigation-menu__hamburger-inner::after { bottom: -0.5em; } .ecl-navigation-menu__hamburger--squeeze .ecl-navigation-menu__hamburger-inner { transition-duration: 0.075s; transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } .ecl-navigation-menu__hamburger--squeeze .ecl-navigation-menu__hamburger-inner::before { transition: top 0.075s 0.12s ease, opacity 0.075s ease; } .ecl-navigation-menu__hamburger--squeeze .ecl-navigation-menu__hamburger-inner::after { transition: bottom 0.075s 0.12s ease, transform 0.075s cubic-bezier(0.55, 0.055, 0.675, 0.19); } .ecl-navigation-menu__hamburger--squeeze[aria-expanded='true'] .ecl-navigation-menu__hamburger-inner { transform: rotate(45deg); transition-delay: 0.12s; transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } .ecl-navigation-menu__hamburger--squeeze[aria-expanded='true'] .ecl-navigation-menu__hamburger-inner::before { opacity: 0; top: 0; transition: top 0.075s ease, opacity 0.075s 0.12s ease; } .ecl-navigation-menu__hamburger--squeeze[aria-expanded='true'] .ecl-navigation-menu__hamburger-inner::after { bottom: 0; transform: rotate(-90deg); transition: bottom 0.075s ease, transform 0.075s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1); } @include ecl-media-breakpoint-up(md) { .ecl-navigation-menu { background-color: map-get($ecl-colors, 'blue-25'); } .ecl-navigation-menu__root { background-color: transparent; flex-direction: row; } .ecl-navigation-menu__root[aria-hidden='true'] { display: flex; } .ecl-navigation-menu__root::before { display: none; } .ecl-navigation-menu__toggle { display: none; } .ecl-navigation-menu__group { flex-wrap: wrap; left: 0; padding: map-get($ecl-spacing, 'xs') 0 map-get($ecl-spacing, 's'); position: absolute; top: 100%; width: 100%; } .ecl-navigation-menu__link { border-bottom-width: 0; font-size: map-get($ecl-font-size, 'xs'); margin: 0; padding: map-get($ecl-spacing, 's'); } .ecl-navigation-menu__group .ecl-navigation-menu__link { padding: map-get($ecl-spacing, 'xs') map-get($ecl-spacing, 's') map-get($ecl-spacing, 'xs') 0; } .ecl-navigation-menu__link[aria-expanded]::after { position: relative; } .ecl-navigation-menu__link[aria-expanded='true'] { background-color: map-get($ecl-colors, 'grey-5'); color: map-get($ecl-colors, 'blue-120'); } .ecl-navigation-menu__link[aria-expanded='true'] ~ .ecl-navigation-menu__group { align-items: baseline; display: flex; z-index: map-get($ecl-z-index, 'highlight'); } .ecl-navigation-menu__item { display: flex; } .ecl-navigation-menu__group .ecl-navigation-menu__item { flex-basis: 25%; } .ecl-navigation-menu__item--active { position: static; } .ecl-navigation-menu__item--active::before { display: none; } .ecl-navigation-menu__item--active > .ecl-navigation-menu__link[aria-expanded='true'] { color: map-get($ecl-colors, 'blue-120'); } }
- URL: /components/raw/ecl-navigation-menus/_horizontal-menus.scss
- Filesystem Path: framework/components/ecl-navigation/ecl-navigation-menus/_horizontal-menus.scss
- Size: 7.4 KB
-
Content:
import { queryAll } from '@ec-europa/ecl-base/helpers/dom'; import { toggleExpandable } from '@ec-europa/ecl-expandables/expandables'; const onClick = (node, menu) => e => { if (node && node.hasAttribute('aria-haspopup')) { const hasPopup = node.getAttribute('aria-haspopup'); if (hasPopup === '' || hasPopup === 'true') { e.preventDefault(); toggleExpandable(node, { context: menu, closeSiblings: true, }); } } }; const onKeydown = (node, menu) => e => { const currentTab = node.parentElement; const previousTabItem = currentTab.previousElementSibling || currentTab.parentElement.lastElementChild; const nextTabItem = currentTab.nextElementSibling || currentTab.parentElement.firstElementChild; // don't catch key events when ⌘ or Alt modifier is present if (e.metaKey || e.altKey) return; // catch left/right and up/down arrow key events // if new next/prev tab available, show it by passing tab anchor to showTab method switch (e.keyCode) { // ENTER or SPACE case 13: case 32: onClick(e.currentTarget, menu)(e); /* e.preventDefault(); toggleExpandable(e.currentTarget, { context: menu, closeSiblings: true, }); */ break; // ARROWS LEFT and UP case 37: case 38: e.preventDefault(); previousTabItem.querySelector('a').focus(); break; // ARROWS RIGHT and DOWN case 39: case 40: e.preventDefault(); nextTabItem.querySelector('a').focus(); break; default: break; } }; export const megamenu = ({ selector: selector = '.ecl-navigation-menu', toggleSelector: toggleSelector = '.ecl-navigation-menu__toggle', listSelector: listSelector = '.ecl-navigation-menu__root', linkSelector: linkSelector = '.ecl-navigation-menu__link', } = {}) => { const megamenusArray = queryAll(selector); megamenusArray.forEach(menu => { // Make the toggle expandable const toggle = menu.querySelector(toggleSelector); if (toggle) { toggle.addEventListener('click', () => toggleExpandable(toggle, { context: menu }) ); } // Get the list of links const list = menu.querySelector(listSelector); // Get expandables within the list const nodesArray = queryAll(linkSelector, list); nodesArray.forEach(node => { node.addEventListener('click', onClick(node, list)); node.addEventListener('keydown', onKeydown(node, list)); }); }); }; export default megamenu;
- URL: /components/raw/ecl-navigation-menus/megamenu.js
- Filesystem Path: framework/components/ecl-navigation/ecl-navigation-menus/megamenu.js
- Size: 2.5 KB
- Handle: @ec-europa/ecl-navigation-menus
- Tags: organism
- Preview:
- Filesystem Path: framework/components/ecl-navigation/ecl-navigation-menus/ecl-navigation-menus.twig