Target the new view snapshot on the root with ::view-transition-new(root) and animate its clip-path. A CSS variable holds the starting inset per theme, so the same keyframes can wipe from a different edge depending on which theme is being entered.
::view-transition-new(root) {
animation: reveal 0.5s ease;
}
[data-theme='dark'] {
--from: 0 0 100% 0;
}
[data-theme='light'] {
--from: 100% 0 0 0;
}
@keyframes reveal {
from {
clip-path: inset(var(--from, 100% 0 0 0));
}
to {
clip-path: inset(0 0 0 0);
}
}Trigger the swap through document.startViewTransition so the browser captures before/after snapshots to animate between. Toggling the theme attribute inside the callback is what makes the [data-theme] variable available for the keyframes above.
function toggleTheme() {
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'
if (!document.startViewTransition) {
document.documentElement.dataset.theme = next
return
}
document.startViewTransition(() => {
document.documentElement.dataset.theme = next
})
}inset(0 0 100% 0) means clipped fully from the top, so the reveal grows downward; the light theme uses the opposite edge to feel directional. Falls back to an instant swap in browsers without View Transitions support.
/* inset(top right bottom left) */
/* --from: 0 0 100% 0 -> wipe down from the top */
/* --from: 100% 0 0 0 -> wipe up from the bottom */