登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

CSS @starting-style 怎样为弹窗补上首次进入动画

来源:17golang原创

时间:2026-11-01 10:47:57 440浏览 收藏

热门推荐
漫画APP
动画内容聚合,热门资源快捷查看
立即下载

弹窗第一次出现时,很多人会发现 transition 写了却没有淡入:元素之前没有可供比较的已渲染状态,或者它从 display: none 直接变成可见状态,浏览器没有普通过渡所需的“上一帧”。解决办法是用 @starting-style 明确提供首次进入的起始值,再把 display 配进允许离散变化的过渡中。

官方资料:https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/%40starting-style

把弹窗动画拆成默认隐藏态、首次进入起始态和打开完成态:@starting-style 只负责没有上一帧时从哪里开始,transition-behavior: allow-discrete 负责让 display 在动画边界翻转。这样进入时能淡入,退出时也不会立即消失。

先区分首次进入与普通返回

普通过渡通常是“旧样式 → 新样式”。但 dialog.showModal() 会让弹窗从不可见状态进入顶层,首次显示时并没有一个已经绘制过的可见样式可以当作起点。此时只写下面的最终样式和 transition,弹窗仍可能直接出现:

.settings-dialog {
  /* 让透明度和位移变化使用同一段过渡 */
  transition: opacity 220ms ease, transform 220ms ease;
  opacity: 1;
  transform: translateY(0);
}

@starting-style 的职责是为“第一次样式更新”提供起始值。它不是永久状态,也不是替代默认样式的隐藏规则;弹窗已经显示过以后,关闭再打开时会回到默认隐藏态和新的进入起始态之间的过渡。

CSS starting-style 弹窗三种状态的静态关系说明图
图1:状态结构说明图,展示弹窗首次进入、显示完成和隐藏返回之间的静态关系。

给 dialog 建立三种 CSS 状态

先只处理透明度和轻微位移,不急着加入复杂阴影或缩放。默认的 dialog 状态保持隐藏并准备退出;dialog:open 描述打开后的最终状态;嵌套在打开规则里的 @starting-style 则给第一次进入提供透明和位移起点:

.settings-dialog {
  /* 默认状态用于关闭后的回退,也作为退出动画的终点 */
  opacity: 0;
  transform: translateY(12px) scale(0.98);
  transition:
    opacity 220ms ease,
    transform 220ms ease,
    display 220ms allow-discrete;
}

.settings-dialog:open {
  /* 打开状态是进入动画的终点 */
  opacity: 1;
  transform: translateY(0) scale(1);

  @starting-style {
    /* 首次显示没有上一帧时,从这里开始过渡 */
    opacity: 0;
    transform: translateY(12px) scale(0.98);
  }
}

这里有一个容易混淆的地方:进入时使用的是 @starting-style → :open,关闭时则是 :open → 默认规则。也就是说,@starting-style 不会在每一次普通样式变化中持续介入,不能把它当成永久的“关闭状态”。

把 display 放进离散过渡

如果弹窗在退出时立刻变成 display: none,透明度还没有来得及变成 0,元素就已经从渲染树中消失,用户看到的结果就是“没有淡出”。将 display 列入 transition,并使用 allow-discrete,可以让这个离散属性在动画边界翻转,而不是用连续插值。

.settings-dialog {
  /* display 不是连续数值,所以显式允许离散过渡 */
  transition-behavior: allow-discrete;
  transition:
    opacity 220ms ease,
    transform 220ms ease,
    display 220ms allow-discrete;
}

.settings-dialog:not(:open) {
  /* 关闭状态保留到退出动画结束,再由 display 隐藏 */
  display: none;
}

在从隐藏到可见的方向,display 会在动画开始时切到可见值,使透明度和位移有机会播放;从可见到隐藏的方向,display 会尽量延迟到动画结束再翻转。这样做的关键不是给 display 设置一个“中间值”,而是让它配合连续属性的可见时间窗口。

dialog display 离散过渡和 allow-discrete 的静态边界说明图
图2:离散过渡说明图,展示 display 翻转边界与淡入淡出属性的配合关系。

