0 Pure CSS7 with JSPublished 2026-10-05Free · no sign-up
Seven ways to move between pages or screens: a cross-fade, a slide that follows the direction of travel, a View Transitions API morph, a curtain wipe, a circle that grows from the clicked link, content that staggers in and a push transition like a phone app.
A mini app with two pages and a tab bar: the outgoing page fades out and the new one fades in. Pages are stacked in one grid cell, the hidden one is inert so it cannot be focused, and aria-current marks the active tab.
View code
<div class="pt1"><div class="pages"><section class="on"><h3>Home</h3><p>Recent orders and quick actions.</p></section><section inert><h3>Settings</h3><p>Account, billing and team.</p></section></div><nav aria-label="Demo"><button aria-current="page">Home</button><button>Settings</button></nav></div>
var w = document.querySelector('.pt1'), ss = w.querySelectorAll('section'), bs = w.querySelectorAll('nav button');
bs.forEach(function (b, i) { b.onclick = function () {
ss.forEach(function (s, j) { s.classList.toggle('on', i === j); s.inert = i !== j; });
bs.forEach(function (x, j) { if (i === j) x.setAttribute('aria-current', 'page'); else x.removeAttribute('aria-current'); });
}; });
02. Direction-Aware Slide
TransitionSlide
Going forward slides the new page in from the right and going back slides it in from the left, which tells people where they are in a sequence. The direction is a data attribute on the container that the keyframes read through two animation names.
var w = document.querySelector('.pt2'), s = w.querySelector('section'), n = 1;
var text = ['Choose a template.', 'Add your products.', 'Pick a domain.', 'Go live.'];
function go(d) {
var k = Math.min(4, Math.max(1, n + d)); if (k === n) return; n = k;
w.dataset.dir = d > 0 ? 'fwd' : 'back';
s.classList.remove('go'); void s.offsetWidth; s.classList.add('go');
s.querySelector('h3').textContent = 'Step ' + n; s.querySelector('p').textContent = text[n - 1];
}
w.querySelector('.n').onclick = function () { go(1); };
w.querySelector('.b').onclick = function () { go(-1); };
03. View Transitions API Swap
TransitionView Transitions
document.startViewTransition() snapshots the old state, runs your DOM update and cross-fades to the new state, and a shared view-transition-name morphs the thumbnail into the hero image. Browsers without the API simply update instantly, so nothing breaks.
A coloured panel sweeps across, covers the old page, the content swaps underneath, and the panel sweeps away. It is two keyframe halves on one overlay, and the swap happens on the animation's midpoint using a timer that matches the CSS duration.
View code
<div class="pt4"><div class="pg"><h3>Work</h3><p>Selected projects 2024-2026.</p></div><div class="cv" aria-hidden="true"></div><button>Go to About</button></div>
var w = document.querySelector('.pt4'), cv = w.querySelector('.cv'), b = w.querySelector('button'), about = false;
b.onclick = function () {
var swap = function () { about = !about; w.querySelector('h3').textContent = about ? 'About' : 'Work'; w.querySelector('p').textContent = about ? 'A two-person studio in Bristol.' : 'Selected projects 2024-2026.'; b.textContent = about ? 'Go to Work' : 'Go to About'; };
if (matchMedia('(prefers-reduced-motion: reduce)').matches) { swap(); return; }
cv.classList.remove('run'); void cv.offsetWidth; cv.classList.add('run'); setTimeout(swap, 450);
};
05. Circle Reveal from the Clicked Link
TransitionClip-path
The next page grows out of the link you clicked as an expanding circle, made by animating clip-path: circle() from the pointer's coordinates. The position is passed in two custom properties so the same keyframes work from any link.
var w = document.querySelector('.pt5'), s = w.querySelector('section'), last;
w.querySelectorAll('ul button').forEach(function (b) { b.onclick = function (e) {
var r = w.getBoundingClientRect(), br = b.getBoundingClientRect();
s.style.setProperty('--x', (e.clientX ? e.clientX - r.left : br.left - r.left + 20) + 'px');
s.style.setProperty('--y', (e.clientY ? e.clientY - r.top : br.top - r.top + 20) + 'px');
s.querySelector('h3').textContent = b.textContent; s.hidden = false; last = b; s.querySelector('.x').focus();
}; });
s.querySelector('.x').onclick = function () { s.hidden = true; last.focus(); };
06. Staggered Content on Page Change
TransitionStagger
When the page changes, its heading, text and list items animate in one after another rather than all at once. Each element gets an index custom property, and a single keyframe uses it for its delay, so adding an item needs no new CSS.
View code
<div class="pt6"><button>Show next page</button><section><h3 style="--i:0">Features</h3><p style="--i:1">Everything included in every plan.</p><ul><li style="--i:2">Unlimited projects</li><li style="--i:3">Version history</li><li style="--i:4">Custom domains</li></ul></section></div>
var s = document.querySelector('.pt6 section'), h = s.querySelector('h3'), pages = [['Features', 'Everything included in every plan.'], ['Security', 'How we keep your data safe.'], ['Support', 'Help when you need it.']], k = 0;
document.querySelector('.pt6 button').onclick = function () {
k = (k + 1) % pages.length; h.textContent = pages[k][0]; s.querySelector('p').textContent = pages[k][1];
s.classList.remove('go'); void s.offsetWidth; s.classList.add('go');
};
07. Push and Scale Like a Native App
TransitionMobile
Opening a detail screen pushes it in from the right while the list behind it scales down and dims, the way phone apps move between levels. Both screens live in the frame, the hidden one is inert, and Back reverses the same transitions.
View code
<div class="pt7"><section class="list"><h3>Inbox</h3><button>Lunch on Friday?</button><button>Invoice #2041</button></section><section class="det" inert><button class="back">← Inbox</button><h3></h3><p>Message body goes here.</p></section></div>
var w = document.querySelector('.pt7'), l = w.querySelector('.list'), d = w.querySelector('.det'), last;
l.querySelectorAll('button').forEach(function (b) { b.onclick = function () { last = b; d.querySelector('h3').textContent = b.textContent; w.classList.add('open'); d.inert = false; l.inert = true; d.querySelector('.back').focus(); }; });
d.querySelector('.back').onclick = function () { w.classList.remove('open'); d.inert = true; l.inert = false; last.focus(); };
About these CSS Page Transitions
Transitions explain navigation
A good transition shows where you went: forward slides from the right, back from the left, a detail view grows out of the item you tapped. Keep them short, around 300 to 400ms, so they never feel like waiting.
The View Transitions API
document.startViewTransition(update) takes a snapshot, runs your DOM change and animates between the two. Giving matching elements the same view-transition-name morphs one into the other. Browsers without it just update instantly.
Hidden pages must stay hidden
When two pages are stacked for a cross-fade, make the inactive one inert so keyboard focus and screen readers cannot wander into it, and move focus to the new page's heading or first control.
Turn them off on request
Large movements are the motion most likely to cause discomfort. Under prefers-reduced-motion: reduce swap the content instantly or use a quick fade.
Frequently asked questions
Do these work between real page loads?
These demos switch views inside one page. For full page loads, cross-document view transitions use @view-transition { navigation: auto; } in browsers that support it.
Will transitions hurt performance?
Not if they animate transform, opacity and clip-path, which the browser can run off the main thread.
Is a JavaScript framework needed?
No. Each demo is plain HTML, CSS and a few lines of script.