返回知识中心
平台

View Transitions API:状态与导航

用 document.startViewTransition 协调 DOM 状态、ready 与 finished、历史记录、终止处理和减少动效降级。

BotBrowser Team

文档中心

想直接进入 平台 文档吗?

这篇文章属于知识中心的指南库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

View Transitions API 可以让同文档路由切换更连贯,但动画只是契约的一部分。BotBrowser 能在授权场景中重放并记录受限的 DOM、生命周期和偏好状态;它不能让不支持的浏览器实现该 API,也不能证明辅助技术会播报什么。当过渡同时涉及路由或动效偏好时,请结合Navigation API 历史与隐私和媒体查询偏好与用户控制阅读。

TL;DR

  • 用一次同步 DOM 更新包住 document.startViewTransition(),让更新回调具备幂等性。
  • 把 transition.ready 视为捕获伪元素可以开始动画的节点,把 transition.finished 视为视觉状态稳定的节点。
  • 更新被拒绝或过渡终止时,应进入可控降级,而不是把它当作路由失败;新的 DOM 仍要可用。
  • 先提交应用状态再测量新布局,不要在路由遥测和过渡遥测中重复记录同一次导航或同一个矩形。
  • 尊重 prefers-reduced-motion: reduce:保留状态变化和历史记录,只关闭或缩短动画。

内容

建模生命周期

CSS View Transitions 规范描述了旧渲染快照、更新回调以及可通过伪元素动画化的新渲染。JavaScript 入口是 document.startViewTransition(updateCallback)。它会立即返回 ViewTransition 对象,但回调和捕获会稍后发生。这个对象是生命周期句柄,不代表路由已经完成。

为状态明确命名:

从 DOM 提交经过 ready、animation、finished、abort 到减少动效降级的流程

状态含义可记录字段
requested应用接受了一次过渡请求navigationId、路由、动效偏好
updating回调正在修改所属 DOMupdateStarted: true
ready新伪元素可用于动画ready: true,不要声称绘制已完成
animating动画或即时降级路径正在运行animationMode
finished过渡稳定或被跳过finished: true
aborted捕获、更新或动画无法继续abortReason、降级状态

这样就不会把 requested、ready、finished 算作三次导航,它们只是同一事件的阶段。整个序列保持一个 navigationId,并只写一个终态。浏览器可以跳过动画但仍提交 DOM,所以 finished 不等于用户看到了非零时长动画。

在边界处检查该 API。缺少 document.startViewTransition 时,仍执行相同的路由更新,只跳过视觉捕获。不要因为增强能力缺失而改变历史记录语义。

只提交一次 DOM 状态

更新回调是新 DOM 的唯一负责人。保持它同步且短小;在请求过渡前准备好数据,或先提交加载状态。第二次点击要么忽略,要么分配明确的新 navigationId,不能悄悄增加两条历史记录。

let navigationId = 0;

function showRoute(route, render) {
  const id = ++navigationId;
  const commit = () => {
    render(route);
    document.documentElement.dataset.route = route;
  };

  if (!document.startViewTransition) {
    commit();
    return Promise.resolve({ id, mode: 'fallback', outcome: 'committed' });
  }

  const transition = document.startViewTransition(commit);
  return transition.finished.then(
    () => ({ id, mode: 'view-transition', outcome: 'finished' }),
    error => ({ id, mode: 'view-transition', outcome: 'aborted', error: error.name })
  );
}

使用冻结的路由记录渲染,并只更新一次状态仓库。如果观察者在捕获期间修改另一个元素,旧快照和新路由会混在一起;这是应用竞态,不是盲目重试的理由。只把 view-transition-name 用于跨状态代表同一语义对象的元素。重复名称可能导致捕获失败;未命名元素也必须保持可读。

处理 ready、finished 与 abort

transition.ready 在新的伪元素树可动画化时解决,适合添加类名,但不等于用户已经看到新页面。transition.finished 在动画结束或无动画路径完成后解决。用它清理临时类和释放锁,不要把它当作第二次路由事件。

const transition = document.startViewTransition(updateRoute);
const record = { navigationId, ready: false, outcome: 'updating' };

transition.ready.then(
  () => {
    record.ready = true;
    record.outcome = 'animating';
  },
  error => {
    record.outcome = 'aborted';
    record.abortReason = error.name;
  }
);

transition.finished.then(
  () => {
    record.outcome = 'finished';
    releaseRouteLock();
  },
  error => {
    record.outcome = 'aborted';
    record.abortReason = error.name;
    releaseRouteLock();
  }
);

