<nav class="rtds-side-navigation " aria-labelledby="sidebarLabel" id="sideNav">

    <span id="sidebarLabel" class="rtds-side-navigation__title rtds-sr-only md:rtds-not-sr-only md:rtds-pb-4 md:rtds-border-b md:rtds-border-gray-01">
        <span class="rtds-side-navigation__label-context">Titolo della sezione</span>
        <span class="rtds-block rtds-font-medium">Indice della sezione</span>
    </span>

    <!-- Mobile toggle button -->
    <button class="rtds-side-navigation__list-toggle rtds-nav-list-toggle md:rtds-hidden rtds-btn rtds-btn--icon-right rtds-btn--only-text rtds-btn--small rtds-group" aria-expanded="false" aria-controls="sideNavList">
        <span class="rtds-grid rtds-gap-2 rtds-text-left">

            <span class="rtds-side-navigation__label-context">Titolo della sezione</span>

            <span>Indice della sezione</span>
        </span>
        <svg class="rtds-icon rtds-fill-current rtds-w-5 rtds-h-5" aria-hidden="true" focusable="false" role="img">
            <use href="../../icons.svg#mini--chevron-up" />
        </svg>

    </button>

    <!-- Navigation list -->
    <ul class="rtds-nav-list rtds-side-navigation__list has-nav-dropdown" id="sideNavList">

        <li class="rtds-side-navigation__item is-first-level">
            <span class="rtds-side-navigation__first-level-label is-current">
                <a href="#" class="rtds-side-navigation__link rtds-nav-link is-current" aria-current="page">
                    <span class="rtds-side-navigation__label">

                        Item 1
                    </span>
                </a>

            </span>

        </li>

        <li class="rtds-side-navigation__item is-first-level">
            <span class="rtds-side-navigation__first-level-label">
                <a href="#" class="rtds-side-navigation__link rtds-nav-link">
                    <span class="rtds-side-navigation__label">

                        Item 2
                    </span>
                </a>

            </span>

        </li>

        <li class="rtds-side-navigation__item is-first-level">
            <span class="rtds-side-navigation__first-level-label">
                <a href="#" class="rtds-side-navigation__link rtds-nav-link">
                    <span class="rtds-side-navigation__label">

                        Item 3
                    </span>
                </a>

            </span>

        </li>

    </ul>
</nav>
<nav class="rtds-side-navigation{% if horizontal %} rtds-side-navigation--horizontal{% endif %}{% if anchor %} rtds-side-navigation--anchor{% endif %}{% block classes %} {% if classes %}{{ classes }}{% endif %}{% endblock %}" 
     aria-labelledby="{{ labelId }}"
     {% if id %}id="{{ id }}"{% endif %}
     {% block attributes %}{% endblock %}>
  
  <span id="{{ labelId }}" class="rtds-side-navigation__title{% if hiddenLabel %} rtds-sr-only{% else %} rtds-sr-only md:rtds-not-sr-only md:rtds-pb-4 md:rtds-border-b md:rtds-border-gray-01{% endif %}">
    {% if labelContext %}<span class="rtds-side-navigation__label-context">{{ labelContext | safe }}</span>{% endif %}
    <span class="rtds-block rtds-font-medium">{{ label | safe }}</span>
  </span>

  <!-- Mobile toggle button -->
  <button class="rtds-side-navigation__list-toggle rtds-nav-list-toggle md:rtds-hidden rtds-btn rtds-btn--icon-right rtds-btn--only-text rtds-btn--small rtds-group" 
          aria-expanded="false" 
          aria-controls="{{ listId }}">
    <span class="rtds-grid rtds-gap-2 rtds-text-left">
      {% if labelContext %}
        <span class="rtds-side-navigation__label-context">{{ labelContext | safe }}</span>
      {% endif %}
      <span>{{ label | safe }}</span>
    </span>
    {% render '@icon--small', { id: 'mini--chevron-up', size: 'rtds-w-5 rtds-h-5' }, true %}
  </button>

  <!-- Navigation list -->
  <ul class="rtds-nav-list rtds-side-navigation__list has-nav-dropdown" id="{{ listId }}">
    {% for firstLevel in firstLevels %}
      <li class="rtds-side-navigation__item is-first-level{% if itemClasses %} {{ itemClasses }}{% endif %}{% if firstLevel.isCurrentTrail %} is-current-trail{% endif %}">
        <span class="rtds-side-navigation__first-level-label{% if firstLevel.isCurrentPage %} is-current{% endif %}">
          <a href="{% if firstLevel.href %}{{ firstLevel.href }}{% else %}#{% endif %}" 
             class="rtds-side-navigation__link rtds-nav-link{% if firstLevelLinkClasses %} {{ firstLevelLinkClasses }}{% endif %}{% if firstLevel.isCurrentPage %} is-current{% endif %}"
             {% if firstLevel.isCurrentPage and not singlePage %} aria-current="page"{% endif %}>
            <span class="rtds-side-navigation__label">
              {% if firstLevel.iconLeft %}
                {% render '@icon--small', { id: firstLevel.iconLeft, size: 'rtds-w-4 rtds-h-4' }, true %}
              {% endif %}
              {{ firstLevel.label }}
            </span>
          </a>
        
          {% if not firstLevel.subItems %}
        </span>
          {% endif %}

        {% if firstLevel.subItems %}
          <button class="rtds-side-navigation__nav-toggle rtds-nav-toggle rtds-group{% if firstLevel.isCurrentPage %} is-current{% endif %}"
                  aria-expanded="{% if firstLevel.isCurrentTrail %}true{% else %}false{% endif %}"
                  aria-controls="{{ id }}-subnav-{{ loop.index }}">
            <span class="rtds-sr-only">Dettaglio {{ firstLevel.label }}</span>
            {% set buttonIcon = "" %}
            {% if firstLevel.iconRight %}
              {% set buttonIcon = firstLevel.iconRight %}
            {% else %}
              {% set buttonIcon = "mini--chevron-down" %}
            {% endif %}
            {% render '@icon--small', { id: buttonIcon, classes: 'rtds-transition-all rtds-duration-200 rtds-ease-out group-aria-expanded:rtds-rotate-180', size: 'rtds-w-4 rtds-h-4' }, true %}
          </button>
        </span>
          
          <ul class="rtds-side-navigation__submenu{% if firstLevel.isCurrentTrail %} rtds-block{% else %} rtds-hidden{% endif %} rtds-w-full{% if subnavClasses %} {{ subnavClasses }}{% endif %}" 
              id="{{ id }}-subnav-{{ loop.index }}">
            {% for item in firstLevel.subItems %}
              <li class="rtds-side-navigation__item">
                <a href="{% if item.href %}{{ item.href }}{% else %}#{% endif %}" 
                   class="rtds-side-navigation__link rtds-nav-link{% if subnavLinkClasses %} {{ subnavLinkClasses }}{% endif %}{% if item.isCurrentPage %} is-current{% endif %}"
                   {% if item.isCurrentPage %} aria-current="page"{% endif %}>
                  <span class="rtds-side-navigation__label">{{ item.subitemLabel }}</span>
                </a>
              </li>
            {% endfor %}
          </ul>
        {% endif %}
      </li>
    {% endfor %}
  </ul>
