Ein Modal zu coden, ist simpel.

Ein gutes Modal zu coden, ist extrem aufwändig:

  • Du musst ein Element erstellen, das Inhalt über dem Rest der Seite anzeigt
  • Du musst einen Backdrop erstellen, der den Rest der Seite visuell in den Hintergrund rücken lässt
  • Du musst alle interaktiven Elemente, die nicht zum Modal gehören, temporär inaktiv (inert) machen
  • Du musst eine oder mehrere Möglichkeiten schaffen, das Modal durch Klicken/Tippen zu schließen
  • Das Modal sollte zusätzlich mit der Tastatur (Escape) schließbar sein
  • Wenn das Modal geschlossen wurde, sollte der Fokus im Zweifelsfall wieder auf dem Element liegen, welches vor dem Öffnen des Modals fokussiert war
  • Du solltest, aus ästhetischen, aber auch aus Gründen der UX, ein weiches Öffnen und Schließen des Modals implementieren

Was, wenn ich dir sage, dass es mittlerweile sehr einfach ist, ein Modal zu programmieren, das all diesen Ansprüchen genügt?

Ohne viel JavaScript.

Das wäre der Hammer, ich weiß.

Und es geht tatsächlich. In unter 10 Minuten!

Wir benötigen dafür ein relativ neues HTML-Element namens “dialog”. Der Browser-Support liegt bei über 96%, also alles cool. Dieses Element füllen wir nun mit beliebigem Inhalt.

<dialog>Hallo Exord!</dialog>

Das Dialog-Element ist per default unsichtbar und kann per JavaScript als Modal geöffnet werden.

const dialogElement = document.querySelector("dialog");

dialogElement.showModal();

Es gibt neben showModal() auch noch die Funktion show(), die den Inhalt anzeigt, jedoch eben nicht als Modal. Für uns jetzt weniger interessant.

Auf jeden Fall sollte das Ergebnis so aussehen:

Screenshot_2024-06-25_at_16.46.10.png

Schauen wir uns mal an, wie wir das Modal stylen können, denn schön geht anders!

dialog {
  border: none;
  border-radius: .5rem;
  box-shadow: 0 .25rem 1rem rgba(0, 0, 0, .1);
}

Drei Zeilen CSS und schon sieht die Welt ganz anders aus. Ein Z-Index ist an dieser Stelle überflüssig, da das Modal automatisch ganz oben erscheint. Praktisch.

Screenshot_2024-06-25_at_14.04.30.png

Schließen mit Escape klappt auch schon. Probier es aus und staune!

Gut, aber wir brauchen auf jeden Fall einen Button, der das Fenster schließt. Dazu wrappen wir den Inhalt des Dialog-Elements in ein Form-Tag und geben als Methode "dialog" an. Das bewirkt, dass der Button innerhalb des Form-Tags das Modal automatisch schließt – ganz ohne JavaScript!

<dialog>
  <form method="dialog">
    Hallo Exord
    <button>Ok</button>
  </form>
</dialog>

Damit der Inhalt des Modals schön untereinander angezeigt wird, ergänzen wir im CSS:

dialog {
  ...
}

dialog form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

Screenshot_2024-06-25_at_14.15.13.png

Das Geniale ist, dass der Button auch direkt Fokus bekommt und mit Enter gedrückt werden kann. Accessability ist nicht nur fair gegenüber Benachteiligten, sondern erfreut auch Menschen (wie mich), die einfach gerne mit der Tastatur arbeiten.

Wir haben nun ein sauberes Modal, welches die Mindestanforderungen erfüllt. Dass man es nicht automatisch mit einem Klick auf den Backdrop schließen kann, ist noch suboptimal, aber das fixen wir gleich.

Jetzt geht es an die weichen Übergänge, denn es kann verwirrend sein, wenn UI-Elemente einfach erscheinen bzw. verschwinden, ohne dass man sehen kann woher sie kommen oder wohin sie gehen.

