元素首次出现时的过渡动画问题
CSS Transitions有一个长期痛点:元素从 display:none 切换到可见状态时,过渡动画无法生效。浏览器在元素进入文档流时没有”上一个状态”可供插值,导致opacity从0到1、transform从scale(0)到scale(1)等入场动画必须依赖@keyframes或JavaScript定时器hack。
传统解决方案:
/* 传统方案:两帧requestAnimationFrame */
const el = document.querySelector(".modal");
el.style.display = "block";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.classList.add("visible");
});
});
双重RAF不可靠,浏览器可能合并帧导致第一帧被跳过。CSS @starting-style提案正是为解决这个问题而生。
@starting-style语法与工作原理
@starting-style 规则为元素定义”进入文档时的初始样式”,浏览器将其作为过渡的起始状态进行插值。Chrome 117+和Safari 17.4+已支持该特性:
.card {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.card.visible {
opacity: 1;
transform: translateY(0);
}
@starting-style {
.card.visible {
opacity: 0;
transform: translateY(20px);
}
}
工作流程:当 .card 获得 .visible 类时,浏览器检测到transition属性有变化,查找 @starting-style 中 .card.visible 的样式作为起始值,计算从 opacity:0; translateY(20px) 到 opacity:1; translateY(0) 的插值动画。无需JavaScript介入。
display:none切换场景的完整方案
配合 transition-behavior: allow-discrete 和 @starting-style,实现display:none到可见的完整过渡:
.overlay {
display: none;
opacity: 0;
transition: opacity 0.3s ease, display 0.3s ease allow-discrete;
}
.overlay.open {
display: block;
opacity: 1;
}
@starting-style {
.overlay.open {
opacity: 0;
}
}
allow-discrete 允许discrete属性(display、visibility)参与过渡。display从none到block的变化时机被延迟到过渡完成后,实现”淡入后显示、隐藏后消失”的完整闭环。
反向退出动画同样生效——移除 .open 类时,opacity从1过渡到0,display在过渡结束后才切换回none。这是以往需要复杂JS才能实现的效果。
Popover和Dialog的原生过渡方案
HTML popover属性和dialog元素天生支持显示/隐藏切换,配合@starting-style可实现零JS的弹窗过渡:
[popover] {
opacity: 0;
transform: scale(0.9);
transition: opacity 0.2s ease, transform 0.2s ease,
display 0.2s ease allow-discrete,
overlay 0.2s ease allow-discrete;
inset: 0;
margin: auto;
}
[popover]:popover-open {
opacity: 1;
transform: scale(1);
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(0.9);
}
}
overlay 属性控制顶层覆盖层,配合 allow-discrete 可实现backdrop的淡入淡出。这种方式比 ::backdrop 伪元素动画更灵活,且无需额外JavaScript。
Vue3/React组件中的集成实践
Vue3中使用@starting-style替代Transition组件:
<template>
<div v-if="show" class="panel" :class="{ active: animated }">
<slot />
</div>
</template>
<script setup>
import { ref, watch, nextTick } from "vue";
const show = ref(false);
const animated = ref(false);
watch(show, async (val) => {
if (val) {
await nextTick();
animated.value = true;
} else {
animated.value = false;
}
});
</script>
更简洁的做法是直接用CSS驱动,通过 :popover-open 或自定义属性选择器触发,省去 animated 状态变量。React同理,只需在条件渲染时添加类名,@starting-style自动处理入场动画。
浏览器兼容与降级方案
截至2026年8月,@starting-style在Chrome 117+、Edge 117+、Safari 17.4+中可用,Firefox尚未支持。降级策略:
@supports (transition-behavior: allow-discrete) {
.modal {
transition: opacity 0.3s, display 0.3s allow-discrete;
}
@starting-style {
.modal.open { opacity: 0; }
}
}
@supports not (transition-behavior: allow-discrete) {
.modal {
animation: fadeIn 0.3s ease forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
}
渐进增强方案确保在不支持@starting-style的浏览器中仍能呈现入场效果,仅退出动画降级为即时隐藏。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/cssstartingstyle-yu-guo-du-dong-hua-ling-yan-chi-chu-shi/