Navigation/Dropdown-Menü
- 30min
- einfach
- Grundkenntnisse in
● Navigation/Grundstruktur
● Links/Gestaltung mit CSS
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!
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 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.
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.
/* 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?
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!
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?
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 alle benutzbar machen.[1]
Ziele sind:
- HTML: verschachtelte Listen mit bis zu drei Ebenen ohne Klassen
- CSS: Auf schmalen Viewports vertikal - auf genügend breiten Viewports horizontal angeordnet.
- 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!)
- Sie können mit der Tab-Taste ↹ angetabbt und dann mit ⏎ oder Space ausgewählt werden.
- ESC oder ein Weitertabben schließt das aufgeklappte Menü
- die oben vorgestellten Dreiecke werden interaktive Buttons
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.
<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:
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:
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:
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 …
- das aria-expanded-Attribut von
true
auffalse
und umgekehrt getoggelt - 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.
Information: Pfeil-Navigation
Bei einer Tastaturbedienung sollte man auch mit den Pfeiltasten navigieren können. Normalerweise würde man mit rechter Pfeiltast zum nächsten li der selben Ebene; mit Pfeiltaste unten in die nächste Unterliste gehen. Bei unserem responsivem 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.
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.
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.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.
Wenn Du die Farben ändern willst, entferne die Festlegung im
:root{}
-Regelsatz und lege sie in Deinem Stylesheet fest.
Quellen
- ↑ Eine Vorgänger-Version von Beatovich aus dem Jahr 2011 finden Sie in unserem Museum: Museum/Eine zugängliche Multilevel-Dropdown-Navigation
- ↑ Aria-Controls is Poop (heydonworks.com)
- ↑ auto-translation of `aria-label` - Do not rely on it! (Adrian Roselli on twitter)
- ↑ https://github.com/h5bp/html5-boilerplate/blob/main/dist/css/style.css
- ↑ Selfhtml Forum: Buttons mit visually-hidden Elementen in einem horizontalen Scrollcontainer
- Menus & Menu Buttons 10 May, 2017 (inclusive-components.design)