page-transitionPage transition
Preserve continuity across page or view changes with a safe fallback.
Preview
Route navigation is accompanied by coordinated exit and entrance states for page-level content.
Implement Page transition: Preserve continuity across page or view changes with a safe fallback. Use Duration 280ms, Distance 32px, Direction left, Easing soft. Trigger it once when state changes and keep it replayable. Use semantic HTML, scope CSS to .motion-demo--page-transition, and mount the provided vanilla JavaScript interaction runtime. Provide equivalent low-motion behavior for prefers-reduced-motion and .force-reduced-motion.
<div class="motion-demo motion-demo--page-transition" data-motion="page-transition" aria-label="Page transition demo">
<div class="motion-replay-scene">
<div class="motion-state-stack"><article class="motion-state motion-state--from">Previous</article><article class="motion-state motion-state--to">Page transition</article></div>
<div class="motion-action-row"><button class="motion-replay-button" type="button" data-motion-replay>Replay motion</button></div>
</div>
</div>.motion-demo.motion-demo--page-transition {
box-sizing: border-box;
position: relative;
display: grid;
place-items: center;
width: min(100%, 34rem);
min-height: 15rem;
padding: 2rem;
overflow: hidden;
color: #18181b;
background: #fafafa;
border: 1px solid #e4e4e7;
border-radius: 16px;
isolation: isolate;
}
.motion-demo.motion-demo--page-transition *, .motion-demo.motion-demo--page-transition *::before, .motion-demo.motion-demo--page-transition *::after {
box-sizing: border-box;
}
.motion-demo.motion-demo--page-transition .motion-surface {
position: relative;
display: grid;
gap: 0.65rem;
width: min(100%, 18rem);
padding: 1.25rem;
background: #ffffff;
border: 1px solid #e4e4e7;
border-radius: 16px;
box-shadow: none;
}
.motion-demo.motion-demo--page-transition .motion-surface strong { font-weight: 500; }
.motion-demo.motion-demo--page-transition .motion-line {
display: block;
width: 100%;
height: 0.5rem;
background: #e4e4e7;
border-radius: 999px;
}
.motion-demo.motion-demo--page-transition .motion-line:last-child { width: 62%; }
.motion-demo.motion-demo--page-transition .motion-button {
position: relative;
min-height: 2.75rem;
padding: 0.75rem 1.1rem;
color: white;
font: inherit;
font-weight: 700;
background: #18181b;
border: 0;
border-radius: 999px;
cursor: pointer;
}
.motion-demo.motion-demo--page-transition .motion-button:focus-visible { outline: 3px solid rgb(37 99 235 / 32%); outline-offset: 3px; }
.motion-demo.motion-demo--page-transition .motion-action-row {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 0.85rem;
}
.motion-demo.motion-demo--page-transition .motion-replay-scene {
width: min(100%, 22rem);
display: grid;
place-items: center;
}
.motion-demo.motion-demo--page-transition .motion-pausable-scene {
width: min(100%, 22rem);
display: grid;
place-items: center;
}
.motion-demo.motion-demo--page-transition button.motion-surface {
color: inherit;
font: inherit;
text-align: start;
cursor: pointer;
}
.motion-demo.motion-demo--page-transition .motion-replay-button,
.motion-demo.motion-demo--page-transition .motion-undo-button,
.motion-demo.motion-demo--page-transition .motion-dismiss-button,
.motion-demo.motion-demo--page-transition .motion-pause-button {
min-height: 2.75rem;
padding: 0.65rem 0.9rem;
color: #18181b;
font: 650 0.78rem/1 ui-sans-serif, system-ui, sans-serif;
background: #ffffff;
border: 1px solid #d4d4d8;
border-radius: 8px;
cursor: pointer;
}
.motion-demo.motion-demo--page-transition .motion-replay-button:focus-visible,
.motion-demo.motion-demo--page-transition .motion-undo-button:focus-visible,
.motion-demo.motion-demo--page-transition .motion-dismiss-button:focus-visible,
.motion-demo.motion-demo--page-transition .motion-pause-button:focus-visible,
.motion-demo.motion-demo--page-transition [data-reorder-item]:focus-visible,
.motion-demo.motion-demo--page-transition [data-swipe-target]:focus-visible {
outline: 3px solid rgb(37 99 235 / 32%);
outline-offset: 3px;
}
.motion-demo.motion-demo--page-transition .motion-status {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.motion-demo.motion-demo--page-transition .motion-ripple-ink {
position: absolute;
width: 12px;
height: 12px;
pointer-events: none;
background: rgb(255 255 255 / 72%);
border-radius: 50%;
transform: translate(-50%, -50%) scale(0.95);
opacity: 0;
}
.motion-demo.motion-demo--page-transition .motion-state-stack { position: relative; width: min(100%, 18rem); height: 9rem; }
.motion-demo.motion-demo--page-transition .motion-state { position: absolute; inset: 0; display: grid; place-items: center; padding: 1rem; background: #ffffff; border: 1px solid #e4e4e7; border-radius: 16px; }
.motion-demo.motion-demo--page-transition .motion-state--to { background: #18181b; color: white; }
.motion-demo.motion-demo--page-transition .motion-scroll-track > span { color: #71717a; font: 600 0.72rem/1.2 ui-monospace, monospace; letter-spacing: 0.08em; text-transform: uppercase; }
.motion-demo.motion-demo--page-transition .motion-orbit-item { display: grid; place-items: center; width: 2.5rem; height: 2.5rem; color: white; background: #2563eb; border-radius: 50%; }
.motion-demo.motion-demo--page-transition .motion-text-stack { position: relative; width: min(100%, 18rem); min-height: 4rem; }
.motion-demo.motion-demo--page-transition svg { width: min(100%, 20rem); height: auto; }
.motion-demo.motion-demo--page-transition .motion-state--from { animation: motion-page-transition-out 280ms cubic-bezier(0.23, 1, 0.32, 1) both; }
.motion-demo.motion-demo--page-transition .motion-state--to { animation: motion-page-transition-in 280ms cubic-bezier(0.23, 1, 0.32, 1) both; }
::view-transition-old(root) { animation: motion-page-transition-out 280ms cubic-bezier(0.23, 1, 0.32, 1) both; }
::view-transition-new(root) { animation: motion-page-transition-in 280ms cubic-bezier(0.23, 1, 0.32, 1) both; }
@keyframes motion-page-transition-out { to { opacity: 0; transform: translateX(32px); } }
@keyframes motion-page-transition-in { from { opacity: 0; transform: translateX(-32px); } to { opacity: 1; transform: none; } }
@keyframes motion-page-transition-reduced-fade {
from { opacity: 0.84; }
to { opacity: 1; }
}
.force-reduced-motion .motion-demo.motion-demo--page-transition {
animation: motion-page-transition-reduced-fade 160ms cubic-bezier(0.23, 1, 0.32, 1) both !important;
}
.force-reduced-motion .motion-demo.motion-demo--page-transition *,
.force-reduced-motion .motion-demo.motion-demo--page-transition *::before,
.force-reduced-motion .motion-demo.motion-demo--page-transition *::after {
animation: none !important;
animation-delay: 0ms !important;
animation-iteration-count: 1 !important;
transition-property: opacity, color, background-color, border-color !important;
transition-duration: 160ms !important;
transition-delay: 0ms !important;
scroll-behavior: auto !important;
}
.force-reduced-motion .motion-demo.motion-demo--page-transition .motion-button,
.force-reduced-motion .motion-demo.motion-demo--page-transition [data-reorder-item],
.force-reduced-motion .motion-demo.motion-demo--page-transition [data-swipe-target],
.force-reduced-motion .motion-demo.motion-demo--page-transition [data-spring-target] {
transform: none !important;
}
.force-reduced-motion .motion-demo.motion-demo--page-transition .motion-path { stroke-dashoffset: 0 !important; }
.force-reduced-motion .motion-demo.motion-demo--page-transition .motion-word--from { opacity: 0 !important; visibility: hidden; }
.force-reduced-motion .motion-demo.motion-demo--page-transition .motion-word:not(.motion-word--from) { opacity: 1 !important; filter: none !important; transform: none !important; }
@media (prefers-reduced-motion: reduce) {
.motion-demo.motion-demo--page-transition {
animation: motion-page-transition-reduced-fade 160ms cubic-bezier(0.23, 1, 0.32, 1) both !important;
}
.motion-demo.motion-demo--page-transition *, .motion-demo.motion-demo--page-transition *::before, .motion-demo.motion-demo--page-transition *::after {
animation: none !important;
animation-delay: 0ms !important;
animation-iteration-count: 1 !important;
transition-property: opacity, color, background-color, border-color !important;
transition-duration: 160ms !important;
transition-delay: 0ms !important;
scroll-behavior: auto !important;
}
.motion-demo.motion-demo--page-transition .motion-button,
.motion-demo.motion-demo--page-transition [data-reorder-item],
.motion-demo.motion-demo--page-transition [data-swipe-target],
.motion-demo.motion-demo--page-transition [data-spring-target] {
transform: none !important;
}
.motion-demo.motion-demo--page-transition .motion-path { stroke-dashoffset: 0 !important; }
.motion-demo.motion-demo--page-transition .motion-word--from { opacity: 0 !important; visibility: hidden; }
.motion-demo.motion-demo--page-transition .motion-word:not(.motion-word--from) { opacity: 1 !important; filter: none !important; transform: none !important; }
}(() => {
const config = {
"id": "page-transition",
"duration": 280,
"distance": 32,
"resistance": 65,
"position": 50,
"stiffness": 220,
"damping": 24,
"mass": 1,
"velocity": 0,
"rippleSize": 220,
"rippleOpacity": 24,
"dragScale": 103,
"autoplay": false
};
const mount = (root, config) => {
const cleanups = [];
const addCleanup = (callback) => cleanups.push(callback);
const cleanup = () => {
for (let index = cleanups.length - 1; index >= 0; index -= 1) {
try { cleanups[index](); } catch { /* keep cleanup reentrant */ }
}
cleanups.length = 0;
};
const on = (target, type, listener, options) => {
target.addEventListener(type, listener, options);
addCleanup(() => target.removeEventListener(type, listener, options));
};
const reduced = () => root.closest(".force-reduced-motion") !== null ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const watchReduced = (callback) => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
const notify = () => callback(reduced());
media.addEventListener("change", notify);
const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(notify);
let ancestor = root;
while (observer && ancestor) {
observer.observe(ancestor, { attributes: true, attributeFilter: ["class"] });
ancestor = ancestor.parentElement;
}
addCleanup(() => {
media.removeEventListener("change", notify);
if (observer) observer.disconnect();
});
};
const replay = root.querySelector("[data-motion-replay]");
if (!replay) return cleanup;
const restart = () => {
root.getAnimations({ subtree: true }).forEach((animation) => {
animation.cancel();
animation.currentTime = 0;
animation.play();
});
};
addCleanup(() => root.getAnimations({ subtree: true }).forEach((animation) => {
if (animation.playState === "paused") animation.play();
}));
on(replay, "click", restart);
return cleanup;
};
document.querySelectorAll("[data-motion=\"page-transition\"]").forEach((root) => {
const cleanupKey = "__motionLexiconCleanup";
if (typeof root[cleanupKey] === "function") root[cleanupKey]();
root[cleanupKey] = mount(root, config);
});
})();Learn more
Terms and distinctions
page-transitionPage transition
页面过渡Route navigation is accompanied by coordinated exit and entrance states for page-level content.
从一个页面或路由导航到另一个页面或路由时播放的动画。
view-transitionView transition
视图过渡The browser captures old and new render states, then animates their snapshots and designated shared elements.
浏览器在两个状态或页面之间进行形变过渡,并连接共享元素。
View transition refers to the browser connecting DOM states or page snapshots; Page transition covers route-change animation more broadly.
Before implementation
- Purpose
- Preserve context across page or route changes and explain the new page's hierarchical source.
- Frequency
- Use for low- to medium-frequency navigation, keeping primary task paths brief and skippable.
- Trigger
- Trigger after route intent is confirmed, managing data waiting separately from visual transition.
- Enter / exit
- Exit the old page quickly and enter the new page when interactive content is ready, preserving shared-element identity.
- Interruptibility
- New navigation replaces the previous target immediately, and browser back resumes from current transition state.
- Gesture and keyboard
- Respect platform swipe-back gestures while keeping links, keyboard, and history navigation equally reachable.
- Reduced-motion strategy
- Use immediate routing with a short content fade while preserving loading and focus feedback.
Accessibility
Use immediate routing with a short content fade while preserving loading and focus feedback.
Parameter reference
| Parameter | Range / options | Default | Purpose |
|---|---|---|---|
duration | 80ms–1200ms | 280ms | Controls the time required for one motion cycle. |
distance | 0px–120px | 32px | Controls how far the element travels. |
direction | Up / Down / Left / Right / Exit | Left | Selects direction. |
ease | Soft / Snap / Calm / Linear | Soft | Controls how velocity changes over time. |
Pre-delivery review
- Do not let transition overlays delay link response.
- Verify forward, back, refresh, and deep-link paths.