Navigation/Dropdown-Menü
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 Touch-Geräten ohne Maus funktionieren
- auch mit den Pfeil- oder Tab-Tasten anwählbar sein
Und das werden wir alles berücksichtigen!
Inhaltsverzeichnis
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.
<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 Kindelement 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.
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 Kind-Kombinator nav > ul selektiert.
In den folgenden CSS-Beispielen erhalten die Listen der zweiten Ebene die Klasse submenu.
Submenü ausblenden
Kennzeichen eines Dropdown-Menüs ist, dass das Submenü im Ausgangszustand 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.
/* 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 mit der Tabulatortaste erreichbar sind.
Wenn diese 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?
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. Dies wäre mit has() mittlerweile möglich.
Achtung!
Man kann die bis jetzt vorgestellten Navigationen mit der Maus ansteuern und bedienen. Mit der Tastatur kann man 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 Geschwister-Kombinator
a:hover ~ .submenuselektiert, 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?
Viele Anfänger versuchen in ihren Webseiten mit HTML und CSS auszukommen. Dies ist auch völlig korrekt:- HTML zeichnet den Inhalt möglichst semantisch und sparsam aus.
- CSS sorgt für die Gestaltung
- 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 Maus, Touch und Tastatur benutzbar machen.[1]
Ziele sind:
- HTML: verschachtelte Listen mit bis zu drei Ebenen ohne Klassen
- CSS: Auf schmalen Viewports wird das Menü vertikal, auf breiteren Viewports horizontal angeordnet.
- JavaScript: Untermenüs erhalten automatisch Buttons zum Auf- und Zuklappen.
- Links und Buttons sind mit der Tab-Taste ↹ erreichbar.
- Die Buttons können mit ⏎, Space oder per Klick bedient werden.
- Esc oder das Verlassen der Navigation schließt geöffnete Untermenüs.
Button erzeugen
Grundproblem der CSS-basierten Variante ist, dass ein Link mit Untermenü zwei Aufgaben hätte: Er müsste einerseits das Untermenü sichtbar machen und andererseits auf die Zielseite führen. Mit Maus und Hover ist das noch möglich, mit Touch und Tastatur wird diese doppelte Interaktion aber unzuverlässig.
Deshalb erhält jedes Listenelement, das eine direkte Unterliste enthält, zusätzlich einen Button. Der Link bleibt ein normaler Link; der Button öffnet und schließt das zugehörige Untermenü.
<li>
<a href="#">Seite 2</a>
<ul>
<li><a href="#">Seite 2a</a></li>
<li><a href="#">Seite 2b</a></li>
</ul>
</li>
Die Buttons werden erst beim Laden der Seite eingefügt. Dadurch bleibt das HTML übersichtlich und die Navigation funktioniert als einfache Linkliste, falls JavaScript nicht ausgeführt wird.
nav.querySelectorAll('li > ul').forEach((submenu, submenuIndex) => {
const submenuId = `submenu-${navIndex + 1}-${submenuIndex + 1}`;
submenu.id = submenu.id || submenuId;
submenu.insertAdjacentHTML('beforebegin', `
<button type="button" aria-expanded="false" aria-controls="${submenu.id}">
<span class="visually-hidden">Untermenü auf- und zuklappen</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"></path>
</svg>
</button>
`);
});
aria-expanded beschreibt den aktuellen Zustand des Buttons. Mit aria-controls wird der Button dem Untermenü zugeordnet, das er steuert. type="button" verhindert, dass der Button in einem Formular versehentlich ein Absenden auslöst.
Obwohl der Button nur einen Pfeil anzeigt, enthält er einen erklärenden Text. Die Klasse visually-hidden versteckt diesen Text visuell; für Screenreader bleibt er zugänglich. Ein aria-label würde zwar vorgelesen, aber nicht zuverlässig automatisch übersetzt.[2]
.visually-hidden {
position: absolute !important;
clip-path: inset(50%) !important;
padding: 0 !important;
border: 0 !important;
height: 1px !important;
width: 1px !important;
overflow: hidden !important;
}
Siehe auch:
Auf- und Zuklappen
Das JavaScript lauscht innerhalb der Navigation auf Klicks. Wird ein Button mit aria-expanded angeklickt, wird sein Zustand gewechselt und das zugehörige Untermenü über die Klasse visible ein- oder ausgeblendet.
nav.addEventListener('click', event => {
const button = event.target.closest('button[aria-expanded]');
if (!button || !nav.contains(button)) {
return;
}
toggleSubmenu(button);
});
function toggleSubmenu(button) {
const isOpen = button.getAttribute('aria-expanded') === 'true';
setSubmenuState(button, !isOpen);
}
function setSubmenuState(button, isOpen) {
const submenu = document.getElementById(button.getAttribute('aria-controls'));
if (!isOpen) {
closeSubmenus(submenu);
}
button.setAttribute('aria-expanded', String(isOpen));
submenu.classList.toggle('visible', isOpen);
}
Wird ein Menü geschlossen, werden zuerst alle geöffneten Untermenüs darunter geschlossen. So bleiben keine verwaisten Untermenüs sichtbar, wenn ein übergeordnetes Menü eingeklappt wird.
function closeSubmenus(root, keepTarget = null) {
const buttons = root.querySelectorAll('button[aria-expanded="true"]');
buttons.forEach(button => {
if (!keepTarget || !button.closest('li').contains(keepTarget)) {
setSubmenuState(button, false);
}
});
}
Mit Esc werden alle geöffneten Untermenüs geschlossen. Beim Wechsel des Tastaturfokus bleiben nur die Menüs offen, in denen sich der aktuelle Fokus befindet. Verlässt der Fokus die Navigation, wird ebenfalls alles geschlossen.
Information: Pfeil-Navigation
Bei einer Tastaturbedienung sollte man auch mit den Pfeiltasten navigieren können. Normalerweise würde man mit rechter Pfeiltaste zum nächstenli der selben Ebene; mit Pfeiltaste unten in die nächste Unterliste gehen. Bei unserem responsiven Dropdown stehen wir vor dem Problem, dass es ab 45em Breite von column (senkrecht) auf row (waagerecht) umschaltet und dann die Pfeilrichtungen geändert werden müssten.Aus diesem Grund verzichten wir auf eine Implementierung, da die Tab-Steuerung die gewohntere Konvention ist.
Matthias Scharwies (Diskussion) 06:24, 2. Mär. 2023 (CET)Kopiervorlage für Ungeduldige
Dieses Skript fügt sowohl die Interaktivität als auch das dafür nötige CSS in deine 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 eigene Skriptdatei extern einzubinden.
Speichere 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.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/VoiceOver-Fix, siehe 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.
Wenn du die Farben ändern willst, entferne die Festlegung im
:root{}-Regelsatz und lege sie in deinem Stylesheet fest.
Weblinks
- ↑ Eine Vorgänger-Version von Beatovich aus dem Jahr 2011 findest du bei web.archive.org: [Eine zugängliche Multilevel-Dropdown-Navigation
- ↑ auto-translation of `aria-label` - Do not rely on it! (Adrian Roselli on twitter)