Next route/WORK
INTERACTION_164CSS + JS
Shared Element Transition
A card image flies into position on the detail page.
Transitions / LightView
A curved wipe sweeps across the screen between pages. The path parked off-screen before the trigger; morphing the path across the viewport and out the other side once a route change.
/* Liquid Wipe Transition — A curved wipe sweeps across the screen between pages. */
.liquid-wipe-transition {
--dur: 780ms;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--stagger: 60ms;
will-change: transform, opacity;
}
.liquid-wipe-transition > * {
opacity: 0;
transform: translateY(14px);
animation: liquid-wipe-transition-in var(--dur) var(--ease) forwards;
animation-delay: calc(var(--i, 0) * var(--stagger));
}
/* the path parked off-screen -> morphing the path across the viewport and out the other side */
@keyframes liquid-wipe-transition-in {
to {
opacity: 1;
transform: none;
filter: none;
}
}
@media (prefers-reduced-motion: reduce) {
.liquid-wipe-transition > * {
opacity: 1;
transform: none;
animation: none;
}
}import { useEffect, useRef } from "react";
/** Liquid Wipe Transition — trigger: a route change */
export function LiquidWipeTransition({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
el.dataset["state"] = "in";
return;
}
const io = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
el.dataset["state"] = "in";
io.unobserve(el);
}
},
{ threshold: 0.2 },
);
io.observe(el);
return () => io.disconnect();
}, []);
return (
<div ref={ref} className="liquid-wipe-transition" data-state="out">
{children}
</div>
);
}A card image flies into position on the detail page.
Panels close over the page then lift to reveal the next route.
The outgoing page blurs out and the next resolves into focus.
Routes slide horizontally in the direction of travel.