Europa Component Library

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

{#
  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