Navigation/Dropdown-Menü

Aus SELFHTML-Wiki
< Navigation(Weitergeleitet von Quernavigation)
Wechseln zu: Navigation, Suche

Es ist eine Konvention, dass Inhaltsverzeichnisse am Anfang eines Buches stehen. Auch bei Webseiten sucht der Nutzer von oben links nach unten rechts und erwartet die Navigation oben.

In diesem Kapitel lernst Du die Funktionsweise einer Dropdown-Navigation kennen, die sich mit Maus und Tastatur auf- und zuklappen lässt. Dies war früher ein Paradebeispiel für die Power von CSS und dem durch die Maus ausgelösten Hover. Heute haben sich die Anforderungen geändert. Eine solche Navigation muss sich …

  • responsiv an alle Viewports anpassen und die Links auf kleinen Bildschirmen untereinander anordnen.
  • auch auf Touchgeräten ohne Maus funktionieren
  • auch mit den Pfeil- oder Tabtasten anwählbar sein

Und das werden wir alles berücksichtigen!

Dropdown nur mit CSS

Navigationen bei größeren Webprojekten enthalten in vielen Fällen keine sequenzielle Auflistung von Links, sondern verteilen diese auf verschiedene Navigationsebenen.

HTML: verschachtelte Listen

Zur logischen Abbildung dieser Struktur eignen sich verschachtelte Listen.

verschachtelte Listen (ohne CSS) ansehen …
<nav>
  <ul>
    <li><a href="#">Seite 1</a></li>

    <li><a href="#">Seite 2</a>
      <ul>
        <li><a href="#">Seite 2a</a></li>
        <li><a href="#">Seite 2b</a></li>
      </ul>
    </li>

    <li aria-current="page"><a href="#">aktuelle Seite</a></li>

    <li><a href="#">Seite 4</a>
      <ul>
        <li><a href="#">Seite 4a</a></li>
        <li><a href="#">Seite 4b</a></li>
        <li><a href="#">Seite 4c</a></li>
      </ul>
    </li>

    <li><a href="#">Seite 5</a></li>
    <li><a href="#">Seite 6</a></li>
  </ul>
</nav>

In das zweite und vierte li-Element ist hier eine weitere Liste eingefügt, da Listen wie ul oder ol selbst keine anderen Elemente als li als direktes Nachfahrenelement enthalten dürfen.
Selbst ohne CSS wird die Struktur von verschachtelten Listen über die Browser-Voreinstellungen sehr gut deutlich. Mit der Tastatur können alle Links ausgewählt werden.


verschachtelte Listen (mit CSS) ansehen …
nav > ul {
 ...
} 

@media (min-width: 45em) {
  nav > ul {	
    ...  
  }
}

Die Formatierung der ersten Navigationsebene bleibt erhalten. Damit nur die Elemente der ersten Ebene angesprochen werden, werden sie über den Kindselektor nav > ul selektiert.

Die Listen der zweiten Ebene erhalten eine Klasse submenu.

Submenü ausblenden

Kennzeichen eines Dropdown-Menüs ist, dass das Submenü im Originalzustand ausgeblendet ist und erst beim Auswählen mit der Maus über die Pseudoklasse :hover oder mit der Tastatur über :focus selektiert und sichtbar gemacht wird.

Navigationsleiste mit mehreren Ebenen ansehen …
/*     submenu navigation links      */
nav .submenu { 
  visibility: hidden;  
  height: 0;
  z-index: 1000; 
}
nav .submenu li { 
  display: block; 
  width: 15em;
}
 
/**     Show the submenu on hover, focus     **/
nav li:hover  .submenu,
nav li:active  .submenu, 
nav li:focus  .submenu { 
  visibility: visible;
  height: auto;
}

Die Listen der zweiten Ebene erhalten eine Klasse submenu, die im Normalzustand mit visibility: hidden ausgeblendet ist.

Listenelemente, die als Kindelement ein Submenü beinhalten, erhalten einen tabindex="0", damit sie antabbar sind.

