mitmario.dev

Overlay und Untermenü ohne JavaScript

CSS Im Browser 5 Min Lesezeit 4 BeispieleLektion 4 von 6

Ein Dialog, ein Klappbereich und ein Untermenü sehen nach Arbeit für JavaScript aus. Dabei kennt der Browser ihre Zustände inzwischen selbst. CSS greift sie über Attribute und Pseudoklassen ab.

Der Dialog

Ein Dialog ohne Browser-Look
*,
*::before,
*::after {
  box-sizing: border-box;
}

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

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

dialog[open] {
  max-width: 380px;
  padding: 24px;
  border: 0;
  border-radius: 12px;
  box-shadow: 0 24px 48px rgb(0 0 0 / 18%);
}

dialog h2 {
  margin: 0 0 8px 0;
  font-size: 20px;
}

dialog p {
  margin: 0 0 24px 0;
  color: #52525b;
}

/* Greift nur am modal geöffneten Dialog. Dieser trägt
   bloß open, hier siehst du die Fläche also nicht,
   im nächsten Beispiel schon. */
dialog::backdrop {
  background-color: rgb(24 24 27 / 60%);
}

.button {
  min-height: 44px;
  padding: 8px 16px;
  border: 0;
  border-radius: 8px;
  font: inherit;
  color: #ffffff;
  background-color: #2563eb;
  cursor: pointer;
}

dialog kommt aus dem HTML-Kurs. Sobald es modal aufgeht, bringt es einiges mit, das du sonst selbst bauen müsstest. Es liegt dann über allem anderen, der Rest der Seite lässt sich nicht bedienen, und die Escape-Taste schließt es.

Im CSS sprichst du es über dialog[open] an, denn ohne das Attribut steht es auf display: none. Meistens heißt das Rahmen weg, eigener Radius, mehr Innenabstand, ein Schatten und eine max-width, damit der Text auf breiten Bildschirmen lesbar bleibt.

::backdrop ist die Fläche hinter dem Dialog. Die gibt es nur, solange er modal offen ist. Ein Dialog, der nur das open-Attribut trägt wie im ersten Beispiel, ist nicht modal. Es gibt keine Abdunkelung, der Rest der Seite bleibt bedienbar, und die Escape-Taste schließt ihn nicht. Aus dem Fluss genommen ist er trotzdem. Der Browser gibt ihm position: absolute und schiebt ihn mit margin: auto waagerecht in die Mitte, und alles andere klebt links.

Am Dialog steht im Streifen position: absolute, obwohl im Stylesheet nichts davon vorkommt. Du musst dafür auf seinen Innenabstand zeigen, sonst trifft der Zeiger den Text darin. Die Mitte siehst du erst im Vollbild. Bei einer 1536 Pixel breiten Seite steht dort width: 380px und margin: 0px 578px, und 578 plus 380 plus 578 sind genau die 1536. In der schmalen Spalte ist der Dialog so breit wie die Seite, und dann steht dort margin: 0px. Das ist dieselbe Figur wie beim Zentrieren, ein automatischer Außenabstand steht als ausgerechnete Zahl da.

Modal über einen Button
*,
*::before,
*::after {
  box-sizing: border-box;
}

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

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

dialog[open] {
  max-width: 380px;
  padding: 24px;
  border: 0;
  border-radius: 12px;
  box-shadow: 0 24px 48px rgb(0 0 0 / 18%);
}

/* Die Fläche hinter dem modalen Dialog. Sie gibt es
   nur, solange er über command="show-modal" offen ist. */
dialog::backdrop {
  background-color: rgb(24 24 27 / 60%);
}

dialog h2 {
  margin: 0 0 8px 0;
  font-size: 20px;
}

dialog p {
  margin: 0 0 24px 0;
  color: #52525b;
}

.button {
  min-height: 44px;
  padding: 8px 16px;
  border: 0;
  border-radius: 8px;
  font: inherit;
  color: #ffffff;
  background-color: #2563eb;
  cursor: pointer;
}

Modal geht er über einen Button mit commandfor und command="show-modal" aus dem HTML-Kurs auf, ganz ohne JavaScript. Das können alle großen Browser seit Dezember 2025. Erst dann liegt hinter dem Dialog die abgedunkelte Fläche aus der ::backdrop-Regel. Der Button darin schließt ihn mit command="close", und Escape tut es auch.

details und summary

details mit eigenem Zeichen
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.5;
}

details {
  max-width: 46ch;
  padding: 16px 0;
  border-bottom: 1px solid #e4e4e7;
}

summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  list-style: none;
  cursor: pointer;
}

/* Safari baut das Dreieck nicht als Aufzählungszeichen. */
summary::-webkit-details-marker {
  display: none;
}

/* Das eigene Zeichen. Es dreht sich, sobald das
   offene details seinen Zustand über [open] meldet. */
summary::after {
  content: "+";
  font-size: 22px;
  line-height: 1;
  color: #2563eb;
  transition: rotate 0.2s ease-out;
}

details[open] summary::after {
  rotate: 45deg;
}

details[open] .inhalt {
  margin-block-start: 12px;
}

.inhalt p {
  margin: 0;
  color: #52525b;
}

details ist das Element für alles, was auf- und zuklappt, eine Frageliste, ein Filterbereich, ein Abschnitt mit Zusatzinfos. Das Umschalten macht der Browser, du gestaltest nur.

Das kleine Dreieck sitzt am summary und ist ein Aufzählungszeichen. Weg ist es deshalb mit list-style: none, genau wie bei der Liste in der Lektion davor. Nur Safari baut es bisher anders. Dort bleibt es mit list-style: none stehen und braucht zusätzlich summary::-webkit-details-marker { display: none; } (Stand Oktober 2026). Ein eigenes Zeichen baust du dann mit ::after und content.