用最小 HTML、CSS 和 JavaScript 连接状态

下面的示例只负责打开和关闭原生弹窗。JavaScript 不参与动画计算,只改变 dialog 的生命周期状态;动画全部交给 CSS。代码中的注释说明了打开、关闭和资源监听的职责边界:



显示设置

选择保存后关闭弹窗。

const openButton = document.querySelector("#open-settings");
const dialog = document.querySelector("#settings-dialog");

// 打开动作只改变 dialog 状态,进入动画由 CSS 接管。
openButton.addEventListener("click", () => {
  if (!dialog.open) {
    dialog.showModal();
  }
});

// 原生 form method="dialog" 会调用 close(),无需手工移除节点。
dialog.addEventListener("close", () => {
  // 这里可以同步表单结果,但不要在此处直接设置 display:none。
  console.log("dialog result:", dialog.returnValue);
});

如果你使用自定义按钮而不是 method="dialog",关闭时调用 dialog.close("cancel") 即可。不要为了播放动画先把节点删除;节点一旦被删除,就没有退出过渡可以承载了。

兼容与降级边界

@starting-style 和离散属性过渡属于较新的 CSS 能力。写稿时 MDN 将 @starting-style 标为 Baseline 2024,但旧浏览器或受限运行环境仍可能不支持。降级策略应优先保证弹窗能打开和关闭,再把动画当作增强:

/* 不支持新规则时,至少保留可见的原生 dialog 行为 */
.settings-dialog {
  opacity: 1;
  transform: none;
}

@supports (transition-behavior: allow-discrete) {
  /* 支持离散过渡的环境再启用 display 与进入起始值 */
  .settings-dialog {
    opacity: 0;
    transform: translateY(12px);
    transition:
      opacity 220ms ease,
      transform 220ms ease,
      display 220ms allow-discrete;
  }

  .settings-dialog:open {
    opacity: 1;
    transform: translateY(0);

    @starting-style {
      /* 只在支持这组特性时提供首次进入起点 */
      opacity: 0;
      transform: translateY(12px);
    }
  }
}

还要注意用户的减少动态效果设置。弹窗是信息入口,不能因为动画而降低可读性:

@media (prefers-reduced-motion: reduce) {
  /* 减少动态时缩短过渡,保留状态变化和可访问性 */
  .settings-dialog {
    transition-duration: 1ms;
  }
}

排查清单:动画仍然不出现时看哪里

  1. 确认选择器确实命中了同一个 dialog,不要把 @starting-style 写到另一个组件作用域里。
  2. 确认进入和退出的连续属性有明确的默认值与打开值,至少先检查 opacity 和 transform。
  3. 确认 display 出现在过渡列表中,并且使用了 allow-discrete。
  4. 确认关闭逻辑没有同步删除节点、设置内联 display:none 或跳过原生 close() 生命周期。
  5. 确认页面没有启用 prefers-reduced-motion 分支后把时长压到几乎不可见。

最小可用的判断标准是:首次打开能从透明、偏移状态进入;重复关闭时仍能看见短暂退出;不支持新特性时弹窗仍然可以正常使用。达到这三个条件,再考虑阴影、遮罩和更复杂的层级动画。

常见问题

@starting-style 能替代 CSS animation 吗?

它主要服务于 CSS transition 的首次样式更新。若使用关键帧动画,起始帧可以直接写在 from 或 0% 中,不需要用 @starting-style。

为什么打开动画有了,关闭动画还是没有?

通常是关闭时立即变成 display:none 或节点被删除。把 display 作为允许离散过渡的属性,并让关闭状态留到过渡完成,才能给透明度和位移留下播放时间。

必须使用原生 dialog 才能用这个规则吗?

不是。任何首次加入 DOM 或从不可见状态变为可见的元素都可以使用,但原生 dialog 的打开、关闭和顶层显示场景更容易暴露“没有上一帧”的问题。

不支持 allow-discrete 时应该怎么办?

保留无动画的可见状态作为降级,或者用 JavaScript 在状态切换后再添加类名。不要让兼容分支阻止用户打开和关闭弹窗。

css
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>