Wenn Sie mit der Maus oder über die Tastatur ausgewählt werden, wird das Submenü eingeblendet; die folgenden Listenpunkte rutschen nach unten.

Wie kann ein Nutzer aber erkennen, dass ein (jetzt ausgeblendetes) Submenü vorhanden ist?

Pfeile, um verborgene Inhalte anzuzeigen ansehen …
nav .subtitle::after {
  position: absolute;
  content: " ▶";
  color: currentColor;
  display: inline-block;
  width: 2em;
  right:0;
}

nav .subtitle:hover::after, nav .subtitle:focus::after{
	content:" ▼";
}

Menüpunkte, die Untermenüs enthalten werden, erhalten nun after-Pseudoelemente. Diese erhalten über die content-Eigenschaft Rechtspfeile, die absolut am rechten Rand positioniert werden. Diese werden mit currentColor in der aktuellen Textfarbe eingefärbt.

Im Beispiel haben die Menüpunkte die Klasse .subtitle erhalten, da CSS bisher keine nachfolgenden Elemente selektieren konnte. Diese wäre mit has() mittlerweile möglich.

Achtung!

Das Beispiel funktioniert nicht wie gewünscht.

Sie können die bis jetzt vorgestellten Navigationen mit der Maus ansteuern und bedienen. Mit der Tastatur können Sie nur die dargestellten, aber nicht die ausgeblendeten Verweise ansteuern.

  • Man kann zwar die Submenüs über das tabindex des li-Elements sichtbar machen, die Listeneinträge jedoch nicht mit der Tastatur durchtabben.
  • Wenn man über den Geschwisterselektor a:hover ~ .submenu selektiert, wird das Submenu ein- , sobald der erste Listenpunkt angewählt wird, aber doch wieder ausgeblendet.

Eine touch- und tastaturbedienbare Variante lässt sich nur mit JavaScript erreichen: inklusives Dropdown-Menü

--Matthias Scharwies (Diskussion) 08:09, 31. Okt. 2017 (CET)

Wäre es nicht besser, wenn man die Pfeile anklicken und so das Submenü öffnen könnte?

Zusammenspiel von HTML, CSS und HTML
Viele Anfänger versuchen in ihren Webseiten mit HTML und CSS auszukommen. Dies ist auch völlig korrekt:
  1. HTML zeichnet den Inhalt möglichst semantisch und sparsam aus.
  2. CSS sorgt für die Gestaltung
  3. Mit einigen Zeilen JavaScript können wir aber unsere Navigation um Zusatzfunktionen erweitern, damit sie für alle Nutzer wirklich benutzbar ist!

Dabei muss das HTML nicht verändert werden. JavaScript wird in einer zusätzlichen Schicht als progressive enhancement (schrittweise Verbesserung) verwendet.

inklusives Dropdown-Menü

Dieses Beispiel erweitert die CSS-basierte Variante um einige Zusatzfunktionen, die das Dropdown-Menü für alle benutzbar machen.[1]

Ziele sind:

  1. HTML: verschachtelte Listen mit bis zu drei Ebenen ohne Klassen
  2. CSS: Auf schmalen Viewports vertikal - auf genügend breiten Viewports horizontal angeordnet.
  3. JavaScript Bedienung mit Maus, Touch und Tastatur
    • die oben vorgestellten Dreiecke werden interaktive Buttons
      • Sie können mit der Tab-Taste angetabbt und dann mit oder Space ausgewählt werden.
        (Shift+ führt zurück!)
    • ESC oder ein Weitertabben schließt das aufgeklappte Menü


Button erzeugen

Grundproblem der CSS-basierten Variante ist die Möglichkeit mit der Maus über einem Listenelement zu hovern (und so das Untermenü sichtbar zu machen) und es anschließend anzuklicken, um den Link auszulösen. Während es auf Touchgeräten schwierig ist, zwischen einem sanften Tab und einem festen Klick zu unterscheiden, ist diese doppelte Interaktion mit der Tastatur unmöglich.

