1. 题目与适用场景
管理后台有一个 dialog 元素和一个 Popover 菜单。设计要求打开、关闭都能过渡,但组件不能依赖 JavaScript 每帧改样式;旧浏览器仍要可用,减少动效用户不应看到强制动画。请说明 @starting-style 的适用边界、CSS 与脚本职责、回退和验证。
2. 面试官考察点
- 是否知道首次样式更新没有可供 transition 的旧计算样式,
@starting-style用来提供起点。 - 是否能区分进入和离开、离散属性
display、top-layer 元素和普通 opacity/transform 过渡。 - 是否会把脚本只用于状态切换,把焦点、Escape、语义和动画偏好交给正确的层处理。
- 是否设计支持检测、取消竞态、减弱动效和真实浏览器验证,而非只贴一段 CSS。
3. 回答前需要澄清的问题
- 组件是 dialog、Popover 还是普通节点?top layer 和默认焦点行为会改变方案。
- 需要关闭动画后再移除元素,还是关闭后立即不可交互?这决定
display与事件边界。 - 旧浏览器要保留静态显示、即时切换,还是必须视觉等价?
- 用户开启
prefers-reduced-motion时是缩短时间、取消位移,还是完全取消动画?
4. 30 秒回答框架
我先让 HTML 状态和语义负责显示、焦点与关闭,CSS 负责可逆的视觉过渡。进入时用 @starting-style 声明元素首次显示前的 opacity/transform 起点;离散的 display 切换使用 allow-discrete 或对应的兼容策略,确保元素在动画期间仍可绘制。退出过渡不能只依赖一个起点规则,要明确最终状态和 DOM 生命周期。旧浏览器即时切换,prefers-reduced-motion 降低或取消动画,并用键盘、读屏和竞态测试验证。
5. 分步骤深入解答
第一步:确定状态和语义边界
dialog.showModal()、Popover 的打开状态和普通 class 切换不是同一语义。先保证关闭时不可聚焦、打开时焦点进入合理位置,Escape 和取消事件可用。动画只反映状态,不应成为可访问性状态的来源;脚本应在状态改变时触发 API 或切换属性,而不是用定时器模拟可见性。
第二步:建立进入起点
首次样式更新时,浏览器可能没有上一帧样式,普通 transition 找不到起点。把初始值放进 @starting-style:
.dialog {
opacity: 1;
transform: scale(1);
transition: opacity 180ms ease, transform 180ms ease;
}
@starting-style {
.dialog[open] {
opacity: 0;
transform: scale(0.96);
}
}起点只用于首次样式更新,不是持久的关闭状态。若组件重新插入 DOM,是否重新触发进入要由生命周期和 DOM 操作决定,不能假设每次属性切换都会执行起点规则。
第三步:处理离散属性和退出
display 不能像 opacity 那样连续插值。需要让元素在过渡期间保持可绘制,并在结束时切换到不可见状态;支持的浏览器可用 transition-behavior: allow-discrete,同时为不支持者提供即时切换。退出时要保留节点到 transition 完成,或由原生组件管理其生命周期;不要用固定 setTimeout 猜测动画是否结束。监听 transitionend 时过滤属性名,并处理取消、重复打开和节点卸载。
第四步:兼容 top layer 与交互
对话框和 Popover 进入 top layer 后,层叠上下文、backdrop 和焦点行为会改变。分别定义内容与 ::backdrop 的过渡,确认点击外部、Escape、滚动锁定和多个实例的状态。脚本只保留 API 调用、取消和业务回调,不能把 @starting-style 当作 JS 状态机的替代品。
第五步:尊重动效偏好并验证
在 @media (prefers-reduced-motion: reduce) 中取消位移、缩短时间或直接关闭 transition,仍保留状态反馈。测试首次打开、重复打开、快速开关、关闭期间导航、脚本失败、旧浏览器、键盘、读屏、backdrop 和焦点回收。记录动画取消率、过渡中断、不可见节点残留与可访问性回归,而不是只检查截图。
6. 高质量示范回答
我会先区分组件语义:dialog或 Popover 的打开、焦点和 Escape 由原生 API 与少量脚本负责,CSS 只表现状态。普通 transition 没有首次样式更新的旧起点,因此用@starting-style给打开状态提供 opacity 和 transform 起点;display这类离散属性要用支持的离散过渡或旧浏览器即时切换。退出必须保留节点到过渡完成,不能用固定计时器猜生命周期。对 top layer 内容和 backdrop 分开定义规则,并在 reduced-motion、快速开关、键盘和读屏路径验证。
7. 常见错误
- 把起点规则当作关闭状态 → 后续属性切换找不到正确状态 → 明确持久状态、进入起点和最终关闭状态。
- 给
display写普通 opacity 过渡 → 元素提前不可绘制或仍可交互 → 使用离散过渡能力并定义兼容路径。 - 用
setTimeout移除节点 → 用户改速、取消或丢帧时生命周期漂移 → 监听有效 transition 事件或使用原生生命周期。 - 动画控制焦点和语义 → 读屏与键盘状态依赖视觉时间 → 先保证 HTML/API 状态,再添加视觉过渡。
- 忽略 reduced motion → 用户偏好被违背 → 缩短或取消动画,保留清晰状态变化。
8. 追问及应对
追问一:为什么普通 transition 不足以实现首次淡入?
首次样式更新前没有可比较的旧计算样式,浏览器可能直接应用最终值。@starting-style 提供这一时刻的起点;它只描述首次更新,不等于所有后续关闭状态。
追问二:关闭时如何让 display 参与过渡?
让元素在过渡期间保持可绘制,并在离散时间点切换 display;支持 allow-discrete 时使用它,旧浏览器即时关闭。无论哪条路径,都要确保关闭后不能继续获得焦点或鼠标交互。
追问三:快速连续打开和关闭会怎样?
状态可能在上一条 transition 完成前反转。取消旧动画、读取当前状态并让新状态成为唯一提交者;不要用两个并行定时器分别移除和打开节点。测试交替点击、Escape 和导航卸载。