Skip to content

Accordion Animations

Im nächsten Schritt wollen wir unser Accordeon animieren. Dazu müssen wir wissen: das geht nicht, wenn wir display: none zum Verstecken des Inhalts verwenden. display: none lässt sich nicht animieren (bzw. jetzt schon über einen Umweg, aber ob das hier Sinn macht, müsste noch ausprobiert werden).

Um Elemente zu verstecken, könnten wir sie entweder aus dem sichtbaren Bereich hinausschieben (sie sind dann aber immer noch fokussierbar bzw. über AT erreichbar!) oder mit visibility: hidden verstecken (das Element nimmt dann immer noch seinen Platz im DOM ein!).

Beim Accordeon verstecken wir die entsprechenden Elemente über die nicht sichtbare Höhe. Da wir die Höhe animieren wollen, können wir den Accordeon-Inhalt im geschlossenen Zustand mit height: 0 und overflow: hidden verstecken (siehe weiter unten – Calculating Height). Der Inhalt ist dann nicht sichtbar, ist aber im DOM weiterhin vorhanden (und kann auch durchsucht werden).

Wenn wir eine Eigenschaft wie height animieren, erzeugt das jank (TODO: setze einen Link zu den Animations, wenn sie da sind), aber in dem Fall haben wir keine andere Wahl.

Als erstes setzten wir die Höhe des Accordeon-Inhalts auf Null:

.accordion-content {
height: 0;
}

Jetzt schaut’s ein bissl zerrüttelt aus … der Inhalt macht einen Overflow. Warum ist das so?

Höhen werden in CSS normalerweise auf Basis der box-sizing Eigenschaft kalkuliert:

  • box-sizing: content-box bedeutet, dass die Höhe nur so hoch wie der Inhalt selbst ist (das ist gleichzeitig der default Wert)
  • box-sizing: boder-box zählt zur Höhe des Inhalts noch padding und border dazu.

Calculations for height change if you set height explicitly

Section titled “Calculations for height change if you set height explicitly”
  • Wenn height größer ist als padding+ border, entspricht die Höhe dem gesetzten Wert.
  • Wenn height gleich oder kleiner ist als padding + border, setzt der Browser die Höhe auf den Wert von padding+ border.

Wenn height zu klein für den Inhalt (oder überhaupt Null ist), wird trotzdem ein Teil vom Inhalt sichtbar sein, weil der overflow der Box by default auf visible steht. Oder anders ausgedrückt: wenn der Inhalt größer ist als der reservierte Platz dafür, dann darf er überlaufen.

Um alle Inhalte zu verstecken, müssten wir also nicht nur height, sondern auch padding und, falls vorhanden, border auf Null setzen.
Daraus ergibt sich, dass wir nicht nur height, sondern auch padding und eventuell border animieren müssten. Das würde aber zu noch mehr jank führen.

Die Lösung? Wir passen das HTML an und wrappen den Accordeon-Content in ein weiteres <div>. Dessen Höhe wird animiert, die Elemente innerhalb dieses <div> können padding und border haben.

<div class="accordion-content">
<div class="accordion-inner">
<!-- content -->
</div>
</div>

Das CSS wird angepasst:

.accordion-content {
height: 0;
overflow: hidden;
transition: height 0.3s ease-out;
.is-open & {
height: auto;
}
}

height soll jetzt also animiert werden, allerdings funktioniert das nicht, wenn heightauf auto gesetzt ist. Wir müssen die Höhe auf einen bestimmten Wert setzen. Den exakten Wert kennen wir nicht, können ihn aber mit JavaScript ermitteln und ihn dann für das jeweilige Accordeon einsetzen.

Die Höhe eines Elements ermitteln wir mit getBoundingClientRect(). Dazu müssen wir aber zuerst das Element bestimmen, das uns die Höhe verraten soll. .accordion-content haben wir auf eine Höhe von Null gesetzt, aber die Höhe des Inhalts, .accordion-inner, bleibt davon unberührt.

.accordion-inner ist ein Kindelement von .accordion-content, und .accordion-content ist ein Geschwisterelement des geklickten .accordion-header. Wir ergänzen unseren EventListener:

accordionContainer.addEventListener('click', e => {
const accordionHeader = e.target.closest('.accordion-header');
if (accordionHeader) {
// ...
const accordionContent = accordionHeader.nextElementSibling;
const accordionContentInner = accordionContent.children[0];
const height = accordionContentInner.getBoundingClientRect().height;
}
});

Die ermittelte Höhe muss nun noch ins HTML. Wir setzen die height Property ebenfalls mit JavaScript, also eine style property, zu der wir noch ein px hinzufügen müssen, weil getBoundingClientRect() Pixelwerte zurückgibt:

accordionContent.style.height = `${height}px`;

Um das Accordeon wieder zu schließen, müssen wir den inline-Style auf Null zurücksetzen. (inline-Styles haben eine höhere Spezifität als die CSS-Klasse!)
Wir prüfen also, ob das Accordeon offen oder geschlossen ist und setzen den Wert der Höhe entsprechend ein:

accordionContainer.addEventListener('click', e => {
const accordionHeader = e.target.closest('.accordion-header');
if (accordionHeader) {
// ...
if (accordion.classList.contains(is-open)) {
accordioncontent.style.height = 0;
} else {
accordionContent.style.height = `${height}px`;
}
accordion.classList.toggle('is-open');
}
});

Wir können den Code etwas klarer machen, indem wir height vorab initialisieren und der Variablen erst später einen Wert zuweisen:

let height;
if (accordion.classList.contains('is-open')) {
height = 0;
} else {
height = accordionContent.getBoundingClientRect().height;
}
accordion.classList.toggle('is-open');
accordionContent.style.height = `${height}px`;