Wie man auf Mobilgeräten ungewolltes Website-Scrolling verhindert

Hallo Alle!

Bevor ich mit dem Thema dieses Posts anfange, muss ich ein paar Sachen erwähnen:

  1. Dieser Post ist ein Test-Post. Ich habe ihn geschrieben, um ein paar Sachen mit dieser Website auszuprobieren, nicht weil das Thema wirklich interessant ist.
  2. Dieser Post trifft nur auf die aktuelle Version dieser Website zu (siehe Veröffentlichungsdatum oben). Wenn du ihn liest, kann er schon komplett hinfällig sein.

Da das nun klar ist, los geht's!

Das Problem

Wenn du diese Website sowohl auf sowohl einem Mobil-, als auch einem Desktop-Gerät mal verwendet hast, dann wird dir aufgefallen sein, dass sie sich an die Größe deines Geräts anpasst. Das nennt man Responsive Design, und auch wenn es wichtig ist, ist es im heutigen Web nicht wirklich beeindruckend.

Ein Teil dieser Seite, das massiv davon beeinflusst wird, ist die Navigation. Wenn du einen großen Bildschirm hast, dann werden die einzelnen Navigationsziele direkt in der Kopfzeile angezeigt. Wenn du andererseits einen kleinen Bildschirm verwendest, dann sind sie hinter einem Hamburger-Menü versteckt. Das gleiche gilt auch für die Seitenleiste auf großen Bildschirmen, die am Handy über den Pfeil-Knopf geöffnet werden kann.

Wenn eines dieser beiden Menüs geöffnet ist, dann bedeckt es den gesamten Bildschirm. Wie du es erwarten würdest, kannst du dann nicht mit dem Inhalt dahinter interagieren. Vermutlich denkst du, dass sich der Browser automatisch darum kümmert.

Leider ist dem so nicht. Als Website-Autor muss man daran denken, dieses Verhalten zu implementieren. In diesem Post erkläre ich nun, wie ich das gemacht habe.

Wie die Menüs funktionieren

Bevor ich das jedoch machen kann, muss ich erklären wie die Menüs funktionieren.

Während JavaScript in (so gut wie) allen Browsern verfügbar ist, gibt es Leute, die es deaktivieren. Obwohl ich mit dieser Entscheidung dieser Leute nicht übereinstimme, möchte ich trotzdem, dass sie meine Seite verwenden können. Daher habe ich nach einem Weg gesucht, Menüs ohne JavaScript umzusetzen und habe ihn auf Stack Overflow gefunden.

Vereinfacht gesagt haben wir eine Checkbox, ein Label und ein <div> mit dem Menüinhalt. Wir machen die Checkbox und das Menü unsichtbar. Wenn die Checkbox abgehackt ist (der Benutzer hat auf das Label getippt), machen wir den Inhalt wieder sichtbar.

Ein kleines Code-Beispiel:

1<input type="checkbox" name="schalter" id="schalter" />
2<label for="schalter">Zeigen/Verbergen-Knopf</label>
3<div id="menu">Inhalt</div>
1#input[type=checkbox],
2#menu {
3 display: none;
4}
5
6#schalter:checked ~ #menu {
7 display: block;
8}

Versuch 1

Mit diesem Vorwissen über die Funktionsweise der Menüs können wir uns nun daran machen, das eigentliche Problem zu lösen.

Im Prinzip müssen wir nur überprüfen ob eine der Checkboxes abgehackt ist, und falls dem so sein sollte das Scrollen für den <html>-Tag sperren.

Der folgende Code repliziert die Funktionsweise meiner ersten Implementation. Ich habe ihn aber etwas aufgeräumt, damit ich später die Unterschiede gut zeigen kann.

1function updateScrollLock() {
2 // die Werte der Checkboxes
3 let nav = document.getElementById("nav-toggle").checked;
4 let aside = document.getElementById("aside-toggle").checked;
5
6 // die zur Umsetzung erforderlichen Elemente
7 let html = document.getElementsByTagName("html")[0];
8
9 // die Sperre umsetzen
10 if (nav || aside) {
11 html.style.overflowY = "hidden";
12 } else {
13 html.style = null;
14 }
15}

Auf den ersten Blick scheint dieser Code zu funktionieren. Es gibt jedoch einen Haken.

Die Kopfzeile dieser Website bleibt immer am oberen Bildschirmrand. Die einzige Ausnahme dabei bilden vertikal kleine Geräte (wie z.B. Smartphones im Breitbildmodus), wo sie am oberen Ende der Seite bleibt. Das verursacht nun ein Problem.

Wenn der Benutzer nun auf seinem Handy ein Menü öffnet und es dann auf die Seite dreht, dann hat er plötzlich keine Möglichkeit mehr das Menü zu schließen. Hier ist ein Bild davon, wie es auf meinem Google Pixel 5 ausschaut:

Ein Bildschirmfoto dass das Fehlen der Kopfzeile auf einem Smartphone im Breitbildmodus zeigt.

Versuch 2

Wir können das obige Problem leicht lösen, indem wir die Kopfzeile am oberen Bildschirmrand fixieren, wenn ein Menü geöffnet ist. Wir können dafür die gleiche Methode verwenden wie bei allen anderen Geräten, nur dass wir sie jetzt in JavaScript statt CSS umsetzen müssen.

