Modal scale-inWeb · App
Target
Implement the "Page slide transition" effect in vanilla HTML/CSS/JS. Context: On navigation the current view slides out to the left and fades while the next view slides in from the right. Trigger: the route/page changes. Timeline: time-based (duration + easing). Motion tokens (normal): - translateX (leaving): 0 → -30% - opacity (leaving): 1 → 0 - translateX (entering): 100% → 0 - duration: 320ms - easing: cubic-bezier(0.2, 0.8, 0.2, 1) - note: back navigation mirrors the direction Motion tokens (prefers-reduced-motion): - opacity (leaving): 1 → 0 - opacity (entering): 0 → 1 - duration: 200ms - easing: ease-out Technique: prefer pure CSS transitions/animations; use JS only to toggle classes. Acceptance: - animate only transform and opacity unless the spec lists other properties - no layout shift; content is visible without the animation - when prefers-reduced-motion is set, use the reduced tokens only - both views are position-absolute in a clipped container during the transition - the leaving view is removed (or inert) after the animation ends
Generated from the spec. Prompts differ by target name only; the tokens are the source of truth.
demo.html
<div class="fx-page-slide-transition" data-fx-root>
<div class="fx-phone">
<div class="fx-view fx-a"><div class="fx-bar"><span class="fx-title">Inbox</span></div><div class="fx-rows"><span class="fx-r"></span><span class="fx-r"></span><span class="fx-r"></span><span class="fx-r"></span></div></div>
<div class="fx-view fx-b"><div class="fx-bar"><span class="fx-back" aria-hidden="true">‹</span><span class="fx-title">Message</span></div><div class="fx-rows"><span class="fx-r fx-tall"></span><span class="fx-r"></span><span class="fx-r fx-short"></span></div></div>
</div>
</div>
demo.css
.fx-page-slide-transition { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
.fx-page-slide-transition .fx-phone { position: relative; width: 46%; max-width: 220px; aspect-ratio: 9 / 14; max-height: 86%; border-radius: 18px; overflow: hidden; background: #fff; box-shadow: 0 0 0 1px var(--fx-muted, #e4e4e7); }
.fx-page-slide-transition .fx-view { position: absolute; inset: 0; display: flex; flex-direction: column; background: #fff; transition: transform 320ms cubic-bezier(.2, .8, .2, 1), opacity 320ms cubic-bezier(.2, .8, .2, 1); }
.fx-page-slide-transition .fx-bar { height: 40px; display: flex; align-items: center; gap: 6px; padding: 0 12px; border-bottom: 1px solid var(--fx-muted, #e4e4e7); font: 600 13px/1 Inter, system-ui, sans-serif; color: var(--fx-ink, #18181b); }
.fx-page-slide-transition .fx-back { font-size: 20px; color: var(--fx-accent, #4f46e5); line-height: 1; }
.fx-page-slide-transition .fx-rows { display: flex; flex-direction: column; gap: 8px; padding: 12px; }
.fx-page-slide-transition .fx-r { height: 22px; border-radius: 6px; background: var(--fx-muted, #e4e4e7); }
.fx-page-slide-transition .fx-tall { height: 60px; background: #eef2ff; } .fx-page-slide-transition .fx-short { width: 50%; }
.fx-page-slide-transition .fx-b { transform: translateX(100%); }
.fx-page-slide-transition.is-pushed .fx-a { transform: translateX(-30%); opacity: 0; }
.fx-page-slide-transition.is-pushed .fx-b { transform: translateX(0); }
@media (prefers-reduced-motion: reduce) {
.fx-page-slide-transition .fx-view { transition: opacity 200ms ease-out; }
.fx-page-slide-transition .fx-b { transform: none; opacity: 0; }
.fx-page-slide-transition.is-pushed .fx-a { transform: none; } .fx-page-slide-transition.is-pushed .fx-b { opacity: 1; }
}
demo.js
export default function mount(root) {
let t1, t2;
return {
play() { clearTimeout(t1); clearTimeout(t2); root.classList.remove('is-pushed'); t1 = setTimeout(() => root.classList.add('is-pushed'), 500); t2 = setTimeout(() => root.classList.remove('is-pushed'), 2100); },
stop() { clearTimeout(t1); clearTimeout(t2); root.classList.remove('is-pushed'); },
};
}
Inspired by developer.chrome.com — the default iOS push transition, and the View Transitions API. The preview is an original re-creation; the effect name belongs to its author.
Classic two-layer push. With the View Transitions API the same tokens map to `::view-transition-old(root)` / `::view-transition-new(root)` keyframes; keep the CSS version as the fallback for Firefox.
Added 2026-11-09 · Report an issue