web-animation-view-transitions-82d500 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-animation-view-transitions-82d500 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: The browser screenshots the old state, holds the new one live, and animates between them as a tree of pseudo-elements.document.startViewTransition()drives a same-document change;@view-transition { navigation: auto }on both pages drives a cross-document one.view-transition-namepulls an element out of the root snapshot so it can travel on its own, and the whole animation is customised in CSS rather than in script.
Detailed Resources:
startViewTransition() takesthe update as a callback and captures around it; follow examples/spa.md.
@view-transition { navigation: auto }, and names are set through the pageswap and pagereveal events; follow examples/shared-elements.md.
it needs a view-transition-name matched on both sides; follow examples/shared-elements.md.
<critical_requirements>
Guard `startViewTransition` behind a support check, and run the update either way. The call is absent rather than inert where the API is unimplemented, so an unguarded call throws and the DOM change never happens at all.
Give each participating element a name no other visible element carries. The name identifies one snapshot; two visible elements claiming the same one abort the whole transition, not just their own part of it.
Clear a dynamically assigned `view-transition-name` once `transition.finished` resolves. A name left on an element collides with the next transition that assigns it, which is why the failure shows up on the second navigation rather than the first.
Give every customised transition a `prefers-reduced-motion` branch. A page-level transition moves the entire viewport, which is the class of motion the preference exists for; collapse the duration or skip the transition and apply the update directly.
</critical_requirements>
Auto-detection: startViewTransition, view-transition-name, view-transition-class, @view-transition, ::view-transition-old, ::view-transition-new, ::view-transition-group, ::view-transition-image-pair, :active-view-transition-type, pageswap, pagereveal, updateCallbackDone, skipTransition, match-element
Applies to:
Handled elsewhere:
before-and-after pair to capture
<philosophy>
A view transition is a screenshot and a live view, animated against each other. Everything follows from that: the old side is inert pixels, so anything moving inside it freezes; naming an element lifts it into its own snapshot pair with its own animation; and the whole tree is styled with CSS because it is a tree of pseudo-elements, not a script-driven animation.
The DOM update itself stays ordinary. startViewTransition wraps a change that would have happened anyway, which is why the fallback for an unsupported browser is simply making the change.
</philosophy>
<decision_framework>
Does an element exist on both sides and represent the same thing?
├─ YES -> give it one view-transition-name on both sides; it gets its own group and travels
└─ NO -> leave it in the root snapshot; it cross-fades with everything else around itEach named element adds a group, an image pair and two snapshots to the tree, so naming everything costs more than it buys — name what a reader would follow with their eyes.
Cross-fade at a different duration -> set animation-duration on ::view-transition-old/new(root)
Slide, scale, wipe -> keyframes on the old and new pseudo-elements
Direction-dependent motion -> a transition type, selected with :active-view-transition-type()
Geometry computed at the moment of the transition (a reveal from a click point)
-> await transition.ready, then animate the pseudo-elementCSS is right for an element that is unique on the page — a header, a hero, a single panel. Script is right for anything appearing more than once, because a static rule over a list gives every row the same name and breaks the transition; assign per element before the transition and clear after it, or use view-transition-name: match-element where support allows.
</decision_framework>
<patterns>
const SUPPORTS_VIEW_TRANSITIONS =
typeof document !== "undefined" && "startViewTransition" in document;
function updateWithTransition(updateFn: () => void | Promise<void>): void {
if (!SUPPORTS_VIEW_TRANSITIONS) {
updateFn();
return;
}
document.startViewTransition(() => updateFn());
}The typeof document guard matters during server rendering, where there is no document to probe.
Full code: examples/core.md
The callback performs the DOM update; the browser captures before calling it and again after it resolves.
const transition = document.startViewTransition(async () => {
await updateFn();
});
// Options form: classify the transition for CSS to select on
const transition = document.startViewTransition({
update: () => updateDOM(),
types: ["slide-forward"],
});
await transition.finished;| Promise | Resolves when |
|---|---|
transition.updateCallbackDone | The DOM update callback has finished |
transition.ready | The pseudo-element tree exists and animation is next |
transition.finished | The animation is over and the new view is live |
An async callback holds the page frozen until it resolves, so anything slow inside it is a visible stall — update to a loading state rather than awaiting a network round trip.
Full code: examples/core.md
/* on both the source and the destination document */
@view-transition {
navigation: auto;
}No script and no router involvement; it covers push, replace and traverse navigations within one origin. Only one side opting in yields no transition at all.
Full code: examples/shared-elements.md
Matching names on either side of the change make one element travel instead of cross-fading.
.product-thumbnail {
view-transition-name: product-hero;
}
.product-image {
view-transition-name: product-hero;
}
::view-transition-group(product-hero) {
animation-duration: var(--hero-duration);
animation-timing-function: var(--hero-easing);
}The group animates position and size; the image pair cross-fades the two snapshots inside it. Where the two sides differ in aspect ratio, object-fit on the old and new pseudo-elements stops the snapshot squashing.
Full code: examples/shared-elements.md
The default cross-fade is a UA stylesheet animation on the old and new pseudo-elements; overriding it is ordinary CSS.
::view-transition-old(root) {
animation: slide-out-left var(--transition-duration) var(--transition-easing);
}
::view-transition-new(root) {
animation: slide-in-right var(--transition-duration) var(--transition-easing);
}Full code: examples/core.md
A transition type is a label the CSS can select on, so forward and back share one set of keyframes and differ only in which pair is applied.
html:active-view-transition-type(forwards) {
&::view-transition-old(content) {
animation-name: slide-out-left;
}
&::view-transition-new(content) {
animation-name: slide-in-right;
}
}
html:active-view-transition-type(backwards) {
&::view-transition-old(content) {
animation-name: slide-out-right;
}
&::view-transition-new(content) {
animation-name: slide-in-left;
}
}document.startViewTransition({
update: () => navigateForward(),
types: ["forwards"],
});transition.types is a mutable set on an existing transition, for the case where the direction is only known after the update has started.
Full code: examples/spa.md
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.01ms !important;
}
}const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
function shouldEnableTransitions(): boolean {
if (window.matchMedia(REDUCED_MOTION_QUERY).matches) return false;
return "startViewTransition" in document;
}The CSS form collapses the animation while keeping the transition machinery, which is enough for a cross-fade. The script form skips the transition entirely, which is the honest answer for a full-viewport slide or a reveal. Between them sits a third: cancel the animation on ::view-transition-group(*), which is what carries the size and position change, and leave the old and new pseudo-elements cross-fading. That drops the travel without dropping the feedback.
Full code: examples/spa.md
Where the animation depends on something known only at the moment of the click, await transition.ready and animate the pseudo-element directly.
const transition = document.startViewTransition(updateFn);
await transition.ready;
document.documentElement.animate(
{
clipPath: [`circle(0 at ${x}px ${y}px)`, `circle(${r}px at ${x}px ${y}px)`],
},
{
duration: REVEAL_DURATION_MS,
easing: REVEAL_EASING,
pseudoElement: "::view-transition-new(root)",
},
);The animation targets the document element with a pseudoElement option, because the pseudo-element has no node of its own to address.
Full code: examples/spa.md
</patterns>
<red_flags>
Breaks at runtime:
document.startViewTransition(...) with no support check — throws where the API is absent, so theDOM update is lost along with the animation — branch on "startViewTransition" in document
view-transition-name — the whole transition is abandoned,including the parts that were correct — assign per element from script, or use match-element
view-transition-name set in CSS on a list selector — every row claims the same name — set itfrom script before the transition and clear it after
transition.finished — collides on the next transition, so the failureappears one navigation later than the cause — clear it in the finished handler
@view-transition on only one of the two documents — cross-document transitions need both sides toopt in — add the at-rule to the destination as well
transition.ready resolves — the tree does not exist yet and theanimation silently targets nothing — await the promise first
prefers-reduced-motion branch — a full-viewport slide reaches users who asked for none — addthe media query, or skip the transition in script
<meta name="view-transition" content="same-origin"> — obsolete and ignored — use the@view-transition at-rule
Surprising behaviour:
live and keeps playing
far apart
interaction latency — under 300ms for anything on a navigation path
match-element needs Chrome 137+ or Safari 18.4+ and is unavailable in Firefoxpagereveal handler registered late misses the event — put it in <head>, or mark the scriptblocking="render"
auto, inherit, none and unset are CSS-wide keywords rather than custom identifiers, so theycannot be used as names
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.