Deshalb fügen wir nun jedem Listenelement, dem auf den Link eine Unterliste (=Untermenü) folgt, einen Button hinzu, der diese Funktionalität ermöglicht.

HTML: Button zum Öffnen des Untermenüs ansehen …
 
<nav>
  <ul>
    <li><a href="#">Seite 2</a>
      <button aria-expanded="false">
        <span class="visually-hidden">Untermenü aufklappen</span>
      </button>
      <ul>
        <li><a href="#">Seite 2a</a></li>
        <li><a href="#">Seite 2b</a></li>
      </ul>
    </li>
...

Zwischen dem Link und dem Untermenü befindet sich nun ein Button. Durch das aria-expanded-Attribut wird angezeigt, dass er das folgende Element auf- und zuklappen kann. (Die explizite Zuordnung über ein aria-controls-Attribut ist laut Heydon Pickering bei aufeinanderfolgenden Elementen nicht nötig.[2]).

Obwohl der Button einen Pfeil anzeigen soll, wird hier in einem span ein Hinweistext notiert, der über die Klasse visually-hidden auf Bildschirmen versteckt wird. Ein aria-label würde zwar von Screenreadern vorgelesen, aber nicht übersetzt.[3]

Das folgende JavaScript erzeugt nun unsere Buttons:

JavaScript: Buttons erzeugen ansehen …
 
	const submenus = document.querySelectorAll('nav li > ul');
	for(let submenu of submenus) {
		submenu.classList.add('submenu');
		submenu.insertAdjacentHTML('beforebegin', `
			<button aria-expanded="false">
				<span class="visually-hidden">Untermenü aufklappen</span>
				<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'>
				  <path d='M0.3,0.1 0.3,0.9 0.8,0.5z' />
				</svg>
			</button>
		`);
	}

Mit querySelectorAll('nav li > ul') können wir nun alle Submenüs, die direkte Kinder eines li sind, finden. Die gefundenen Ergebnisse sind nun in einer node list, die mit einer for..of-Schleife jetzt für jeden Treffer den oben besprochenen Button mit insertAdjacentHTML einfügt.

Und nun wird der Text des Buttons durch einen SVG-Pfeil ersetzt und das Untermenü ausgeblendet:

CSS: Buttontext ausblenden ansehen …
 
button {
 	position: relative;     /* WICHTIG! */
}
.visually-hidden {
	position: absolute !important;
	clip: rect(1px, 1px, 1px, 1px) !important;
	padding: 0 !important;
	border: 0 !important;
	height: 1px !important;
	width: 1px !important;
	overflow: hidden !important;
}

[aria-expanded] {
  position: absolute;
  ...
  background: transparent;
  outline:transparent;
  border: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath d='M0.3,0.1 0.3,0.9 0.8,0.5z' fill='white'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
}

Die Klasse visually-hidden versteckt den Text visuell[4], für Screenreader ist er aber weiterhin erreichbar, die so eine Anleitung erhalten. Sehende werden durch den als Hintergrundbild angezeigten Pfeil geleitet. Es ist wichtig, dem Button, der dieses span-Element enthält, die position:relative;-Eigenschaft zu geben, weil sich das visuell versteckte span-Element andernfalls störend auf das Seitenlayout auswirken kann.[5]

Siehe auch:

Auf- und Zuklappen

Beim Laden des Dokuments wurden nun die Buttons hinzugefügt. Der Benutzer kann nun entscheiden, ob er den Link selbst oder den Button auswählt.

Das JavaScript wird um einen addEventListener erweitert, der auf Klicks lauscht:

JavaScript: Auf- und Zuklappen ansehen …
 
	document.documentElement.addEventListener('click', event => {
		if (event.target.tagName == 'BUTTON' && event.target.hasAttribute('aria-expanded')) {
			event.target.setAttribute('aria-expanded', event.target.getAttribute('aria-expanded') != 'true');
			event.target.nextElementSibling.classList.toggle('visible');
			event.target.parentNode.childNodes.classList.toggle('visible');
		}
	});
	
	document.addEventListener('keyup', (event) => {
			if (event.key === 'Escape')  {
				 hideSubmenu();
			}
  		if ((event.key === 'Tab') && (!event.target.closest('.visible')) ) {
					hideSubmenu();
	    	
			}
	});
	
	function hideSubmenu() {
				let elements = document.querySelectorAll('.visible');
      	elements.forEach(function(element) {
        	element.classList.remove('visible');
				});	
	}

Wenn jetzt ein Click-Event ausgelöst wird, wird bei jedem geklickten Element, das ein Button ist und ein aria-exanded-Attribut hat …

  1. das aria-expanded-Attribut von true auf false und umgekehrt getoggelt
  2. das mit nextElementSibling ermittelte Untermenü um eine Klasse visible ergänzt

Erwartetes Verhalten ist auch das Schließen eines geöffneten Menüs mit der ESC-Taste. So überprüft ein EventListener das Keyup-Event, das jedesmal feuert, wenn eine Keyboard-Taste losgelassen wird.

Wenn die Tab-Taste nicht auf einen Link oder Button in einer geöffneten Liste kommt, werden eventuell geöffnete Submenüs geschlossen. Dies wird erreicht, indem mit !event.target.closest('.visible') mit Element.closest() geschaut wird, ob eines der Elternelemente des Elements, in dem das Ereignis ausgelöst wurde, die Klasse visible hat.

Auch wenn ESC gedrückt wird, wird die Funktion hideSubmenu aufgerufen. Sie

  • entfernt bei allen Elemente mit visible diese Klasse wieder
  • setzt das aria-expanded-Attribut bei den Buttons wieder auf false

→ das Submenü wird wieder eingeklappt.


Kopiervorlage für Ungeduldige

Dieses Script fügt sowohl die Interaktivität als auch das dafür nötige CSS in Ihre Webseiten ein. Da es auf allen (Unter)-seiten verfügbar sein sollte, ist es empfehlenswert, dies nicht wie im Beispiel in den head des Dokuments, sondern als eigenes Script-Dokument extern einzubinden.