</nav>
{
  "label": "Indice della sezione",
  "labelId": "sidebarLabel",
  "id": "sideNav",
  "listId": "sideNavList",
  "firstLevels": [
    {
      "label": "Item 1",
      "isCurrentPage": true
    },
    {
      "label": "Item 2"
    },
    {
      "label": "Item 3"
    }
  ],
  "labelContext": "Titolo della sezione"
}
  • Content:
    /**
     * SIDE NAVIGATION
     * Supporta sia vertical che horizontal tramite modifier class
     */
     @layer components {
        /* ============================================
           BASE STYLES - Common to all layouts
           ============================================ */
        
        .rtds-side-navigation__title {
            @apply rtds-grid rtds-gap-3 rtds-content-03 rtds-text-base rtds-font-bold;
        }
    
        .rtds-side-navigation__label-context {
            @apply rtds-text-lg md:rtds-text-xl lg:rtds-text-2xl rtds-block rtds-heading-3 rtds-content-01;
        }
    
        .rtds-side-navigation__item {
            @apply rtds-content-03 rtds-bg-white rtds-flex rtds-flex-wrap rtds-items-stretch rtds-text-base;
        }
    
        .rtds-side-navigation__first-level-label {
            @apply rtds-flex rtds-flex-1 rtds-font-bold;
        }
    
        .rtds-side-navigation__link {
            @apply rtds-text-current rtds-border-transparent rtds-flex rtds-flex-1 rtds-items-center rtds-gap-2 rtds-p-4 rtds-transition hover:rtds-content-primary hover:rtds-underline;
        }
    
        .rtds-side-navigation__label {
            @apply rtds-inline-block;
        }
    
        .rtds-side-navigation__link:where(.is-current) {
            @apply rtds-content-primary rtds-border-current;
        }
    
        :where(.rtds-side-navigation__link.is-current) .rtds-side-navigation__label {
            @apply rtds-pb-1 rtds-border-b-2 rtds-border-current;
        }
    
        .rtds-side-navigation__nav-toggle {
            @apply rtds-flex rtds-items-center rtds-justify-center rtds-w-8 hover:rtds-content-primary;
        }
    
        /* MOBILE */
        .rtds-side-navigation__list-toggle {
            @apply rtds-w-full rtds-justify-between rtds-items-end rtds-pl-0 rtds-border-y-0 rtds-border-l-0 rtds-border-r-0 rtds-border-b rtds-rounded-none rtds-border-gray-01 rtds-font-medium rtds-content-03 rtds-text-sm md:rtds-text-base hover:rtds-bg-white focus:rtds-bg-white hover:rtds-border-gray-02;
        }
    
        .rtds-side-navigation__list-toggle:where([aria-expanded="false"]) .rtds-icon {
            @apply -rtds-rotate-180;
        }
    
        .rtds-side-navigation__list-toggle:where([aria-expanded="false"]) ~ .rtds-side-navigation__list {
            @apply rtds-hidden md:rtds-block;
        }
    
        /* sub-navigation */
        .rtds-side-navigation__submenu {
            @apply rtds-pl-4;
        }
    
    
        /* ============================================
           HORIZONTAL MODIFIER - Layout orizzontale da MD
           ============================================ */
        
        .rtds-side-navigation--horizontal {
            @screen md {
                /* Title nascosto per horizontal */
                .rtds-side-navigation__title {
                    @apply rtds-sr-only;
                }
    
                /* Lista: flex horizontal */
                .rtds-side-navigation__list {
                    @apply rtds-flex rtds-flex-wrap rtds-gap-0 rtds-py-4 rtds-border-t-0;
                }
    
                /* Items: no wrap, posizione relativa */
                .rtds-side-navigation__item {
                    @apply rtds-flex-nowrap rtds-relative;
                }
    
                /* First level label: inline con toggle */
                .rtds-side-navigation__first-level-label {
                    @apply rtds-flex rtds-items-center;
                }
    
                /* Link: padding ridotto, border-bottom, no underline */
                .rtds-side-navigation__link {
                    @apply rtds-mx-4 rtds-px-2 rtds-py-0 rtds-text-sm md:rtds-text-base rtds-font-semibold rtds-border-b-2 rtds-border-transparent hover:rtds-border-primary hover:rtds-no-underline;
                }
    
                .rtds-side-navigation__link:where(.is-current) {
                    @apply rtds-border-primary rtds-content-primary;
                }
    
                :where(.rtds-side-navigation__link.is-current) .rtds-side-navigation__label {
                    @apply rtds-pb-0 rtds-border-b-0;
                }
    
                /* Toggle button per horizontal */
                .rtds-side-navigation__nav-toggle {
                    @apply rtds-px-2 rtds-py-2 rtds-w-auto;
                }
    
                /* Submenu: dropdown assoluto */
                .rtds-side-navigation__submenu {
                    @apply rtds-absolute rtds-top-full rtds-left-0 rtds-min-w-[200px] rtds-pl-0 rtds-bg-white rtds-border rtds-border-gray-01 rtds-shadow-lg rtds-z-10;
                }
    
                /* Submenu items */
                .rtds-side-navigation__submenu .rtds-side-navigation__item {
                    @apply rtds-w-full rtds-flex-wrap;
                }
    
                /* Submenu links con hover background */
                .rtds-side-navigation__submenu .rtds-side-navigation__link {
                    @apply rtds-mx-0 rtds-px-6 rtds-py-3 rtds-text-sm rtds-border-b-0 hover:rtds-background-01 hover:rtds-border-transparent;
                }
    
                /* Submenu link corrente con background */
                .rtds-side-navigation__submenu .rtds-side-navigation__link:where(.is-current) {
                    @apply rtds-background-02 rtds-font-medium rtds-border-transparent;
                }
    
                .rtds-side-navigation__submenu .rtds-side-navigation__link:where(.is-current) .rtds-side-navigation__label {
                    @apply rtds-border-b-0;
                }
            }
        }
    
    
        /* ============================================
           ANCHOR NAVIGATION
           ============================================ */
        
        .rtds-side-navigation--anchor {
            @apply rtds-scroll-m-[--header-height] md:rtds-scroll-m-0;
        }
    
        :where(.rtds-side-navigation--anchor) .rtds-side-navigation__link {
            @apply rtds-border-b-0 rtds-border-l-4;
        }
    
        :where(.rtds-side-navigation--anchor .rtds-side-navigation__link.is-current) .rtds-side-navigation__label {
            @apply rtds-pb-0 rtds-border-b-0;
        }
    
        /* Anchor + horizontal: usa border-bottom */
        .rtds-side-navigation--anchor.rtds-side-navigation--horizontal {
            @screen md {
                :where(.rtds-side-navigation__link) {
                    @apply rtds-border-l-0 rtds-border-b-2;
                }
            }
        }
    }
  • URL: /components/raw/side-navigation/side-navigation.css
  • Filesystem Path: components/04-organisms/side-navigation/side-navigation.css
  • Size: 5.9 KB
  • Content:
    'use strict';
    
    /**
     * SIDE NAVIGATION - JavaScript
     * Gestisce sia vertical che horizontal navigation
     * Supporta keyboard navigation, ESC key, e focus management
     */
    
    // Helper function to find the closest ancestor with a specific tag name
    function findAncestor(element, tagName) {
      while (element) {
        if (element.tagName.toLowerCase() === tagName) {
          return element;
        }
        element = element.parentElement;
      }
      return null;
    }
    
    class submenuDisclosure {
      constructor(domNode) {
        this.rootNode = domNode;
        this.parentNav = domNode.closest('.rtds-side-navigation');
        this.isHorizontal = this.parentNav?.classList.contains('rtds-side-navigation--horizontal');
        this.controlledNodes = [];
        this.openIndex = null;
        this.useArrowKeys = true;
        
        // Trova tutti i link e toggle button di primo livello
        this.topLevelNodes = [
          ...this.rootNode.querySelectorAll('.rtds-nav-link, .rtds-nav-toggle'),
        ];
    
        this.init();
      }
    
      init() {
        this.topLevelNodes.forEach((node) => {
          // Gestisci button + menu
          if (
            node.tagName.toLowerCase() === 'button' &&
            node.hasAttribute('aria-controls')
          ) {
            const menuLiParent = node.closest('li');
            const menu = menuLiParent.querySelector('ul');
    
            if (menu) {
              // Salva riferimento al menu controllato
              this.controlledNodes.push(menu);
    
              // Inizializza stato (aperto se current trail, chiuso altrimenti)
              if (menu.querySelector('.is-current') || node.classList.contains('is-current')) {
                node.setAttribute('aria-expanded', 'true');
                this.toggleMenu(menu, true);
              } else {
                node.setAttribute('aria-expanded', 'false');
                this.toggleMenu(menu, false);
              }
    
              // Attach event listeners
              menu.addEventListener('keydown', this.onMenuKeyDown.bind(this));
              node.addEventListener('click', this.onButtonClick.bind(this));
              node.addEventListener('keydown', this.onButtonKeyDown.bind(this));
            }
          }
          // Gestisci link semplici
          else {
            this.controlledNodes.push(null);
            node.addEventListener('keydown', this.onLinkKeyDown.bind(this));
          }
        });
    
        // Riferimento al toggle button principale (mobile)
        this.mainMenuToggle = this.parentNav?.querySelector('.rtds-nav-list-toggle');
    
        // ESC chiude il menu principale su mobile
        this.rootNode.addEventListener('keydown', (event) => {
          if (event.key === 'Escape' && window.innerWidth < 768) {
            if (this.mainMenuToggle?.getAttribute('aria-expanded') === 'true') {
              this.mainMenuToggle.setAttribute('aria-expanded', 'false');
              this.mainMenuToggle.focus();
            }
          }
        });
      }
    
      // Gestisce navigazione con arrow keys
      controlFocusByKey(keyboardEvent, nodeList, currentIndex) {
        switch (keyboardEvent.key) {
          case 'ArrowUp':
          case 'ArrowLeft':
            keyboardEvent.preventDefault();
            if (currentIndex > -1) {
              var prevIndex = Math.max(0, currentIndex - 1);
              nodeList[prevIndex].focus();
            }
            break;
          case 'ArrowDown':
          case 'ArrowRight':
            keyboardEvent.preventDefault();
            if (currentIndex > -1) {
              var nextIndex = Math.min(nodeList.length - 1, currentIndex + 1);
              nodeList[nextIndex].focus();
            }
            break;
          case 'Home':
            keyboardEvent.preventDefault();
            nodeList[0].focus();
            break;
          case 'End':
            keyboardEvent.preventDefault();
            nodeList[nodeList.length - 1].focus();
            break;
        }
      }
    
      // Chiude il submenu aperto
      close() {
        this.toggleExpand(this.openIndex, false);
      }
    
      // Click su toggle button
      onButtonClick(event) {
        var target = event.target;
    
        if (target.tagName.toLowerCase() !== 'button') {
          var buttonAncestor = findAncestor(target, 'button');
          if (buttonAncestor) {
            buttonAncestor.click();
            return;
          }
        }
    
        var button = event.currentTarget;
        var buttonIndex = this.topLevelNodes.indexOf(button);
        var buttonExpanded = button.getAttribute('aria-expanded') === 'true';
        this.toggleExpand(buttonIndex, !buttonExpanded);
      }
    
      // Keyboard events su toggle button
      onButtonKeyDown(event) {
        var targetButtonIndex = this.topLevelNodes.indexOf(document.activeElement);
    
        if (event.key === 'Escape') {
          this.toggleExpand(this.openIndex, false);
        }
        else if (
          this.useArrowKeys &&
          this.openIndex === targetButtonIndex &&
          event.key === 'ArrowDown'
        ) {
          event.preventDefault();
          this.controlledNodes[this.openIndex].querySelector('a').focus();
        }
        else if (this.useArrowKeys) {
          this.controlFocusByKey(event, this.topLevelNodes, targetButtonIndex);
        }
      }
    
      // Keyboard events su link semplici
      onLinkKeyDown(event) {
        var targetLinkIndex = this.topLevelNodes.indexOf(document.activeElement);
        if (this.useArrowKeys) {
          this.controlFocusByKey(event, this.topLevelNodes, targetLinkIndex);
        }
      }
    
      // Keyboard events nel submenu
      onMenuKeyDown(event) {
        if (this.openIndex === null) {
          return;
        }
    
        var menuLinks = Array.prototype.slice.call(
          this.controlledNodes[this.openIndex].querySelectorAll('a')
        );
        var currentIndex = menuLinks.indexOf(document.activeElement);
    
        if (event.key === 'Escape') {
          this.topLevelNodes[this.openIndex].focus();
          this.toggleExpand(this.openIndex, false);
        }
        else if (this.useArrowKeys) {
          this.controlFocusByKey(event, menuLinks, currentIndex);
        }
      }
    
      // Apre/chiude submenu
      toggleExpand(index, expanded) {
        // Chiudi altri submenu aperti
        if (this.openIndex !== index) {
          this.toggleExpand(this.openIndex, false);
        }
    
        if (this.topLevelNodes[index]) {
          this.openIndex = expanded ? index : null;
          this.topLevelNodes[index].setAttribute('aria-expanded', expanded);
          this.toggleMenu(this.controlledNodes[index], expanded);
        }
      }
    
      // Mostra/nascondi menu
      toggleMenu(domNode, show) {
        if (domNode) {
          domNode.style.display = show ? 'block' : 'none';
        }
      }
    
      // Abilita/disabilita arrow keys
      updateKeyControls(useArrowKeys) {
        this.useArrowKeys = useArrowKeys;
      }
    }
    
    /* Initialize Disclosure Menus */
    window.addEventListener('load', function () {
      // Toggle per mobile menu
      const navListToggle = document.querySelector('.rtds-nav-list-toggle');
      if (navListToggle) {
        navListToggle.addEventListener('click', function() {
          const currentState = this.getAttribute('aria-expanded') === 'true';
          this.setAttribute('aria-expanded', (!currentState).toString());
        });
    
        // Mantieni menu aperto durante scroll up su mobile
        let lastScrollTop = 0;
        window.addEventListener('scroll', function() {
          const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
          if (scrollTop < lastScrollTop && navListToggle.getAttribute('aria-expanded') === 'true') {
            navListToggle.setAttribute('aria-expanded', 'true');
          }
          lastScrollTop = scrollTop;
        });
      }
    
      // Inizializza tutti i menu con dropdown
      var dropdownMenus = document.querySelectorAll('.has-nav-dropdown');
      var disclosureMenus = [];
    
      for (var i = 0; i < dropdownMenus.length; i++) {
        disclosureMenus[i] = new submenuDisclosure(dropdownMenus[i]);
      }
    
      // Optional: switch per abilitare/disabilitare arrow keys
      var arrowKeySwitch = document.getElementById('arrow-behavior-switch');
      if (arrowKeySwitch) {
        arrowKeySwitch.addEventListener('change', function () {
          var checked = arrowKeySwitch.checked;
          for (var i = 0; i < disclosureMenus.length; i++) {
            disclosureMenus[i].updateKeyControls(checked);
          }
        });
      }
    }, false);
  • URL: /components/raw/side-navigation/side-navigation.js
  • Filesystem Path: components/04-organisms/side-navigation/side-navigation.js
  • Size: 7.8 KB