如果更新回调抛出异常,保留可用文档并显示现有错误或重试入口。分开记录 domCommitted 与 animationOutcome;DOM 已提交时不应因为动画失败就无条件回滚。abortReason 可限制为 unsupported、duplicate-name、update-error、reduced-motion、superseded。

新导航取代正在运行的过渡后,旧记录的延迟回调不能再写入结果。新路由应从当前状态渲染,而不是恢复过时快照。

避免重复计算历史和测量

pushState() 或 Navigation API 决定路由是否创建历史条目;startViewTransition() 只负责视觉增强。历史操作执行一次,再把结果路由交给过渡回调。popstate 应沿用路由器既定的前进后退语义,不能因为动画启动而再次 push。

Navigation API 与历史指南说明了条目归属。直接沿用其中的 navigationId,不要再创造一个访问计数器。一个路由事件只能有一个过渡尝试和一个终态视觉结果。API 不存在时也保留同一历史调用,并设置 mode: "fallback"。

只有当问题是“新状态捕获后的布局”时才在 ready 后测量;只有当问题是“动画结束后的稳定布局”时才在 finished 后测量。不要把两次矩形相加。

function measureOnce(phase, navigationId, element) {
  const rect = element.getBoundingClientRect();
  return {
    navigationId,
    phase,
    rect: {
      top: Math.round(rect.top),
      left: Math.round(rect.left),
      width: Math.round(rect.width),
      height: Math.round(rect.height),
    },
  };
}

坐标会受滚动、缩放、字体和路由影响。几何是验收条件时,连同 viewport 和 visualViewport 偏移一起记录;不要把矩形变成身份信号。前进后退由历史事件选择目标条目;更晚请求在渲染前标记为 superseded。

构建减少动效的降级路径

MDN 的 prefers-reduced-motion 参考定义了用户偏好。在选择较长的位移效果前先读取它。状态、焦点、路由和历史照常变化,只改变动画。

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.18s;
  animation-timing-function: ease-out;
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 0s;
    animation-iteration-count: 1;
  }
}

每次导航都读取当前偏好。JavaScript 跳过 API 的分支必须提交相同的 DOM。记录 animationMode: "instant" 和 motionPreference: "reduce",但不要把偏好持久化为身份。

视觉过渡不会自动创建焦点陷阱、可访问名称或读屏播报。按路由契约移动焦点,并把键盘和辅助技术检查与动画分开。新的标题应在声明路由就绪前进入 DOM,避免内容闪烁。

运行受限状态矩阵

使用一个 same-origin 场景、稳定数据,并为每行准备新 profile。

行条件预期结果验证目标
AAPI 可用,普通动效DOM、ready、finished 各一次首选生命周期
BAPI 不可用DOM 与历史相同,mode: fallback兼容路径
C重复过渡名称DOM 可恢复并记录终止原因捕获失败处理
Dprefers-reduced-motion: reduce路由和焦点相同,动画即时完成用户控制
E前进或后退条目一个历史事件,没有额外 pushState历史归属
F过渡中发起新请求旧记录变为 superseded过期回调保护
{
  "fixtureId": "vt-nav-01",
  "navigationId": "route-17",
  "mode": "view-transition",
  "motionPreference": "no-preference",
  "domCommitted": true,
  "ready": true,
  "outcome": "finished",
  "historyAction": "push",
  "measurementPhase": "ready",
  "browserVersion": "155.0"
}

只保留一个 historyAction、一个 navigationId 和至多一个测量阶段。不要把查询参数、页面文字或焦点转录复制到可复用产物中。若 E 产生两个条目,先修路由器再调 CSS;若 D 改变焦点,即使截图正确也属于无障碍回归。

BotBrowser 能力边界

BotBrowser 可以在授权 profile 中加载场景、保持路由和动效偏好并记录可见生命周期标签。它不能强制暴露 startViewTransition,不能保证合成器时序,也不能认证辅助技术播报。重放结果只描述所用浏览器、viewport、profile 和样式表。

来源

实践结论

让路由决策、DOM 提交和终态过渡结果各有一个负责人。用 ready 表示捕获阶段,用 finished 做清理,对不支持或失败路径明确记录 abort。减少或关闭动效时仍保留历史和焦点语义,在一个命名阶段测量几何。BotBrowser 可以重放这份受限矩阵,最终体验仍由应用和无障碍复核负责。

#View Transitions API#导航#无障碍#DOM 状态#Web 平台

本文属于指南: Web 平台能力面:一致性复核地图

让 BotBrowser 从研究走向生产

先用这些指南理解模型,再进入跨平台验证、隔离上下文和面向规模化的浏览器部署。