Auf meiner Seite benutze ich position: sticky; was bedeutet, dass ich auch top: 0; setzen muss.

Im folgenden Snippet habe ich die Unterschiede im Vergleich zu vorher hervorgehoben.

1function updateScrollLock() {
2 // die Werte der Checkboxes
3 let nav = document.getElementById("nav-toggle").checked;
4 let aside = document.getElementById("aside-toggle").checked;
5
6 // die zur Umsetzung erforderlichen Elemente
7 let html = document.getElementsByTagName("html")[0];
8 let header = document.getElementsByTagName("header")[0];
9
10 // die Sperre umsetzen
11 if (nav || aside) {
12 html.style.overflowY = "hidden";
13
14 // die Kopfzeile am Bildschirmrand fixieren
15 header.style.position = "sticky";
16 header.style.top = 0;
17 } else {
18 html.style = null;
19 header.style = null;
20 }
21}

Perfekt! Nun können die Benutzer das Menü immer schließen.

Jedoch gibt es immer noch ein Problem. Wenn jemand die Website in einem kleinen Browser-Fenster am PC öffnet, dann ein Menü öffnet und anschließend das Fenster so weit vergrößert, dass er das Desktop-Layout sieht, dann kann er weder das Menü schließen, noch scrollen. Das müssen wir beheben!

Versuch 3

Jetzt kommen wir zum kompliziertesten Teil. Wir müssen überprüfen, ob der Benutzer die Mobil- oder Desktop-Variante sieht. Dafür gibt es verschiedene Techniken, man könnte etwat window.innerHeight überprüfen.

Allerdings will ich nicht in mehreren Dateien Umbruchpunkte festlegen, daher werden wir ein bereits existierendes Element verwenden. Dieses heißt #aside-toggle-closer und wird im Desktop-Modus zu display: none; gesetzt.

Um die tatsächlichen Werte des Elements zu erhalten, müssen wir es zuerst an window.getComputedStyle() übergeben. element.style enthält nur die Regeln für das jeweilige Element.

Nun können wir überprüfen ob es sich um ein Mobilgerät handelt, bevor wir das Scrollen sperren:

1function updateScrollLock() {
2 // ist der Mobilmodus aktiv?
3 let mobile =
4 window.getComputedStyle(document.getElementById("aside-toggle-closer"))
5 .display != "none";
6
7 // die Werte der Checkboxes
8 let nav = document.getElementById("nav-toggle").checked;
9 let aside = document.getElementById("aside-toggle").checked;
10
11 // die zur Umsetzung erforderlichen Elemente
12 let html = document.getElementsByTagName("html")[0];
13 let header = document.getElementsByTagName("header")[0];
14
15 // die Sperre umsetzen
16 if ((nav || aside) && mobile) {
17 html.style.overflowY = "hidden";
18
19 // die Kopfzeile am Bildschirmrand fixieren
20 header.style.position = "sticky";
21 header.style.top = 0;
22 } else {
23 html.style = null;
24 header.style = null;
25 }
26}

update​ScrollLock() aufrufen

Jetzt müssen wir nur noch die Funktion aufrufen, die wir gerade geschrieben haben. Wir müssen sie in drei Szenarien ausführen:

Wir können eine Funktion dafür schreiben:

1function registerScrollLock() {
2 // beim Öffnen der Seite
3 updateScrollLock();
4
5 // beim Öffnen der Seite
6 window.addEventListener("resize", updateScrollLock);
7
8 // wenn eines der Menüs geöffnet oder geschlossen wird
9 document
10 .getElementById("nav-toggle")
11 .addEventListener("change", updateScrollLock);
12 document
13 .getElementById("aside-toggle")
14 .addEventListener("change", updateScrollLock);
15}

Nun müssen wir nur noch diese Funktion beim Laden der Seite ausführen:

1if (document.readyState != "loading") {
2 registerScrollLock();
3} else {
4 document.addEventListener("DOMContentLoaded", registerScrollLock);
5}

Die Hoffnung für weniger JavaScript

Obwohl diese Scroll-Sperre leicht umzusetzen ist, wie du gesehen hast, mag ich sie nicht.

Wenngleich ich nicht denke, dass der Einsatz von JavaScript perse ein Problem ist, so ist dies doch das einzige Feature dieser Website, dass ich nicht ohne es umsetzen konnte, was mich wirklich nervt. Das bedeutet aber auch, dass ich script-src: 'self' in meiner Content Security Policy haben muss.

Also habe ich nach einer CSS-Alternative gesucht, und auch eine gefunden: :has(). Dadurch sollte es möglich sein, CSS wie dieses hier zu schreiben:

1html:has(#nav-toggle:checked),
2html:has(#aside-toggle:checked) {
3 overflow-y: hidden;
4}

Vielleicht fragst du dich jetzt, warum ich diesen Post geschrieben habe, wenn es doch eine CSS-Lösung gibt. Tja, wenn du die verlinkte Seite bis zum Ende liest, dann wirst du sehen, dass derzeit kein Browser :has() unterstützt. Als ich das herausgefunden habe, wirkte es wirklich wie ein schlechter Scherz.

Also kann ich nichts machen außer warten und hoffen, dass ich dieses eine Script eines Tages von meiner Website entfernen kann.

Da das nun gesagt ist, wünsche ich dir einen schönen Tag!