Sidebar navigation component

Description

Component for sidebar navigation. It can be used as multipage navigation or as anchor navigation.

For multipage navigation, current page link must have aria-current=”page” attribute.

It can be displayed using a label (that could be the section title or page title if anchor), or a context label ( section title or page title if anchor) and a label (eg “Indice della pagina”, “Indice della sezione”), if more context is needed and visually, a more highlighted title is needed.

Responsive

For responsive behaviour, the component has a toggle button, hidden on desktop. Button label depends on the context label and navigation label: if a context label is present, it will be used as button label altogheter with the navigation label, otherwise the navigation label will be used, as shown in the examples.

COMPLETE EXAMPLE

In the complete example, there are:

  • label context and navigation label: label context is used when a hightlighted section title is needed, plus another label eg “indice della pagina”, “indice della sezione”; in the markup, they are the same element with different visual styles.
  • two current page, just for showing how configurations work, actually there will be just one is-current/current-page item.

Configurations - for Nunjuck Development

Configurations:

  • labelContext: ‘’ - gives the context for the side navigation, it can ben the page title or the section title
  • label: ‘’ - Navigation title - used also for accessibility
  • labelId: ‘’ - used for accessibility - in arialabelledby attribute on nav
  • hiddenLabel: true - hide visually navigation title, but maintains it for screen readers
  • id: ‘’ - nav id if needed
  • itemClasses: ‘’ - class for first level item
  • firstLevelLinkClasses: ‘’ - classes for first level link text
  • subnavClasses: ‘’ - classes for submnenu list
  • subnavLinkClasses: ‘’ - classes for submenu links
  • firstLevels: set navigation first levels:
    • label: ‘’ - item label
    • iconLeft: id - id for eventual left icon
    • iconRight: id - id for eventual right icon - default is mini–chevron-down
    • isCurrentTrail: true - if trail is current (first level or child item is current section)
    • isCurrentPage: true - if item is current page, set class and aria-current on link
    • subItems: settings for child items
      • subitemLabel: ‘’ - label for item
      • isCurrentPage: true - if item is current page, set class and aria-current on link