<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-block rtds-font-medium">Titolo navigazione</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>Titolo navigazione</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": "Titolo navigazione",
"labelId": "sidebarLabel",
"id": "sideNav",
"listId": "sideNavList",
"firstLevels": [
{
"label": "Item 1",
"isCurrentPage": true
},
{
"label": "Item 2"
},
{
"label": "Item 3"
}
]
}
/**
* 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;
}
}
}
}
'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);
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.
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.
In the complete example, there are:
Configurations: