When the user has a history stack with element transitions, it's possible for them to scroll the element out of view on one or both pages. This can cause elements to translate thousands of pixels across the document and should be avoided.
Here are a few product-level use cases that I would consider good practice with a the proverbial grid-to-hero-image CSS transform.
Scroll Position Priority
G = Grid
H = Hero
IN = In View
OUT = Out of View
In this case, we avoid transforming unless both elements are visible and scroll position is always maintained.
navigation | transition |
---|---|
G IN -> H IN | transform |
H IN -> G IN | transform |
G IN -> H OUT | fade |
H IN -> G OUT | fade |
H OUT -> G OUT | fade |
G OUT -> H OUT | fade |
H OUT -> G IN | fade |
G OUT -> H IN | fade |
Element Visibility Priority
Here we prioritize the visibility of the destination element at the expense of scroll position. Note we never scroll the origin page.
sIV = scrollIntoView()
navigation | transition |
---|---|
G IN -> H IN | transform |
H IN -> G IN | transform |
G IN -> H OUT | sIV -> transform |
H IN -> G OUT | sIV -> transform |
H OUT -> G OUT | siV -> fade |
G OUT -> H OUT | sIV -> fade |
H OUT -> G IN | fade |
G OUT -> H IN | fade |
function ImagesNav() {
let images = useLoaderData();
return (
<div>
{images.map(image => (
<NavImage key={image.id} image={image} />
))}
</div>
);
}
function NavImage({
img,
}: {
img: { id: string; title: string; src: string };
}) {
let href = `/movies/${img.id}`;
let vt = useViewTransition(href);
return (
<div>
<p style={{ viewTransitionName: vt ? "my-movie-title" : "" }}>
{image.title}
</p>
<Link to={href}>
<img
src={image.src}
style={{ viewTransitionName: vt ? "my-movie-image" : "" }}
/>
</Link>
</div>
);
}
function MoviePage(img) {
let movie = useLoaderData();
return (
<main>
<h1>{img.title}</h1>
<img src={img.src} style={{ viewTransitionName: "my-movie-title" }} />
<p style={{ viewTransitionName: "my-movie-image" }}>
{movie.description}
</p>
</main>
);
}
function NavImage({
img,
}: {
img: { id: string; title: string; src: string };
}) {
let href = `/movies/${img.id}`;
let vt = useViewTransition(href);
return (
<NavLink unstable_transitions>
{({ isActive, isPending, isTransitioning }) => (
<>
<p style={{ viewTransitionName: vt ? "my-movie-title" : "" }}>
{image.title}
</p>
<img
src={image.src}
style={{ viewTransitionName: vt ? "my-movie-image" : "" }}
/>
</>
)}
</NavLink>
);
}
function NavImage({
img,
}: {
img: { id: string; title: string; src: string };
}) {
let href = `/movies/${img.id}`;
let vt = useViewTransition(href);
return (
<NavLink
unstable_transitions
className={({ isPending, isTransitioning }) =>
isTransitioning ? "animate" : ""
}
>
<p>{image.title}</p>
<img src={image.src} />
</NavLink>
);
}
Need to go crazy? Drop down to a lower API
useViewTransitions(({ currentLocation, nextLocation }) => {
// do stuff now
let match = matchPath("/movies/:id", nextLocation.pathname);
return transition => {
transition.ready;
transition.finished;
// do stuff later
};
});