AnimationsSimpleApril 8, 2026
View Transitions API
Old page scales down and slides up off screen. New page slides in from below. Uses the browser's native View Transitions API with document.startViewTransition(). No overlay components needed.
View Full Demo →Preview
Home
Uses the native View Transitions API
Source
index.jsx
"use client";
import { useState, useCallback } from "react";
import styles from "./styles.module.css";
const PAGES = [
{ key: "home", label: "Home", bg: "#f5f5f5", color: "#1a1a1a" },
{ key: "about", label: "About", bg: "#0d0d0d", color: "#ffffff" },
{ key: "work", label: "Work", bg: "#1a1f2e", color: "#ffffff" },
];
export default function ViewTransitionsApi() {
const [pageIndex, setPageIndex] = useState(0);
const navigate = useCallback((nextIndex) => {
if (nextIndex === pageIndex) return;
if (!document.startViewTransition) {
setPageIndex(nextIndex);
return;
}
document.startViewTransition(() => {
setPageIndex(nextIndex);
});
}, [pageIndex]);
const page = PAGES[pageIndex];
return (
<div className={styles.demo}>
<div
className={styles.page}
style={{ background: page.bg, color: page.color }}
>
<h2 className={styles.pageTitle}>{page.label}</h2>
<p className={styles.hint}>Uses the native View Transitions API</p>
</div>
<div className={styles.nav}>
{PAGES.map((p, i) => (
<button
key={p.key}
className={`${styles.navBtn} ${i === pageIndex ? styles.active : ""}`}
onClick={() => navigate(i)}
>
{p.label}
</button>
))}
</div>
</div>
);
}
styles.module.css
.demo {
display: flex;
flex-direction: column;
}
.page {
height: 320px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
view-transition-name: page;
}
.pageTitle {
font-size: 2.5rem;
font-weight: 700;
letter-spacing: -0.03em;
}
.hint {
font-size: 0.8125rem;
opacity: 0.4;
}
.nav {
display: flex;
gap: 1px;
background: #e5e5e5;
border-top: 1px solid #e5e5e5;
}
.navBtn {
flex: 1;
padding: 0.875rem;
font-size: 0.875rem;
font-weight: 500;
font-family: inherit;
background: #ffffff;
border: none;
cursor: pointer;
color: #6b6b6b;
transition: background 150ms ease, color 150ms ease;
}
.navBtn:hover {
background: #f5f5f5;
color: #1a1a1a;
}
.navBtn.active {
background: #1a1a1a;
color: #ffffff;
}
/* View Transitions API styles */
@keyframes slide-out-up {
from { opacity: 1; scale: 1; transform: translateY(0); }
to { opacity: 0.5; scale: 0.95; transform: translateY(-80px); }
}
@keyframes slide-in-from-below {
from { opacity: 0; transform: translateY(40px); }
to { opacity: 1; transform: translateY(0); }
}
:global(::view-transition-old(root)) {
animation: slide-out-up 600ms cubic-bezier(0.87, 0, 0.13, 1) both;
}
:global(::view-transition-new(root)) {
animation: slide-in-from-below 600ms cubic-bezier(0.87, 0, 0.13, 1) both;
}