mitmario.dev

Das Menü, das auffährt

CSS Im Browser 4 Min Lesezeit 3 BeispieleLektion 5 von 6

Auf schmalen Bildschirmen klappt ein Menü auf, und dafür braucht es drei Dinge. Einen Auslöser, einen Zustand und einen Übergang zwischen beiden. Zwei davon kennst du längst, das dritte hat einen Haken.

Der Auslöser ohne JavaScript

Es gibt zwei Wege, und beide kommen aus dem HTML-Kurs.

Der erste ist details mit summary. Das Auf- und Zuklappen bringt der Browser mit, du gestaltest nur. Solange es zu ist, liegt der Inhalt allerdings in einem Zustand, in dem sich seine Maße schlecht anfassen lassen. Der Übergang wird dann zur Glückssache.

Der zweite ist eine versteckte Checkbox mit einem label, das auf sie zeigt. Ein Klick auf das Label schaltet die Checkbox um, und im CSS greifst du das mit :checked und dem Geschwisterselektor ~ ab, die du beide schon aus Abschnitt 2 kennst. Das Menü liegt dabei immer im Layout, nur eben zusammengeklappt, und deshalb lässt es sich weich bewegen.

Mit display: none verschwände die Checkbox auch für die Tastatur. Ich nehme deshalb die Technik aus Lektion 4.5, die das Feld winzig macht und beschneidet, es aber im Baum lässt. Das heißt position: absolute, Breite und Höhe 1px und clip-path: inset(50%).

Warum die Höhe nicht funktioniert

Warum die Höhe nicht geht
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

.kopf {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px;
  color: #fafafa;
  background-color: #18181b;
}

.schalter {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
}

.button {
  padding: 8px 16px;
  border-radius: 8px;
  background-color: #3f3f46;
  cursor: pointer;
}

.menue {
  overflow: hidden;
  width: 100%;
  height: 0px;
  transition: height 0.25s ease-out;
}

.schalter:checked ~ .menue {
  height: auto;
}

.menue-inhalt {
  display: grid;
  gap: 4px;
  padding-top: 8px;
}

.menue-inhalt a {
  padding: 8px 0;
  color: #fafafa;
}

.hinweis {
  max-width: 60ch;
  padding: 24px 16px;
  color: #52525b;
}

Naheliegend wäre height: 0 im geschlossenen und height: auto im offenen Zustand, mit einer transition dazwischen. So steht es oft im ersten Anlauf da. Und es klappt hart auf, als wäre der Übergang gar nicht notiert.

Der Grund ist einfach. Eine Animation rechnet Zwischenwerte aus, und dafür braucht sie zwei Zahlen. auto ist keine Zahl. Es ist eine Anweisung an den Browser, selbst nachzumessen, und zwischen 0px und einer Anweisung rechnet er von allein nichts aus. Also überspringt er die Strecke.

Dass die Angabe wirklich dasteht, kannst du nachfragen. In der Eingabezeile der Console antwortet getComputedStyle(document.querySelector('.menue')).transitionProperty mit height, und ...height mit 0px. Der Übergang ist also notiert und die Ausgangshöhe bekannt. Es fehlt nur das andere Ende der Strecke.

Bei display ist es von allein genauso, denn zwischen none und block liegt nichts. Beibringen lässt es sich dem Browser trotzdem, das kommt in den Abschnitten 14 und 15.

Der Weg über die Rasterspur

Von 0fr auf 1fr
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 16px;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  background-color: #fafaf9;
}

.rezept {
  padding: 16px;
  border: 1px solid #d6d3d1;
  border-radius: 8px;
  background-color: #ffffff;
}

.rezept h3,
.rezept p {
  margin: 0 0 8px 0;
}

.schalter {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
}

.zutaten-button {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 4px;
  color: #ffffff;
  background-color: #15803d;
  cursor: pointer;
}

.zutaten {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.25s ease-out;
}

.schalter:checked ~ .zutaten {
  grid-template-rows: 1fr;
}

.zutaten-inhalt {
  overflow: hidden;
}

.zutaten-inhalt ul {
  margin: 12px 0 0 0;
}

.hinweis {
  max-width: 60ch;
  color: #57534e;
}

grid-template-rows: 0fr und grid-template-rows: 1fr sind zwei Zahlen, und dazwischen kann der Browser rechnen, alle großen seit Ende 2022. Mehr braucht es nicht. Der Container wird zum Raster mit genau einer Spur, und die Spur wächst von null auf ihre Inhaltshöhe.

