direction-aware-transitionDirection-aware transition
Match enter and exit directions to navigation order and preserve context.
Preview
Navigation motion uses the route direction to choose opposing travel paths for forward and reverse actions.
Implement Direction-aware transition: Match enter and exit directions to navigation order and preserve context. Use Duration 240ms, Distance 40px, Direction left, Easing soft. Trigger it once when state changes and keep it replayable. Use semantic HTML, scope CSS to .motion-demo--direction-aware-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--direction-aware-transition" data-motion="direction-aware-transition" aria-label="Direction-aware 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">Direction-aware 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--direction-aware-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--direction-aware-transition *, .motion-demo.motion-demo--direction-aware-transition *::before, .motion-demo.motion-demo--direction-aware-transition *::after {
box-sizing: border-box;
}
.motion-demo.motion-demo--direction-aware-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--direction-aware-transition .motion-surface strong { font-weight: 500; }
.motion-demo.motion-demo--direction-aware-transition .motion-line {
display: block;
width: 100%;
height: 0.5rem;
background: #e4e4e7;
border-radius: 999px;
}
.motion-demo.motion-demo--direction-aware-transition .motion-line:last-child { width: 62%; }
.motion-demo.motion-demo--direction-aware-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--direction-aware-transition .motion-button:focus-visible { outline: 3px solid rgb(37 99 235 / 32%); outline-offset: 3px; }
.motion-demo.motion-demo--direction-aware-transition .motion-action-row {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 0.85rem;
}
.motion-demo.motion-demo--direction-aware-transition .motion-replay-scene {
width: min(100%, 22rem);
display: grid;
place-items: center;
}
.motion-demo.motion-demo--direction-aware-transition .motion-pausable-scene {
width: min(100%, 22rem);
display: grid;
place-items: center;
}
.motion-demo.motion-demo--direction-aware-transition button.motion-surface {
color: inherit;
font: inherit;
text-align: start;
cursor: pointer;
}
.motion-demo.motion-demo--direction-aware-transition .motion-replay-button,
.motion-demo.motion-demo--direction-aware-transition .motion-undo-button,
.motion-demo.motion-demo--direction-aware-transition .motion-dismiss-button,
.motion-demo.motion-demo--direction-aware-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--direction-aware-transition .motion-replay-button:focus-visible,
.motion-demo.motion-demo--direction-aware-transition .motion-undo-button:focus-visible,
.motion-demo.motion-demo--direction-aware-transition .motion-dismiss-button:focus-visible,
.motion-demo.motion-demo--direction-aware-transition .motion-pause-button:focus-visible,
.motion-demo.motion-demo--direction-aware-transition [data-reorder-item]:focus-visible,
.motion-demo.motion-demo--direction-aware-transition [data-swipe-target]:focus-visible {
outline: 3px solid rgb(37 99 235 / 32%);
outline-offset: 3px;
}
.motion-demo.motion-demo--direction-aware-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--direction-aware-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--direction-aware-transition .motion-state-stack { position: relative; width: min(100%, 18rem); height: 9rem; }
.motion-demo.motion-demo--direction-aware-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--direction-aware-transition .motion-state--to { background: #18181b; color: white; }
.motion-demo.motion-demo--direction-aware-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--direction-aware-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--direction-aware-transition .motion-text-stack { position: relative; width: min(100%, 18rem); min-height: 4rem; }
.motion-demo.motion-demo--direction-aware-transition svg { width: min(100%, 20rem); height: auto; }
.motion-demo.motion-demo--direction-aware-transition .motion-state--from { animation: motion-direction-aware-transition-out 240ms cubic-bezier(0.23, 1, 0.32, 1) both; }
.motion-demo.motion-demo--direction-aware-transition .motion-state--to { animation: motion-direction-aware-transition-in 240ms cubic-bezier(0.23, 1, 0.32, 1) both; }
@keyframes motion-direction-aware-transition-out { to { opacity: 0; transform: translateX(40px); } }
@keyframes motion-direction-aware-transition-in { from { opacity: 0; transform: translateX(-40px); } to { opacity: 1; transform: none; } }
@keyframes motion-direction-aware-transition-reduced-fade {
from { opacity: 0.84; }
to { opacity: 1; }
}
.force-reduced-motion .motion-demo.motion-demo--direction-aware-transition {
animation: motion-direction-aware-transition-reduced-fade 160ms cubic-bezier(0.23, 1, 0.32, 1) both !important;
}
.force-reduced-motion .motion-demo.motion-demo--direction-aware-transition *,
.force-reduced-motion .motion-demo.motion-demo--direction-aware-transition *::before,
.force-reduced-motion .motion-demo.motion-demo--direction-aware-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--direction-aware-transition .motion-button,
.force-reduced-motion .motion-demo.motion-demo--direction-aware-transition [data-reorder-item],
.force-reduced-motion .motion-demo.motion-demo--direction-aware-transition [data-swipe-target],
.force-reduced-motion .motion-demo.motion-demo--direction-aware-transition [data-spring-target] {
transform: none !important;
}
.force-reduced-motion .motion-demo.motion-demo--direction-aware-transition .motion-path { stroke-dashoffset: 0 !important; }
.force-reduced-motion .motion-demo.motion-demo--direction-aware-transition .motion-word--from { opacity: 0 !important; visibility: hidden; }
.force-reduced-motion .motion-demo.motion-demo--direction-aware-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--direction-aware-transition {
animation: motion-direction-aware-transition-reduced-fade 160ms cubic-bezier(0.23, 1, 0.32, 1) both !important;
}
.motion-demo.motion-demo--direction-aware-transition *, .motion-demo.motion-demo--direction-aware-transition *::before, .motion-demo.motion-demo--direction-aware-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--direction-aware-transition .motion-button,
.motion-demo.motion-demo--direction-aware-transition [data-reorder-item],
.motion-demo.motion-demo--direction-aware-transition [data-swipe-target],
.motion-demo.motion-demo--direction-aware-transition [data-spring-target] {
transform: none !important;
}
.motion-demo.motion-demo--direction-aware-transition .motion-path { stroke-dashoffset: 0 !important; }
.motion-demo.motion-demo--direction-aware-transition .motion-word--from { opacity: 0 !important; visibility: hidden; }
.motion-demo.motion-demo--direction-aware-transition .motion-word:not(.motion-word--from) { opacity: 1 !important; filter: none !important; transform: none !important; }
}(() => {
const config = {
"id": "direction-aware-transition",
"duration": 240,
"distance": 40,
"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=\"direction-aware-transition\"]").forEach((root) => {
const cleanupKey = "__motionLexiconCleanup";
if (typeof root[cleanupKey] === "function") root[cleanupKey]();
root[cleanupKey] = mount(root, config);
});
})();Learn more
Terms and distinctions
direction-aware-transitionDirection-aware transition
方向感过渡Navigation motion uses the route direction to choose opposing travel paths for forward and reverse actions.
前进时内容沿一个方向滑动,返回时沿相反方向滑动,让导航具备方向感。
Before implementation
- Purpose
- Use opposite directions for forward and back navigation to preserve sequence and hierarchy.
- Frequency
- Use for medium-frequency step flows, pagination, and history navigation; apply to peer tabs only when direction is meaningful.
- Trigger
- Trigger from next, previous, browser back, or ordered-index changes.
- Enter / exit
- On forward navigation, new content enters from ahead as old content exits behind; reverse the mapping on back.
- Interruptibility
- Rapid navigation takes over at current progress and renders only the current and latest target.
- Gesture and keyboard
- Swipe pagination follows gesture direction; buttons and keyboard navigation use the same spatial rule.
- Reduced-motion strategy
- Use a short crossfade while preserving forward/back labels and focus position.
Accessibility
Use a short crossfade while preserving forward/back labels and focus position.
Parameter reference
| Parameter | Range / options | Default | Purpose |
|---|---|---|---|
duration | 80ms–1200ms | 240ms | Controls the time required for one motion cycle. |
distance | 0px–120px | 40px | 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
- Test forward direction under RTL layouts.
- Align visual order, DOM order, and navigation history.