Patterns Medium
Page Routing Transitions
SPA-style page routing with directional slide transitions using the View Transitions API.
Open in Lab
MCP
view-transitions-api spa-routing
Targets: JS HTML
Code
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #070a12;
--text: #f0f4fb;
--muted: #8a95a8;
--accent: #86e8ff;
--border: #263249;
--neon-purple: #ae52ff;
}
body {
background: var(--bg);
color: var(--text);
font-family: "Inter", "SF Pro Display", system-ui, sans-serif;
min-height: 100vh;
overflow-x: hidden;
}
/* Nav */
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 50;
display: flex;
justify-content: center;
gap: 0.25rem;
padding: 1rem 2rem;
background: rgba(7, 10, 18, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(134, 232, 255, 0.08);
}
.nav-link {
padding: 0.5rem 1.2rem;
border-radius: 8px;
color: var(--muted);
text-decoration: none;
font: 600 0.82rem / 1 "Inter", system-ui, sans-serif;
transition: color 0.2s, background 0.2s;
}
.nav-link:hover {
color: var(--accent);
}
.nav-link.active {
color: var(--accent);
background: rgba(134, 232, 255, 0.1);
}
/* Pages */
.page {
display: none;
min-height: 100vh;
padding: 7rem 2rem 4rem;
}
.page.active {
display: flex;
align-items: center;
justify-content: center;
}
.page-content {
max-width: 640px;
width: 100%;
}
.eyebrow {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--accent);
margin-bottom: 1rem;
}
h1 {
font-size: clamp(2.2rem, 5vw, 3.5rem);
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.1;
margin-bottom: 1.2rem;
}
.page-body {
font-size: 1rem;
line-height: 1.7;
margin-bottom: 1rem;
}
.page-body.muted {
color: var(--muted);
}
code {
background: rgba(134, 232, 255, 0.1);
color: var(--accent);
padding: 0.1rem 0.4rem;
border-radius: 4px;
font-size: 0.85rem;
}
/* Features */
.feature-list {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.feature {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.8rem 1rem;
border-radius: 10px;
background: rgba(134, 232, 255, 0.04);
border: 1px solid rgba(134, 232, 255, 0.08);
font-size: 0.9rem;
}
.f-icon {
font-size: 0.7rem;
font-weight: 700;
color: var(--accent);
opacity: 0.5;
}
/* Work grid */
.work-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-top: 1.5rem;
}
.work-item {
aspect-ratio: 1.4;
border-radius: 14px;
background: linear-gradient(135deg, hsl(var(--hue, 200) 40% 12%), hsl(var(--hue, 200) 50% 18%));
border: 1px solid hsl(var(--hue, 200) 30% 25%);
display: flex;
align-items: center;
justify-content: center;
font: 600 0.9rem / 1 "Inter", system-ui, sans-serif;
color: hsl(var(--hue, 200) 60% 75%);
}
.btn-gallery {
display: inline-block;
margin-top: 1.5rem;
padding: 0.7rem 2rem;
border-radius: 999px;
border: 1px solid rgba(134, 232, 255, 0.3);
color: var(--accent);
text-decoration: none;
font: 600 0.85rem / 1 "Inter", system-ui, sans-serif;
transition: all 0.25s;
}
.btn-gallery:hover {
background: rgba(134, 232, 255, 0.08);
border-color: var(--accent);
}
/* View Transition animations */
::view-transition-old(page-title),
::view-transition-new(page-title) {
animation-duration: 0.35s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-old(root) {
animation: slide-out-left 0.3s ease-in forwards;
}
::view-transition-new(root) {
animation: slide-in-right 0.3s ease-out forwards;
}
/* Reverse direction */
.nav-back ::view-transition-old(root) {
animation-name: slide-out-right;
}
.nav-back ::view-transition-new(root) {
animation-name: slide-in-left;
}
@keyframes slide-out-left {
to {
transform: translateX(-30px);
opacity: 0;
}
}
@keyframes slide-in-right {
from {
transform: translateX(30px);
opacity: 0;
}
}
@keyframes slide-out-right {
to {
transform: translateX(30px);
opacity: 0;
}
}
@keyframes slide-in-left {
from {
transform: translateX(-30px);
opacity: 0;
}
}
@media (max-width: 640px) {
.nav {
gap: 0;
}
.nav-link {
padding: 0.5rem 0.8rem;
font-size: 0.75rem;
}
.work-grid {
grid-template-columns: 1fr;
}
}if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
initDemoShell({
title: "Page Routing Transitions",
category: "transitions",
tech: ["view-transitions-api", "spa-routing"],
});
const supportsVT = typeof document.startViewTransition === "function";
const pages = ["home", "about", "work", "contact"];
let currentPage = "home";
let currentIndex = 0;
const navLinks = document.querySelectorAll(".nav-link");
function navigateTo(pageName) {
if (pageName === currentPage) return;
const newIndex = pages.indexOf(pageName);
const goingForward = newIndex > currentIndex;
const updateDOM = () => {
// Hide current page
document.getElementById(`page-${currentPage}`).classList.remove("active");
// Show new page
document.getElementById(`page-${pageName}`).classList.add("active");
// Update nav
navLinks.forEach((link) => {
link.classList.toggle("active", link.dataset.page === pageName);
});
currentPage = pageName;
currentIndex = newIndex;
};
if (supportsVT && !prefersReducedMotion()) {
// Set direction class for CSS animation direction
document.documentElement.classList.toggle("nav-back", !goingForward);
document.startViewTransition(updateDOM);
} else {
updateDOM();
}
}
// Nav click handlers
navLinks.forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
navigateTo(link.dataset.page);
});
});
// Handle hash-based navigation
function handleHash() {
const hash = window.location.hash.replace("#", "") || "home";
if (pages.includes(hash)) {
navigateTo(hash);
}
}
window.addEventListener("hashchange", handleHash);
// Keyboard navigation
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowRight") {
const next = pages[Math.min(currentIndex + 1, pages.length - 1)];
navigateTo(next);
window.location.hash = next;
} else if (e.key === "ArrowLeft") {
const prev = pages[Math.max(currentIndex - 1, 0)];
navigateTo(prev);
window.location.hash = prev;
}
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Routing Transitions — stealthisdesign</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Shared nav (persists across transitions) -->
<nav class="nav" style="view-transition-name: main-nav;">
<a href="#home" class="nav-link active" data-page="home">Home</a>
<a href="#about" class="nav-link" data-page="about">About</a>
<a href="#work" class="nav-link" data-page="work">Work</a>
<a href="#contact" class="nav-link" data-page="contact">Contact</a>
</nav>
<!-- Page container -->
<main id="page-container">
<!-- Home -->
<div class="page active" id="page-home">
<div class="page-content">
<span class="eyebrow">Demo 12</span>
<h1 style="view-transition-name: page-title;">Page Routing<br>Transitions</h1>
<p class="page-body">Click the navigation links above to see smooth View Transitions between pages. The nav bar and page title animate as shared elements.</p>
<p class="page-body muted">Each page slides in while the previous slides out. The nav highlight transitions smoothly between active items.</p>
</div>
</div>
<!-- About -->
<div class="page" id="page-about">
<div class="page-content">
<h1 style="view-transition-name: page-title;">About</h1>
<p class="page-body">This demo simulates multi-page routing using SPA-style DOM swaps wrapped in <code>document.startViewTransition()</code>.</p>
<p class="page-body muted">In a real application, you could use the cross-document View Transitions API with <code>@view-transition { navigation: auto; }</code> for actual multi-page apps.</p>
<div class="feature-list">
<div class="feature"><span class="f-icon">01</span><span>Shared element navigation bar</span></div>
<div class="feature"><span class="f-icon">02</span><span>Page title morphing</span></div>
<div class="feature"><span class="f-icon">03</span><span>Slide direction based on nav position</span></div>
<div class="feature"><span class="f-icon">04</span><span>Fallback for unsupported browsers</span></div>
</div>
</div>
</div>
<!-- Work -->
<div class="page" id="page-work">
<div class="page-content">
<h1 style="view-transition-name: page-title;">Work</h1>
<p class="page-body">A portfolio-style page demonstrating how content transitions can maintain context during navigation.</p>
<div class="work-grid">
<div class="work-item" style="--hue: 200;">Scroll Animations</div>
<div class="work-item" style="--hue: 270;">WebGL Scenes</div>
<div class="work-item" style="--hue: 330;">View Transitions</div>
<div class="work-item" style="--hue: 45;">Canvas Effects</div>
</div>
</div>
</div>
<!-- Contact -->
<div class="page" id="page-contact">
<div class="page-content">
<h1 style="view-transition-name: page-title;">Contact</h1>
<p class="page-body">The final page in the routing demo. Navigate back to any other page to see the reverse transition direction.</p>
<a href="/" class="btn-gallery">Back to Showcase</a>
</div>
</div>
</main>
<script src="script.js"></script>
</body>
</html>Page Routing Transitions
SPA-style page routing with directional slide transitions using the View Transitions API.
Source
- Repository:
libs-genclaude - Original demo id:
12-page-routing
Notes
SPA-style page routing with directional slide transitions using the View Transitions API.