Dafür braucht es ein zusätzliches Element. Der Inhalt liegt in einem Kind, und dieses Kind bekommt overflow: hidden. Ohne diese Angabe schrumpft die Spur gar nicht erst auf null. Der Browser gibt jeder Spur als Mindesthöhe die Höhe ihres Inhalts, und diese Mindesthöhe fällt erst weg, wenn das Kind darin selbst abschneidet. Vergisst du sie, steht das Menü dauerhaft offen. Bewegen tut sich dann überhaupt nichts.

Die Höhe muss dabei niemand kennen. Drei Zutaten oder dreißig, es funktioniert gleich.

Zwei Dinge zum Werkzeug. Solange der Inspector eingeschaltet ist, schluckt er Klicks in die Seite. Der Klick auf „Zutaten“ wählt dann nur das Label aus und öffnet die Liste nicht. Zum Auf- und Zuklappen gehört er also ausgeschaltet.

An die aufklappende Liste selbst kommst du mit dem Zeiger nicht heran. Geschlossen ist sie null Pixel hoch, und offen liegen die Zutaten vollständig darüber. Im Streifen steht immer nur das innerste Element unter dem Zeiger.

Bleibt die Eingabezeile der Console. getComputedStyle(document.querySelector('.zutaten')).display antwortet grid, ...transitionProperty antwortet grid-template-rows, und am Kind liefert getComputedStyle(document.querySelector('.zutaten-inhalt')).overflowY das nötige hidden. Nach der Spur selbst kannst du dort genauso fragen, und ...gridTemplateRows antwortet im geschlossenen Zustand mit 0px, nicht mit 0fr. Dort steht also die gerechnete Höhe und nicht die Schreibweise aus dem Stylesheet. 0px ist auch der Wert, der in der Prüfliste steht.

Der Weg über translate

Der andere Weg mit translate
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

.kopf {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px;
  color: #fafafa;
  background-color: #18181b;
}

.schalter {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
}

.button {
  padding: 8px 16px;
  border-radius: 8px;
  background-color: #3f3f46;
  cursor: pointer;
}

/* Der Rahmen ist immer 120px hoch. Damit er die Seite
   im geschlossenen Zustand nicht aufschiebt, liegt er
   aus dem Fluss genommen unter der Kopfzeile. */
.menue {
  position: absolute;
  top: 100%;
  left: 0;
  overflow: hidden;
  width: 100%;
  height: 120px;
}

.menue-inhalt {
  padding: 0 16px;
  background-color: #18181b;
  translate: 0px -120px;
  transition: translate 0.25s ease-out;
}

.schalter:checked ~ .menue .menue-inhalt {
  translate: 0px 0px;
}

.menue-inhalt a {
  display: block;
  padding: 8px 0;
  color: #fafafa;
}

.hinweis {
  max-width: 60ch;
  padding: 24px 16px;
  color: #52525b;
}

Es geht auch mit translate. Der Rahmen bekommt overflow: hidden und eine feste Höhe. Der Inhalt darin sitzt mit translate nach oben geschoben außerhalb und fährt beim Öffnen auf null.

Diese Variante ist billiger, weil sich nur translate bewegt und das Layout unangetastet bleibt. Dafür zahlst du an zwei Stellen. Die feste Höhe muss zum Inhalt passen, und wenn ein Link dazukommt, musst du sie nachziehen. Außerdem belegt der Rahmen seine Höhe auch im geschlossenen Zustand, denn verschoben wird nur der Inhalt darin. Der Kasten selbst bleibt stehen. Im Beispiel steht er deshalb mit position: absolute unter der Kopfzeile, sonst hinge unter dem Menü-Button dauerhaft eine 120 Pixel hohe leere Fläche.

Soll das Menü über dem Inhalt liegen, nehme ich translate. Soll es die Seite aufschieben, nehme ich das Raster.

Der Übergang gehört an den geschlossenen Zustand

Das ist dieselbe Regel wie am Anfang des Abschnitts, und hier fällt sie besonders auf. Steht die transition in der Regel mit :checked, geht das Menü weich auf und schnappt beim Schließen zu. Sie gehört an die Grundregel des Menüs, also an den Zustand, der immer gilt.

Für die Dauer sind 200 bis 300 Millisekunden richtig. Ein Menü, das eine halbe Sekunde braucht, fühlt sich beim zweiten Öffnen schon zäh an. Beim zehnten hasst man es.

In der Aufgabe klappt das Menü einer Kopfzeile auf dem Telefon schon auf, nur eben mit einem Sprung.

Zum Mitnehmen

Höhe lässt sich nicht animieren, weil auto keine Zahl ist. Eine Rasterspur von 0fr auf 1fr dagegen schon, solange das Kind darin overflow: hidden trägt, und der Übergang steht wie immer am geschlossenen Zustand.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.