Ein- und Ausblenden von Elementen animieren

In der Vergangenheit konnten CSS-Übergänge Elemente nicht animieren, wenn sie zum ersten Mal dem DOM hinzugefügt wurden oder wenn sich ihre display-Eigenschaft von none geändert hat. Die At-Regel @starting-style und transition-behavior: allow-discrete bieten eine deklarative Möglichkeit, flüssige Ein- und Ausblendanimationen zu erstellen.

Implementierung

1. display: none Umschaltflächen animieren

So animieren Sie ein Element, wenn Sie seine Sichtbarkeit über ein Attribut umschalten (z.B. hidden mit display: none):

  1. Sichtbaren Zustand definieren: Legen Sie die endgültigen Attributwerte (z.B. opacity: 1) für die Basisklasse fest.
  2. Anfangsstatus für den Einstieg definieren: Verwenden Sie @starting-style, um die Werte anzugeben, von denen der Übergang erfolgen soll, wenn das Element sichtbar wird.
  3. Diskrete Übergänge aktivieren: Fügen Sie display in die Property transition ein und verwenden Sie transition-behavior: allow-discrete.
  4. Endzustand definieren: Legen Sie die Zielwerte im Attribut hidden fest.
.card {
  display: block;
  opacity: 1;
  translate: 0;
  /* MANDATORY: Use transition-behavior: allow-discrete for display transition */
  transition:
    display 0.4s,
    opacity 0.4s ease-out,
    translate 0.4s ease-out;
  transition-behavior: allow-discrete;
}

/* Entry animation: transition FROM these values when first rendered */
@starting-style {
  .card {
    opacity: 0;
    translate: 0 -20px;
  }
}

/* Exit animation: transition TO these values when hidden */
.card[hidden] {
  display: none;
  opacity: 0;
  translate: 0 -20px;
}

/* Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
  .card {
    /* Disable movement and shorten duration for a simple fade */
    translate: none;
    transition-duration: 0.1s;
  }

  @starting-style {
    .card {
      translate: none;
    }
  }

  .card[hidden] {
    translate: none;
  }
}

2. Animation beim Einfügen und Entfernen von DOM-Elementen

Für Elemente, die über appendChild() hinzugefügt oder über remove() entfernt wurden:

  • Eintrag: Verwenden Sie @starting-style wie oben gezeigt. Der Browser erkennt die Stiländerung von „nichts“ zu den ursprünglichen Stilen des Elements automatisch und löst den Übergang von den @starting-style-Werten aus.
  • Entfernen: Da element.remove() sofort erfolgt und von sich aus keinen CSS-Übergang auslöst, müssen Sie zuerst den Exit-Übergang auslösen und warten, bis er abgeschlossen ist, bevor Sie den Knoten aus dem DOM entfernen.
// 1. Trigger exit transition
element.hidden = true;

// 2. Wait for all active transitions/animations to finish,
//    with a failsafe timeout in case an animation never ends (e.g. for looping animations)
const animations = element.getAnimations();
if (animations.length > 0) {
  await Promise.race([
    // Promise.allSettled ensures we wait even if some animations fail
    Promise.allSettled(animations.map(a => a.finished)),
    new Promise(r => setTimeout(r, 2000))
  ]);
}

// 3. Finally remove the node from the DOM
element.remove();

