Elemente, die in der „obersten Ebene“ gerendert werden (z. B. <dialog>, Elemente mit dem Attribut popover oder Kurzinfos), waren bisher schwer zu animieren, da sie zwischen display: none und einem sichtbaren Zustand wechseln. Modernes CSS bietet @starting-style, transition-behavior: allow-discrete und die Property overlay, um flüssige Übergänge für diese Elemente zu ermöglichen. In den folgenden Beispielen wird die native CSS-Verschachtelung verwendet.
Implementierung
1. Diskrete Übergänge aktivieren
Wenn Sie das Attribut display animieren möchten, müssen Sie transition-behavior: allow-discrete festlegen. So bleibt das Element während des Übergangs beim Ausblenden sichtbar. Wenn Sie Kurzschreibweisen für Übergänge verwenden, müssen Sie transition-behavior: allow-discrete danach platzieren, damit die Kurzschreibweise nicht aufgehoben wird.
2. Das Attribut overlay
Wenn ein Element in die oberste Ebene verschoben oder daraus entfernt wird, muss die Property overlay umgestellt werden. So bleibt das Element für die Dauer der Animation in der obersten Ebene und wird nicht vorzeitig von anderen Elementen oder dem Darstellungsbereich abgeschnitten.
3. Eingangs-Animationen mit @starting-style
Verwenden Sie die At-Regel @starting-style, um die Stile zu definieren, von denen ein Element übergehen soll, wenn es zum ersten Mal gerendert wird oder sich sein display von none ändert.
4. Bilderrahmen wird animiert
Das Pseudoelement ::backdrop kann ähnlich animiert werden, indem Übergänge auf seine eigenen Eigenschaften angewendet werden.
Beispiel
/* 1. Define the visible (open) state */
dialog[open],
[popover]:popover-open {
opacity: 1;
transform: scale(1);
/* 2. Define the starting state for entry (must come after open state) */
@starting-style {
opacity: 0;
transform: scale(0.9);
}
}
/* 3. Define the base (closed/exit) state and transitions */
dialog,
[popover] {
opacity: 0;
transform: scale(0.9);
/* MANDATORY: transition display and overlay for top-layer elements */
transition-property: opacity, transform, display, overlay;
transition-duration: 0.3s;
transition-timing-function: ease-out;
/* Applies to discrete properties like display and overlay */
transition-behavior: allow-discrete; /* Note: be sure to write this after the shorthand */
}
/* 4. Animate the backdrop */
dialog::backdrop,
[popover]::backdrop {
background-color: rgba(0, 0, 0, 0);
transition-property: background-color, display, overlay;
transition-duration: 0.3s;
transition-timing-function: ease-out;
transition-behavior: allow-discrete;
}
dialog[open]::backdrop,
[popover]:popover-open::backdrop {
background-color: rgba(0, 0, 0, 0.5);
@starting-style {
background-color: rgba(0, 0, 0, 0);
}
}
/* 5. Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
dialog,
[popover] {
/* Disable movement and shorten duration for a simple fade */
transform: none;
transition-duration: 0.1s;
}
@starting-style {
dialog[open],
[popover]:popover-open {
transform: none;
}
}
}
Einschränkungen und Barrierefreiheit
- OBLIGATORISCH: Fügen Sie
overlayin Ihretransition-Liste für jedes Element ein, das in die oberste Ebene verschoben oder daraus entfernt wird. - OBLIGATORISCH: Verwende
allow-discretefür die Property-Umstellung vondisplay. - OBLIGATORISCH: Berücksichtigen Sie die Nutzereinstellungen für reduzierte Bewegung mit
prefers-reduced-motion, indem Sie Übergänge vereinfachen (z.B. Transformationen entfernen und Dauer verkürzen). - RICHTIG: Platzieren Sie den
@starting-style-Block innerhalb oder nach dem Selektor für den Status „open“, um eine korrekte Kaskadierung zu gewährleisten. - NICHT:
@starting-stylefür Animationen zum Ausblenden verwenden. Animationen zum Ausblenden werden durch den Übergang zum Basisstatus (geschlossen) 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 für Exit auf oberster Ebene
Eingangs-Animationen funktionieren in reinem CSS in allen Browsern, die @starting-style unterstützen. Es ist keine .is-opening-Klasse erforderlich, da Eingangs-Übergänge nicht von overlay oder diskreten display-Übergängen abhängen.
Für Exit-Animationen ist sowohl die Unterstützung von overlay als auch von diskreten display-Übergängen erforderlich. Wenn eine der beiden Optionen nicht unterstützt wird (z. B. in Firefox und Safari), umschließen Sie die Selektoren für den geöffneten Zustand mit :is() und hängen Sie :where(:not([data-closing])) an (verschachteln Sie &::backdrop und @starting-style), sodass das Festlegen von data-closing den Übergang zum Schließen auslöst, während das Element in der obersten Ebene bleibt. Warten Sie dann, bis sich getAnimations() stabilisiert hat, bevor Sie .close() oder .hidePopover() aufrufen:
:is(dialog[open], [popover]:popover-open):where(:not([data-closing])) {
opacity: 1;
transform: scale(1);
@starting-style {
opacity: 0;
transform: scale(0.9);
}
&::backdrop {
background-color: rgb(0 0 0 / 0.5);
@starting-style {
background-color: transparent;
}
}
}
// Evaluate lazily: canTransitionDisplay() needs document.body, so a top-level
// const would be permanently false if this script runs in <head>.
function supportsTopLayerExit() {
return window.CSS?.supports?.('overlay', 'auto') && canTransitionDisplay();
}
async function closeTopLayer(element) {
if (!supportsTopLayerExit()) {
element.setAttribute('data-closing', '');
const animations = element.getAnimations({ subtree: true });
if (animations.length > 0) {
await Promise.race([
Promise.allSettled(animations.map((a) => a.finished)),
new Promise((r) => setTimeout(r, 2000)),
]);
}
if (!element.hasAttribute('data-closing')) return;
element.removeAttribute('data-closing');
}
element.close();
// Or for popover:
// element.hidePopover();
}
// Route native close requests (Esc, closedby light dismiss) through the same
// helper so they animate too, instead of closing instantly.
dialog.addEventListener('cancel', (event) => {
event.preventDefault();
closeTopLayer(dialog);
});
Das Schließen von Pop-overs durch Klicken außerhalb des Pop-overs und das Ein-/Ausblenden von popovertarget können nicht abgefangen werden (beforetoggle kann nur beim Öffnen abgebrochen werden). In Browsern, die das Fallback benötigen, werden diese Exits also sofort ausgeführt. Stellen Sie ein explizites Schließen-Steuerelement bereit, das closeTopLayer() aufruft, wenn die Animation zum Schließen wichtig ist.
Fallbacks und Browserunterstützung für Popover
Die Popover API ist größtenteils eine progressive Verbesserung, aber ihre definierenden Verhaltensweisen – die Hochstufung auf die oberste Ebene, das Schließen durch Klicken außerhalb des Popovers und der Aufruf von popovertarget – haben kein CSS-Äquivalent. Ältere Browser benötigen ein Polyfill oder ein manuelles Fallback, wenn Sie kein Polyfill bereitstellen möchten.
Polyfill: Damit das Attribut popover in älteren Browsern unterstützt wird, lade @oddbird/popover-polyfill bedingt. OBLIGATORISCH: Prüfe, ob die Funktion unterstützt wird, indem du nach der Property popover in HTMLElement.prototype suchst, und lade das Polyfill nur, wenn die native Unterstützung fehlt. Lade es NICHT bedingungslos.
Mit einem Bundler oder einer Importzuordnung:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Ohne Bundler importieren Sie aus einem CDN in einem <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Einschränkung beim Styling: Das Polyfill kann die echte Pseudoklasse :popover-open nicht definieren, daher wird stattdessen die Klasse .\:popover-open angewendet. OBLIGATORISCH: Kombinieren Sie die beiden mit :is() oder :where(). Andernfalls verwerfen Browser, die :popover-open nicht unterstützen, die gesamte Regel:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Alternativ können Sie für einen Legacy-Fallback ohne Polyfill position: fixed verwenden und die Koordinaten manuell über getBoundingClientRect() berechnen oder die Standardpositionierung mit inset: auto verwenden, wenn das für den Anwendungsfall akzeptabel ist.