Elementen animeren bij openen en sluiten

In het verleden konden CSS-overgangen elementen niet animeren als ze voor het eerst aan de DOM werden toegevoegd of als de eigenschap display werd gewijzigd van none. De at-regel @starting-style en transition-behavior: allow-discrete bieden een declaratieve manier om soepele animaties bij openen en sluiten te maken.

Implementatie

1. display: none schakelaars voor animatie

Zo animeer je een element als je de zichtbaarheid ervan aan- of uitzet via een kenmerk (bijvoorbeeld hidden met display: none):

  1. Definieer de zichtbare status: Stel de uiteindelijke propertywaarden (bijv. opacity: 1) in voor de basisklasse.
  2. De beginstatus van de invoer definiëren: Gebruik @starting-style om de waarden aan te geven waarvan de overgang moet plaatsvinden als het element zichtbaar wordt.
  3. Discrete overgangen aanzetten: Neem display op in de property transition en gebruik transition-behavior: allow-discrete.
  4. Definieer de eindstatus: Stel de doelwaarden in het kenmerk hidden in.
.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. DOM-invoeging en -verwijdering animeren

Voor elementen die zijn toegevoegd via appendChild() of verwijderd via remove():

  • Invoer: Gebruik @starting-style zoals hierboven getoond. De browser herkent automatisch de stijlverandering van 'niets' naar de oorspronkelijke stijlen van het element en activeert de overgang van de @starting-style-waarden.
  • Verwijdering: Omdat element.remove() onmiddellijk is en zelf geen CSS-overgang activeert, moet je eerst de afsluitovergang activeren en wachten tot deze is voltooid voordat je de node uit de DOM verwijdert.
// 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();

Beperkingen en toegankelijkheid

  • MANDATORY: Use transition-behavior: allow-discrete when transitioning display. Zonder deze eigenschap verdwijnt het element onmiddellijk tijdens het afsluiten.
  • Gebruik GEEN allow-discrete in de verkorte notatie van transition. Hierdoor negeren oudere browsers de hele transition-declaratie. Gebruik een afzonderlijke transition-behavior: allow-discrete-declaratie, behalve in use cases waarin dat wenselijk is.
  • VERPLICHT: Gebruik @starting-style voor animaties bij openen. Browsers slaan overgangen over bij de eerste stijluitvoering van een element (eerste weergave of wijziging van display: none), tenzij dit wordt aangeleverd.
  • WEL DOEN: Neem overlay op in de lijst transition als je elementen in de bovenste laag animeert, zoals <dialog> of popover, om te zorgen dat ze in de bovenste laag blijven tijdens de animatie bij sluiten.
  • WEL: Respecteer de gebruikersvoorkeuren voor minder beweging met de mediaquery prefers-reduced-motion.
  • NIET DOEN: Vertrouw op @starting-style voor animaties bij sluiten. Hiermee wordt alleen het startpunt voor een overgang bij openen gedefinieerd. Sluitingsanimaties worden gedefinieerd door de overgang naar de verborgen status.

Reservestrategieën

Fallbacks en browserondersteuning voor transition-behavior

Firefox 129+ parseert transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete') retourneert true) zonder de property display daadwerkelijk over te zetten (Firefox-bug 1882408), waardoor elementen onmiddellijk verdwijnen bij het afsluiten.

Als je ondersteuning voor discrete display-overgangen betrouwbaar wilt detecteren, controleer je of de berekende display van een tijdelijk element zichtbaar blijft bij de overgang naar none in plaats van alleen te vertrouwen op CSS.supports('transition-behavior', 'allow-discrete'):

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;
}

Terugval bij afsluiten als afzonderlijke display-overgangen niet worden ondersteund

Invoeranitmaties met @starting-style werken in alle moderne browsers zonder JavaScript. Als afzonderlijke display-overgangen niet worden ondersteund (!canTransitionDisplay()), wordt de instelling hidden display: none onmiddellijk toegepast en wordt de animatie voor verlaten overgeslagen (zowel bij verbergen als vóór element.remove()). Scheid de visuele status voor verlaten ([data-closing]) van display: none ([hidden]), zodat de animatie van opacity en translate wordt afgerond voordat het element wordt verborgen of verwijderd. Vervang de .card[hidden]-regels uit stap 1 (waaronder de prefers-reduced-motion-overschrijving) door:

.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();
}