Einschränkungen und Barrierefreiheit

  • OBLIGATORISCH: Verwende transition-behavior: allow-discrete, wenn du display umstellst. Ohne diese Eigenschaft verschwindet das Element beim Beenden sofort.
  • Verwenden Sie NICHT allow-discrete in der Kurzschreibweise transition, da ältere Browser sonst die gesamte transition-Deklaration ignorieren. Verwenden Sie eine separate transition-behavior: allow-discrete-Deklaration, außer in Anwendungsfällen, in denen dies erwünscht ist.
  • VERPFLICHTEND: Verwenden Sie @starting-style für Einblendungsanimationen. Browser überspringen Übergänge bei der ersten Stilaktualisierung eines Elements (erstes Rendering oder Änderung von display: none), sofern dies nicht angegeben ist.
  • EMPFOHLEN: Nehmen Sie overlay in die Liste transition auf, wenn Sie Elemente der obersten Ebene wie <dialog> oder popover animieren, damit sie während der Ausgangsanimation in der obersten Ebene bleiben.
  • EMPFOHLEN: Berücksichtigen Sie die Nutzereinstellungen für reduzierte Bewegung mit der Medienabfrage prefers-reduced-motion.
  • NICHT: @starting-style für Ausblendanimationen verwenden. Damit wird nur der Startpunkt für einen Einblendübergang definiert. Ausblendungsanimationen werden durch den Übergang zum ausgeblendeten Zustand definiert.

Fallback-Strategien

Fallbacks und Browserunterstützung für „transition-behavior“

Firefox 129+ parst transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') gibt true zurück), ohne die display-Property tatsächlich zu übergeben (Firefox-Fehler 1882408). Dadurch verschwinden Elemente beim Beenden sofort.

Wenn Sie die Unterstützung für diskrete display-Übergänge zuverlässig erkennen möchten, prüfen Sie, ob der berechnete display-Wert eines temporären Elements sichtbar bleibt, wenn es zu none übergeht, anstatt sich nur auf CSS.supports('transition-behavior', 'allow-discrete') zu verlassen:

let supportsDisplayTransition;
function canTransitionDisplay() {
  if (supportsDisplayTransition !== undefined) return supportsDisplayTransition;
  if (!window.CSS?.supports?.('transition-behavior', 'allow-discrete') || !document.body) {
    return false;
  }
  const probe = document.createElement('div');
  // The shorthand is intentional here: browsers that don't parse allow-discrete
  // drop the whole declaration, so display: none applies instantly and the probe
  // correctly returns false. !important guards against global reduced-motion
  // resets like `* { transition: none !important }`.
  probe.style.cssText = 'transition: display 1s allow-discrete !important; display: block;';
  document.body.appendChild(probe);
  getComputedStyle(probe).display;
  probe.style.display = 'none';
  supportsDisplayTransition = getComputedStyle(probe).display === 'block';
  probe.remove();
  return supportsDisplayTransition;
}

Fallback beenden, wenn diskrete display-Übergänge nicht unterstützt werden

Eingangs-Animationen mit @starting-style funktionieren in allen modernen Browsern ohne JavaScript. Wenn diskrete display-Übergänge nicht unterstützt werden (!canTransitionDisplay()), wird durch das Festlegen von hidden sofort display: none angewendet und die Animation zum Ausblenden übersprungen (sowohl beim Ausblenden als auch vor element.remove()). Trennen Sie den visuellen Zustand beim Ausblenden ([data-closing]) von display: none ([hidden]), damit opacity und translate die Animation beenden, bevor das Element ausgeblendet oder entfernt wird. Ersetzen Sie die .card[hidden]-Regeln aus Schritt 1 (einschließlich der prefers-reduced-motion-Überschreibung) durch Folgendes:

.card:where([hidden], [data-closing]) {
  opacity: 0;
  translate: 0 -20px;
}

.card[hidden] {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .card:where([hidden], [data-closing]) {
    translate: none;
  }
}
async function hideElement(el) {
  if (canTransitionDisplay()) {
    el.hidden = true;
  } else {
    el.setAttribute('data-closing', '');
  }

  const animations = el.getAnimations();
  if (animations.length > 0) {
    await Promise.race([
      Promise.allSettled(animations.map((a) => a.finished)),
      new Promise((r) => setTimeout(r, 2000)),
    ]);
  }

  // Skipped if showElement() cancelled the close mid-animation.
  if (el.hasAttribute('data-closing')) {
    el.removeAttribute('data-closing');
    el.hidden = true;
  }
}

function showElement(el) {
  el.removeAttribute('data-closing');
  el.hidden = false;
}

async function removeElement(el) {
  await hideElement(el);
  el.remove();
}