Bevor wir jedoch weitermachen, erstellen wir noch einen Button, der das Modal öffnet, sodass wir gleich besser testen können:

<button id="open_modal">Modal öffnen</button>
<dialog>
  <form method="dialog">
    Hallo Exord
    <button>Ok</button>
  </form>
</dialog>

Das JavaScript sollte dann so aussehen:

const dialogElement = document.querySelector("dialog");
const openModalButton = document.querySelector("#open_modal");

openModalButton.onclick = () => dialogElement.showModal();

Wichtig: es heißt showModal() und nicht openModal(). Während die Funktion für das Schließen closeModal() heißt. 💀

Ist einfach so, müssen wir mit leben.

Jetzt zu den Animationen. Tatsächlich müssen wir dafür nur das CSS verändern, was nicht heißt, dass es einfach wird. Aber immernoch viel simpler als das Ganze mit JavaScript umzusetzen.

dialog {
  border: none;
  border-radius: .5rem;
  box-shadow: 0 .25rem 1rem rgba(0, 0, 0, .1);
  opacity: 0;
  transform: scale(.95);
  transition: all .3s ease allow-discrete;
}

dialog form {
  ...
}

dialog[open] {
  opacity: 1;
  transform: scale(1);
}

Das Dialog-Element haben wir transparent gemacht, leicht herunterskaliert und ihm eine Transition gegeben. Das Entscheidende ist dabei "allow-discrete", ein neues CSS-Feature, dass Transitions von Display-Änderungen erlaubt. Browser-Support ist aktuell bei 70%, aber das wird sich bald ändern. Im Schlimmsten Fall wird das Modal einfach nur nicht animiert, aber funktioniert trotzdem.

Das geöffnete Dialog-Element stylen wir so, dass es nicht transparent und nicht skaliert ist.

Jetzt können wir den Backdrop auf ähnliche Weise anpassen, um auch hier einen weichen Übergang zu erzeugen:

dialog {
  ...
}

dialog::backdrop {
  opacity: 0;
  transition: all .3s ease allow-discrete;
}

dialog form {
  ...
}

dialog[open] {
  ...
}

dialog[open]::backdrop {
  opacity: 1;
}

Jetzt haben wir nur ein Problem: Das Modal schließt weich, aber öffnet ohne Transition. Wir haben bisher nämlich nur den Endzustand des geschlossenen Modals und den Endzustand des geöffneten Modals definiert. Wir müssen jetzt noch den Startzustand des geöffneten Modals definieren:

dialog {
  ...
}

dialog::backdrop {
  ...
}

dialog form {
  ...
}

dialog[open] {
  ...
}

dialog[open]::backdrop {
  ...
}

@starting-style {
  dialog[open] {
    transform: scale(.9);
    opacity: 0;
  }

  dialog[open]::backdrop {
    opacity: 0;
  }
}

Jetzt sollte alles Funktionieren!

Falls es bei dir nicht funktioniert, kann das an der Reihenfolge der CSS-Regeln liegen. Es ist wichtig, dass die @starting-style-Regel nach den anderen kommt. Ansonsten liegt es am Browser.

Close on Backdrop Click

Das Letzte, was wir noch machen können, um unser Modal zu perfektionieren, ist, das Schließen des Modals durch Klicken auf den Backdrop zu ermöglichen. Das ist zwar nicht zwingend notwendig, aber ein Feature, das viele User erwarten.

Leider habe ich dafür nur eine "hacky" Lösung gefunden. Bis es eine native Lösung gibt, ist sie gut genug.

Der Knackpunkt ist, dass wir das Padding vom Dialog-Element auf 0 setzen und stattdessen dem Form-Element das Padding geben. Dann können wir das hier zum JavaScript hinzufügen:

window.addEventListener("click", event => {
  if (event.target === dialogElement) dialogElement.close();
});

Den gesamten Code findest du hier: Codepen

Achso, die Buttons sollten noch gestyled werden. Das darfst du gerne übernehmen! ✌️