@starting-style 和离散属性过渡解决了什么问题?
解释新元素为什么无法从 display:none 过渡到可见状态,说明 @starting-style、allow-discrete 与 opacity/transform 的配合方式,以及退出动画、DOM 时机、兼容性和渐进增强边界。
参考答案
简答
- 新插入的元素或从
display: none变为可见的元素,浏览器只看到最终样式,没有起点,所以 transition 不播放。 @starting-style为“没有前一帧样式”的元素声明过渡起点,替代过去“先加类、下一帧再删类”的 JavaScript 时序。display是离散属性,不能插值;transition-behavior: allow-discrete让它在合适时刻切换,退出时 opacity 等动画才有机会播完。- 动画只是表现层,焦点、
inert、资源清理仍由组件状态负责;不支持时内容照常显示,只是没有动画。
首次插入为什么没有起始帧
传统写法依赖 JavaScript 等一次渲染再移除初始类:
JavaScript
document.body.append(notice); // notice 带 notice--entering 类
requestAnimationFrame(() => notice.classList.remove('notice--entering'));
组件重渲染、批量插入或 SSR 激活都可能打乱这段时序。用 @starting-style 改由 CSS 声明起点:
CSS
.notice {
opacity: 1;
transform: none;
transition: opacity 180ms ease, transform 180ms ease;
}
@starting-style {
.notice {
opacity: 0;
transform: translateY(8px);
}
}
起始规则只在元素第一次获得样式时生效;元素已存在、只是修改属性时,仍由普通 transition 处理。
进入和退出都要处理 display
CSS
.dialog {
display: none;
opacity: 0;
transform: translateY(12px) scale(0.98);
transition:
opacity 180ms ease,
transform 180ms ease,
display 180ms allow-discrete;
}
.dialog[open] {
display: block;
opacity: 1;
transform: none;
}
@starting-style {
.dialog[open] {
opacity: 0;
transform: translateY(12px) scale(0.98);
}
}
进入时 display 立刻切到 block,再从 starting-style 过渡;退出时 display: none 会推迟到动画结束才生效。模态框本身仍应使用原生 <dialog> 的 showModal(),这段 CSS 只负责视觉。
退出动画不能承担业务清理
transitionend 可能因为属性被再次修改、元素被移除、页面进入后台或减少动态效果而不触发。释放监听、定时器和焦点应由组件状态在进入关闭阶段时完成,不要等动画事件。
淡出期间元素仍在页面上,要同步禁止交互:
CSS
.toast[data-closing='true'] {
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.toast,
.dialog {
transition-duration: 1ms;
transform: none;
}
}
pointer-events: none 不影响键盘焦点和读屏,还需要配合 inert 和 ARIA 状态。SSR 首屏已经处于最终样式的内容,通常不应再播放入场动画。
兼容与渐进增强
CSS
@supports (transition-behavior: allow-discrete) {
.notice {
transition: opacity 180ms ease, display 180ms allow-discrete;
}
@starting-style {
.notice {
opacity: 0;
}
}
}
支持范围以 MDN 兼容表为准。验收覆盖插入、关闭、快速重复打开、减少动态效果,以及不支持时内容、关闭按钮和焦点是否仍然正常。