Den Zustand liest du über das Attribut, denn details[open] trifft nur, solange es offen ist. Damit kannst du das eigene Zeichen drehen und den Inhalt einrücken, ohne dass eine Klasse dazukommen muss.

Zwei Dinge noch. summary bekommt cursor: pointer, denn es ist ein Bedienelement und sieht sonst aus wie Text. Und weich auffahren lässt sich der Inhalt nicht ohne Weiteres, weil der geschlossene Zustand für den Browser mehr ist als eine Höhe von null. Deshalb war das aufklappende Menü in Abschnitt 13 auf die Checkbox gebaut und nicht hierauf.

Das Untermenü

Das Untermenü braucht focus-within
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 24px;
  font-family: system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.5;
}

.etikett {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #52525b;
}

nav {
  margin-bottom: 96px;
}

.menue {
  display: flex;
  gap: 24px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.huelle {
  position: relative;
}

.untermenue {
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 170px;
  margin: 0;
  padding: 8px 0;
  border-radius: 8px;
  list-style: none;
  background-color: #ffffff;
  box-shadow: 0 8px 24px rgb(0 0 0 / 14%);
  opacity: 0;
  visibility: hidden;
}

.untermenue a {
  display: block;
  padding: 8px 16px;
}

.maus .huelle:hover .untermenue {
  opacity: 1;
  visibility: visible;
}

.beides .huelle:hover .untermenue,
.beides .huelle:focus-within .untermenue {
  opacity: 1;
  visibility: visible;
}

.hinweis {
  max-width: 60ch;
  color: #52525b;
}

Ein Menüpunkt mit Untermenü braucht zwei Dinge, die zusammengehören. Die Hülle bekommt position: relative, das Untermenü position: absolute mit top: 100%. Ohne das Erste hängt das Zweite am Seitenrand statt unter seinem Punkt. Das ist Lektion 8.3, angewandt auf den Fall, für den man es gelernt hat.

Aufklappen lässt man es dann über :hover am Elternteil. Und über :focus-within.

:focus-within trifft ein Element, wenn irgendetwas darin den Fokus hat. Ohne diese Zeile lässt sich das Untermenü mit der Tastatur nie öffnen. Es ist mit visibility: hidden versteckt, und damit sind auch seine Links aus der Tabulatorreihenfolge genommen. Der Fokus kommt also gar nicht erst hinein. Er springt von „Projekte“ direkt zum nächsten Punkt, und einen Weg zum Öffnen gibt es nicht.

Beim Bauen mit der Maus fällt der Fehler nicht auf, und er kostet eine einzige zusätzliche Zeile im Selektor.

Nachsehen kannst du das am Punkt „Projekte“. Liegt der Zeiger darauf, klappt das Untermenü auf, weil er in der Hülle liegt. Im Streifen steht dabei der Link, denn der deckt die Hülle ganz ab, und ihr position: relative steht blass im Reiter „Regeln“. Gehst du weiter nach unten in einen seiner Links, ohne die Hülle zu verlassen, steht dort 170 × 40. Das visibility: hidden des geschlossenen Zustands fehlt im Streifen. In der Eingabezeile der Console antwortet getComputedStyle(document.querySelector('.untermenue')).visibility mit hidden, solange nichts darauf zeigt.

Auf dem Telefon ist auf :hover kein Verlass. Je nach Browser greift es nach einem Tipp nur kurz oder bis zur nächsten Berührung, verlässlich trägt dort nur :focus-within. Und wenn der Elternpunkt selbst ein Ziel hat, führt der erste Tipp weg, statt das Untermenü aufzuklappen. Soll so eine Navigation auch auf dem Telefon bedienbar sein, bekommt der Elternpunkt kein eigenes Ziel, oder es kommt doch Skript dazu.

Zum Verstecken nehme ich opacity und visibility statt display: none. Beide lassen sich ohne Zusatz weich schalten, display nur mit transition-behavior: allow-discrete, und das kommt erst in Lektion 15.7. Außerdem nimmt visibility: hidden den Inhalt aus der Tabulatorreihenfolge.

Wer stattdessen nur opacity: 0 schreibt, bekommt den umgekehrten Fehler. Das Untermenü ist unsichtbar und trotzdem anspringbar. Der Fokus wandert in Links, die niemand sieht. Wer mit der Tastatur unterwegs ist, sieht eine Seite, auf der der Fokus einfach verschwindet.

Wo dieser Weg aufhört

Das Untermenü über :hover und :focus-within hat zwei Grenzen. Ein Screenreader erfährt nicht, ob es offen ist. Und wer mit einem Screenreader im Lesemodus mit den Pfeiltasten durch die Seite geht, setzt dabei meist gar keinen Fokus. Dann bleibt es zu und wird nicht vorgelesen.

Ein Popover aus dem HTML-Kurs kann beides. Der Button mit popovertarget meldet selbst, ob es offen oder zu ist, und schließen lässt es sich per Klick daneben und mit Escape, ohne eine Zeile Skript. JavaScript braucht es erst für Zustände, die der Browser nicht kennt.

Kennt der Browser den Zustand schon, nehme ich ihn. Das ist weniger Code und funktioniert auch, wenn das Skript nicht lädt.

In der Aufgabe stehen ein Untermenü, ein Klappbereich und ein Dialog, alle drei noch so, wie der Browser sie zeichnet.

Zum Mitnehmen

Ein Untermenü geht über :hover und :focus-within auf, sonst kommt die Tastatur nie hinein. Versteckt wird es mit visibility, damit seine Links nicht anspringbar sind, und wo der Browser den Zustand selbst kennt, wie bei dialog und details, greift CSS ihn ab.

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 4 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.