Empfehlung:
Speicher das Script als eigenes Dokument mit der Dateiendung menu.js ab und binde es in jede deiner Seiten ein:
<script src="js/menu.js"></script>
Passe den Pfad an Deine Dateistruktur an.
fertiges Beispiel ansehen …
document.addEventListener('DOMContentLoaded', function () {


		dropdownExtension();

	
function dropdownExtension () {
	const submenus = document.querySelectorAll('nav li > ul');
	for(let submenu of submenus) {
		submenu.classList.add('submenu');
		submenu.insertAdjacentHTML('beforebegin', `
			<button aria-expanded="false">
				<span class="visually-hidden">Untermenü aufklappen</span>
				<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'>
				  <path d='M0.3,0.1 0.3,0.9 0.8,0.5z' />
				</svg>
			</button>
		`);
	}
	document.documentElement.addEventListener('click', event => {
		if (event.target.tagName == 'BUTTON' && event.target.hasAttribute('aria-expanded')) {
			event.target.setAttribute('aria-expanded', event.target.getAttribute('aria-expanded') != 'true');
			event.target.nextElementSibling.classList.toggle('visible');
			event.target.parentNode.childNodes.classList.toggle('visible');
		}
	});
	
	document.addEventListener('keyup', (event) => {
			if (event.key === 'Escape')  {
				 hideSubmenu();
			}
  		if ((event.key === 'Tab') && (!event.target.closest('.visible')) ) {
					hideSubmenu();
	    	
			}
	});
	
	function hideSubmenu() {
				let elements = document.querySelectorAll('.visible');
      	elements.forEach(function(element) {
        	element.classList.remove('visible');
				});	
	}
}

 const cssRules = `
 nav > ul {
  list-style:none;
	// Safari hack, see https://www.scottohara.me/blog/2019/01/12/lists-and-safari.html
	list-style: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E");  
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  position:relative;
} 

nav li {
  margin: 0.5em;
  padding: 0;
  font-size: 1.5rem;  
  flex: 1 1 0%;  
  position: relative;
}

@media (min-width: 45em) {
  nav > ul {	
    flex-direction: row; 
	height: 3em; 
  }
  nav li {
    flex: 1;
		flex-basis: 15em;
    flex-grow: 0;
    flex-shrink: 0;		
		font-size: 1em;
  }  
}

nav a {
  display: block;
  padding: 0.4em;
  text-decoration: none; 
  font-weight: bold;
  text-align: center;
  border: thin solid var(--background-color);
  border-radius: .5em;
  color: var(--link-color);
  --link-color: gold; 
  background-color: var(--background-color); 
  transition: all .25s ease-in;	 
  position: relative;  
}

nav li[aria-current] a {
  background-color: var(--accent2-color); 
  color: var(--accent1-color);
}

	
nav a:focus,
nav a:hover,
nav li[aria-current] a:focus,
nav li[aria-current] a:hover {    
  color: var(--background-color); 
  background-color: var(--accent1-color);
}	

/*     submenu navigation links      */
nav ul ul { 
  visibility: hidden;  
  height: 0;
  z-index: 1000; 
}
nav ul li { 
  display: block; 
  /* width: 15em; */
}
 
/*     Show the submenu on hover, focus     */
nav li:hover > ul {
  visibility: visible;
  height: auto;
}
nav li:focus-within > ul, 
.visible  { 
  visibility: visible;
  height: auto;
}

[aria-expanded] {
position: absolute;
right: 0.5em;
top: 0.25em;
width:2em;
height:2em;
padding: 0;
color: var(--accent1-color);
background: var(--background-color); /* damit er bei nav a:focus noch sichtbar ist */ 
outline:transparent;
border: thin solid var(--background-color);
border-radius: 0.3em;
}

[aria-expanded] svg {
	pointer-events: none;
  fill: var(--accent1-color);
	border-radius: 0.3em;	
	z-index:10;
}

nav li:hover [aria-expanded] svg {
	background: var(--accent1-color);
	fill: var(--background-color);
}

nav li:focus [aria-expanded] svg {
	background: var(--background-color);
  fill: var(--accent1-color);
}

[aria-expanded]:focus svg,
[aria-expanded]:hover svg{
  fill: var(--background-color);
	background: var(--accent1-color);
 
}

[aria-expanded="true"] svg {
  transform: translate(0,0) rotate(90deg);
}

.visually-hidden,
[visually-hidden="true"] {
	position: absolute !important;
	clip-path: rect(1px, 1px, 1px, 1px) !important;
	padding: 0 !important;
	border: 0 !important;
	height: 1px !important;
	width: 1px !important;
	overflow: hidden !important;
}

	
:root {
	--background-color: midnightblue;
	--accent1-color: gold;
	--accent2-color: darkred;	
	--text-color: black;
}`;

	const stylesheet = document.createElement('style');
	stylesheet.textContent = cssRules;
	document.querySelector('html > head').appendChild(stylesheet);		


});

Das CSS befindet sich in der CSSRules-Variablen und wird beim Initialisieren in ein dynamisch erzeugtes style-Element eingefügt.

Empfehlung:
Wenn Du die Farben ändern willst, entferne die Festlegung im :root{}-Regelsatz und lege sie in Deinem Stylesheet fest.


Quellen

  1. Eine Vorgänger-Version von Beatovich aus dem Jahr 2011 finden Sie in unserem Museum: Museum/Eine zugängliche Multilevel-Dropdown-Navigation
  2. Aria-Controls is Poop (heydonworks.com)
  3. auto-translation of `aria-label` - Do not rely on it! (Adrian Roselli on twitter)
  4. https://github.com/h5bp/html5-boilerplate/blob/main/dist/css/style.css
  5. Selfhtml Forum: Buttons mit visually-hidden Elementen in einem horizontalen Scrollcontainer